DDD and the use of Getters and Setters
Asked Answered
N

2

16

I've read a few articles/posts regarding the use of Getters and Setters, and how they help to defeat the purpose of encapsulation in domain model objects. I understand the logic behind not using setters - you are allowing client code to manipulate attributes of that object, outside the context of the object business rules and invariants.

Now this principal does still confuse me. For example, what happens if I need to change the value of a member variable of an object? For example, if the name of a person changes how can I reflect this in the model? At first I thought, well why not have a function called 'ChangeName' which let's me pass in the new name and it in turn can change the internal 'name' variable. Well.... that's just a setter isn't it!

What I need to clarify - if I were to completely eliminate setters, then in situations such as the above, am I supposed to solely rely on constructor parameters? Should I pass the new attribute value in place of the old attribute value via a constructor, after which I can persist the changes by passing the object to whatever persistence infrastructure I have?

These two articles are useful in this discussion:

  1. http://kellabyte.com/tag/ddd/
  2. http://typicalprogrammer.com/?p=23
Nadabus answered 28/11, 2011 at 3:13 Comment(0)
R
13

Well, this is a classic discussion. There are several other threads here in Stack Overflow about this.

But. Get/Set's (Auto Properties?) are not all bad. But they tend to make you construct your entities as "dead" data containers that only have prop and not methods. The signs of this is often called Anemic Domain - and have very little behavior. My recommendation is:

  1. Try to use prop as little as you can.
  2. Try to find groups of data that belongs together and SHOULD be together like ex. Firstname Middlename and Lastname. Another example is Zipcode, City, Street. These data is better to set through a method. It minimizes the chances for your entity to be invalid.
  3. Often data that belongs together can be grouped as a Value object.
  4. More Value objects tend to bring out more descriptive methods from your entity that are "Verbs" instead of your usually "Nouns" entities.
  5. More methods for your value objects also opens up for adding more behavior and maybe reducing your "Fat" services (maybe you don't have services with too much leaked business logic...).

There are more to say here... but a short answer. About setting data in constructor: I only do that if this entity cannot "live"/exist without that data. For entity Person I would say that Name maybe isn't that kind of important. But Social Security Number may be a candidate for constructor data. Or entity Employee must have Company in constructor, simply because an employee must belongs to a company.

Rem answered 28/11, 2011 at 12:58 Comment(0)
P
0

I think we should look at the principles of DDD and derive the correct answer from there.

Public auto-property getters/setters in C# are functionally just public properties. Using auto-property getters/setters is not inherently bad as long as there are no business rules regarding the correct values of the respective properties and no domain events that need to fire when those properties change.

Also, one should not build aggregates or entities that have only public auto-properties, as that leads to an anemic model and an anemic domain. Such an "aggregate" is not an actual aggregate, but more a DTO or Value Object.

Personally, I think that if we use property accessors (get/set) with bodies to integrate business logic, we can make our code a little bit more readable and probably a lot less verbose.

For example:

// Instead of this:

public DemoAggregate : IAggregate
{
  public string Name { get; private set; }

  public void ChangeName(string newName)
  {
    Name = Check.MinMaxLength(newName, 1, 100,
      $"{nameof(newName)} length must be between 1 and 100 characters.");
  }
}

/* MinMaxLength throws a business exception
 * if the new name is outside of the accepted range
 * otherwise it returns the value unchanged.
 */

// ...you can write this to get one method less:

public AltDemoAggregate : IAggregate
{
  private string _name;

  public string Name
  {
    get => _name;
    set => value = Check.MinMaxLength(newName, 1, 100,
      $"{nameof(newName)} length must be between 1 and 100 characters.");
  }
}

The only issue with the above is that, internally, you can bypass business logic if you set _name directly in some method. But if you're disciplined enough, I don't think it's an issue. It might seem scary for some though and I understand.

On the upside, if you're using something like Entity Framework, I think you can configure it to hydrate new instances by calling properties (not backing fields), thus preventing loading an invalid aggregate from the database (say if you're imported some bulk data that might contain some garbage). I haven't tested this though.

Expression bodied accessors are used in the second example just to show that you can reduce boilerplate a lot.

It's possible to use an expression bodied getter like above because strings have value semantics in C# so that expression returns a copy of _name, thus not exposing a reference to an internal variable.

Note that with C# 9 records for example, you have only value-based equality semantics. A record is still passed by reference! Since records should ideally be immutable (init only), you could return references to such records (which is more performant) and skip having to clone (which is simple for shallow clones but hard for deep clones).

If you have such an object inside an aggregate, for example a DDD Value Object that isn't an immutable record or a record that can be easily cloned, you need to make sure you're not returning a reference to an internal object that can be mutated, thus bypassing business logic and messing with aggregate integrity.

Take a List for example. You can use IReadOnlyList as the return type, but that is not enough if you're only casting a private internal property, because that reference can be "up-casted" outside back to a List and then used to modify it.

In this case you should also use the .AsReadOnly() method of List to return a new read only wrapper list over the elements of the original list.

Beware though that only the wrapper list is protected from change (it has no add or remove methods) but not the elements themselves. It's their responsibility to protected themselves from change.

EDIT:

I just realised that my example is not exactly correct. Such (private?) accessors with logic can be used for simple logic like making sure that when setting an end date, it is not before a start date, but for complex cases, setting an end date for example can happen for a number of reasons which must be all modelled as verbs, for example terminateContract(DateTime finalDay, string reason) or closeContract(DateTime closedEarlyDate), they are must more explicit on the reason to set the end date. In this case anyway, generic logic that should always be applied can live in the setter accessor (this provides deduplication of code) and per-case per-action logic can live in the specific action method.

Plasma answered 4/6, 2021 at 9:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.