Clean&Reactoring Entry Created: 05 Feb 2026 Updated: 05 Feb 2026

Defending the Data

In modern software architecture, "encapsulation" is often misunderstood as simply making fields private and exposing them via public Properties. However, true encapsulation is about localizing invariants—ensuring that data and the logic that manipulates it reside in the same place.

This article explores three powerful rules for enforcing encapsulation, moving beyond standard practices to create code that is safer, more maintainable, and easier to reason about.

Rule 1: Do Not Use Getters or Setters

The Rule: Do not use setters or getters (including public C# Properties) for non-Boolean fields.

The Explanation

In the C# world, we are taught to use Properties ({ get; set; }) as the standard way to expose data. However, exposing data—even through a getter—breaks encapsulation.

When you expose data, you create a Pull-Based Architecture. External classes fetch data from your object to perform calculations elsewhere. This turns your objects into "dumb" data containers and spreads your business logic (invariants) across "Manager" or "Service" classes.

To defend your data, you should aim for a Push-Based Architecture. Instead of asking an object for its data, you pass the necessary arguments to the object and ask it to perform the work. This is often referred to as "Tell, Don't Ask."

C# Example

❌ The "Pull" Approach (Violation)

In this example, the PostLinkGenerator pulls data out of Website and User. If the internal structure of User changes, the generator breaks.


public class Website
{
// Exposing state via properties
public string Url { get; private set; }

public Website(string url)
{
Url = url;
}
}

public class User
{
public string Username { get; private set; }

public User(string username)
{
Username = username;
}
}

public class PostLinkGenerator
{
// The logic lives here, far away from the data
public string GeneratePostLink(Website website, User user, string postId)
{
string url = website.Url;
string userName = user.Username;
return $"{url}/{userName}/{postId}";
}
}

✅ The "Push" Approach (Correct)

Here, we eliminate the getters. We push the requirement into the classes. The Website knows how to format its own link, and the User knows how to utilize the website.

public class Website
{
private readonly string _url;

public Website(string url)
{
_url = url;
}

// Logic is encapsulated here
public string GenerateLink(string username, string id)
{
return $"{_url}/{username}/{id}";
}
}

public class User
{
private readonly string _username;

public User(string username)
{
_username = username;
}

// The User class coordinates the action
public string GenerateLink(Website website, string id)
{
return website.GenerateLink(_username, id);
}
}

public class BlogPost
{
private readonly User _author;
private readonly string _id;

public BlogPost(User author, string id)
{
_author = author;
_id = id;
}

public string GenerateLink(Website website)
{
// No getters used. We delegate behavior.
return _author.GenerateLink(website, _id);
}
}

Refactoring Pattern: Eliminate Getter or Setter

  1. Make the getter/property private.
  2. Fix the resulting compiler errors by moving the logic into the class (Push Code Into Classes).
  3. Delete the unused private getter.

Rule 2: Never Have Common Affixes

The Rule: Your code should not have methods or variables with common prefixes or suffixes.

The Explanation

If you find variables like depositAmount, depositCurrency, and depositDate floating in a method or class, it indicates that these elements belong together but haven't been unified. Common affixes are a cry for help from your code, signaling a missing abstraction (Class).

By grouping these into a dedicated class, you satisfy the Single Responsibility Principle. You can also enforce invariants on that specific group of data (e.g., ensuring Amount is never negative) in one place, rather than repeating checks globally.

C# Example

❌ The Violation

Here, the player prefix is used on multiple variables in the global scope or a large game loop class.

public class Game
{
// Common "player" prefix suggests missing cohesion
private int _playerX;
private int _playerY;
public void DrawPlayer(Graphics g)
{
g.FillRectangle(_playerX * 10, _playerY * 10, 10, 10);
}

public void MovePlayer(int x, int y)
{
_playerX = x;
_playerY = y;
}
}

✅ The Refactoring (Encapsulate Data)

We extract these variables into a dedicated Player class.

public class Player
{
private int _x;
private int _y;

public Player(int x, int y)
{
_x = x;
_y = y;
}

// The behavior follows the data
public void Draw(Graphics g)
{
g.FillRectangle(_x * 10, _y * 10, 10, 10);
}

public void MoveTo(int x, int y)
{
_x = x;
_y = y;
}
}

public class Game
{
// Much cleaner abstraction
private readonly Player _player;

public Game()
{
_player = new Player(1, 1);
}

public void Draw(Graphics g)
{
_player.Draw(g);
}
}

Refactoring Pattern: Encapsulate Data

  1. Create a new class to hold the related variables.
  2. Move variables into the class (initially keeping getters/setters if necessary).
  3. Update call sites to use the new class instance.
  4. Apply Rule 1 (Eliminate Getters/Setters) to move behavior into the new class.

Rule 3: Enforce Sequence

The Rule: Eliminate "Sequence Invariants" by using the constructor.

The Explanation

A "Sequence Invariant" occurs when the code requires method A to be called before method B, but the compiler does not enforce it. For example, needing to call Connect() before Query(). If a developer forgets the order, the application crashes at runtime.

We can eliminate this risk by doing the required work in the Constructor. In Object-Oriented Programming, the constructor is guaranteed to run before any instance method. If the initialization happens in the constructor, it creates a guarantee: If you have an instance of this object, it is in a valid state.

C# Example

❌ The Sequence Invariant

Here, the developer must remember to call Capitalize() before Print(). This is brittle.

public class MessagePrinter
{
private string _value;

public void SetValue(string value)
{
_value = value;
}

// Sequence invariant: This must be called before Print
public void Capitalize()
{
_value = _value.ToUpper();
}

public void Print()
{
Console.WriteLine(_value);
}
}

// Usage
var printer = new MessagePrinter();
printer.SetValue("hello");
// If I forget printer.Capitalize(), the output is wrong!
printer.Print();

✅ The Enforced Sequence

By moving the capitalization logic into the constructor (or a method called by the constructor), we remove the invariant. It is now impossible to have an un-capitalized CapitalizedString.

public class CapitalizedString
{
private readonly string _value;

// The constructor enforces the sequence
public CapitalizedString(string input)
{
if (string.IsNullOrEmpty(input))
throw new ArgumentException("Input cannot be empty");
_value = input.ToUpper();
}

public void Print()
{
// No need to check if capitalized; the type guarantees it.
Console.WriteLine(_value);
}
}

// Usage
var capString = new CapitalizedString("hello");
capString.Print(); // Guaranteed to be "HELLO"

Summary

Defending your data is about more than just private keywords. By adhering to these rules:

  1. Avoid Getters/Setters to force behavior into the classes that hold the data (Push vs. Pull).
  2. Eliminate Common Affixes to discover hidden abstractions and improve cohesion.
  3. Enforce Sequence via constructors to rely on the compiler rather than human memory.
Share this lesson: