Domain Driven Design, Domain objects, attitude about Setters

a 夏天 提交于 2019-11-29 03:56:17

I'm contributing this answer to complement Roger Alsing answer about invariants with other reasons.

Semantic information

Roger clearly explained that property setters don't carry semantic information. Allowing a setter for a property like Post.PublishDate can add confusion as we can't know for sure if the post has been published or only if the publication date has been set. We can't know for sure that this is all that is needed to publish an article. The object "interface" does not show its "intent" clearly.

I believe, though, that properties like "Enabled" do carry enough semantic for both "getting" and "setting". It is something that should be effective immediately and I can't see the need for SetActive() or Activate()/Deactivate() methods for the reason alone of missing semantics on the property setter.

Invariants

Roger also talked about the possibility of breaking invariants through property setters. This is absolutely right, and one should make properties that work in tandem to provide a "combined invariant value" (as the angles of triangle to use Roger's example) as read-only properties and create a method to set them all together, which can validate all the combinations in a single step.

Property order initialization/setting dependencies

This is similar to the problem with invariants as it causes problems with properties that should be validated/changed together. Imagine the following code:

    public class Period
    {
        DateTime start;
        public DateTime Start
        {
            get { return start; }
            set
            {
                if (value > end  && end != default(DateTime))
                    throw new Exception("Start can't be later than end!");
                start = value;
            }
        }

        DateTime end;
        public DateTime End
        {
            get { return end; }
            set
            {
                if (value < start && start != default(DateTime))
                    throw new Exception("End can't be earlier than start!");
                end = value;
            }
        }
    }

This is a naive example of "setter" validation that causes access order dependencies. The following code illustrates this problem:

        public void CanChangeStartAndEndInAnyOrder()
        {
            Period period = new Period(DateTime.Now, DateTime.Now);
            period.Start = DateTime.Now.AddDays(1); //--> will throw exception here
            period.End = DateTime.Now.AddDays(2);
            // the following may throw an exception depending on the order the C# compiler 
            // assigns the properties. 
            period = new Period()
            {
                Start = DateTime.Now.AddDays(1),
                End = DateTime.Now.AddDays(2),
            };
            // The order is not guaranteed by C#, so either way may throw an exception
            period = new Period()
            {
                End = DateTime.Now.AddDays(2),
                Start = DateTime.Now.AddDays(1),
            };
        }

Since we can't change the start date past the end date on a period object (unless it is an "empty" period, with both dates set to default(DateTime) - yes, it's not a great design, but you get what I mean...) trying to set the start date first will throw an exception.

It gets more serious when we use object initializers. Since C# don't guarantee any assignment order, we can't make any safe assumptions and the code may or may not throw an exception depending on the compiler choices. BAD!

This is ultimately a problem with the DESIGN of the classes. Since the property can't "know" that you are updating both values, it can't "turn off" the validation until both values are actually changed. You should either make both properties read-only and provide a method to set both at the same time (losing the feature of object initializers) or remove the validation code from the properties altogether (perhaps introducing another read-only property like IsValid, or validating it whenever needed).

ORM "hydration"*

Hydration, in a simplistic view, means getting the persisted data back into objects. For me this is really the biggest problem with adding logic behind property setters.

Many/most ORMs map the persisted value into a property. If you have validation logic or logic that changes the object state (other members) inside property setters you'll end up trying to validate against an "incomplete" object (one still being loaded). This is very similar to the object initialization problem since you can't control the order the fields are "hydrated".

Most ORMs allow you to map the persistence to private fields instead of properties, and this will allow the objects to be hydrated, but if your validation logic lies mostly inside property setters you may have to duplicate it elsewhere to check that a loaded object is valid or not.

Since many ORM tools support lazy loading (a fundamental aspect of an ORM!) through the use of virtual properties (or methods) mapping to fields will make it impossible for the ORM to lazy load objects mapped into fields.

Conclusion

So, in the end, to avoid code duplication, allow ORMs to perform as best as they could, prevent surprising exceptions depending on the order the fields are set, it is wise to move logic away from property setters.

I'm still figuring out where this 'validation' logic should be. Where do we validate the invariants aspects of an object? Where do we put higher-level validations? Do we use hooks on the ORMs to perform validation (OnSave, OnDelete, ...)? Etc. Etc. Etc. But this is not the scope of this answer.

Setters doesnt carry any semantic information.

e.g.

blogpost.PublishDate = DateTime.Now;

Does that mean that the post has been published? Or just that the publish date have been set?

Consider:

blogpost.Publish();

This clearly shows the intention that the blogpost should be published.

Also, setters may break the object invariants. For example if we have a "Triangle" entity, the invariant should be that the sum of all angles should be 180 degrees.

Assert.AreEqual (t.A + t.B + t.C ,180);

Now if we have setters, we can easily break the invariants:

t.A = 0;
t.B = 0;
t.C = 0;

So we have a triangle where the sum of all angles are 0... Is that really a Triangle? I'd say no.

Replacing setters with methods could force us to maintain invariants:

t.SetAngles(0,0,0); //should fail 

Such call should throw an exception telling you that this causes an invalid state for your entity.

So you get semantics and invariants with methods instead of setters.

The reason behind this is that the entity itself should be responsible for changing its state. There isn't really any reason why you should need to set a property anywhere outside the entity itself.

A simple example would be a entity that has a name. If you have a public setter you would be able to change the name of a entity from anywhere in you application. If you instead remove that setter and put a method like ChangeName(string name) to your entity that will be the only way to change the name. That way you can add any kind of logic that will always run when you change the name because there is only one way to change it. This is also a lot clearer then just setting the name to something.

Basically this means that you expose the behavior on your entities publicly while you keep the state privately.

The original question is tagged .net, so I'll submit a pragmatic approach for the context where you'd like to bind your entities directly to a view.

I know that that's bad practice, and that you should probably have a view model (as in MVVM) or the like, but for some small apps it just makes sense to not over-patternize IMHO.

Using properties is the way out-of-the box data binding works in .net. Maybe the context stipulates that the data binding should work both ways, and hence implementing INotifyPropertyChanged and raising PropertyChanged as a part of the setter logic makes sense.

The entity could e.g. add an item to a broken rule collection or the like (I know CSLA had that concept a few years back and maybe still does) when the client sets an invalid value, and that collection could be shown in the UI. The unit of work would later refuse to persist invalid objects, if it should come that far.

I'm trying to justify decoupling, immutability, and so forth to a large extent. I'm just saying that in some contexts a simpler architecture is called for.

A setter simply sets a value. It is not supposed to be "heavy" with logic.

Methods on your objects that have good descriptive names should be the ones "heavy" with logic and have an analogue in the domain itself.

I strongly recommend reading the DDD book by Eric Evans and Object-Oriented Software Construction by Bertrand Meyer. They have all the samples you would ever need.

I may be off here, but I believe setters should be used to set, as opposed to setter methods. I have a few reasons for this.

a) It makes sense in .Net. Every developer knows about properties. That's how you set things on an object. Why deviate from that for domain objects?

b) Setters can have code. Before 3.5 I believe, setting an object consisted of an internal variable and a property signature

private object _someProperty = null;
public object SomeProperty{
    get { return _someProperty; }
    set { _someProperty = value; }
}

It is very easy and imo elegant to put validation in the setter. In IL, getters and setters are converted into methods anyway. Why duplicate code?

In the example of the Publish() method posted above, I completely agree. There are times when we don't want other developers setting a property. That should be handled by a method. However does it make sense to have a setter method for every property when .Net provides all the functionality we need in the property declaration already?

If you have a Person object, why create methods for every property on it if there's no reason?

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!