5

I have been struggling with this concept in the context of web applications ever since I first read about it. The theory states that the domain objects should encapsulate their behaviour and business logic. A model which contains only data and has its logic somewhere outside is called an "anemic domain model", which is a bad thing. Also, the domain should not perform data access.

If for instance I had a social app which had a bunch of objects of type User, and users should be able to add other users as their friends, the User class should contain a method named Befriend(User user) so that I could do something like userA.Befriend(userB).

class User {

    Friends[] friends;

    void Befriend(User user) { ... }
}

However, the act of befriending might contain some restrictions and so I would have to do some validation in my Befriend method. Here are some purely theoretical restrictions:

  1. The user must not already be your friend
  2. You and the other user must not have common friends
  3. In Bucharest it must be raining

Now let's imagine that the friends lists might be huge, userA might have 50.000 friends, userB might have 100.000 friends. So, for validating 1 and 2 it wouldn't be efficient to eagerly pull the entire friends lists from the database when constructing the user object and then doing those checks in my Befriend method iterating the friends list. In the database I have indexes and checks like these would be trivial (and fast). So naturally I would prefer to put these queries somewhere in my Data Access Layer and use them whenever needed.

class FriendsRepository: IFriendsRepository {

    bool HasFriend(User user, User friend);
    bool HasCommonFriends(User userA, User userB);

}

But how am I supposed to use this object inside my Befriend method from my User object? People say domain objects must not use repositories (even through abstractions such as interfaces), though there seems to be some disagreement here. Say I violated this rule. Domain objects don't benefit from Dependency Injection so I would have to change my Befriend method to:

void Befriend(User user, IFriendsRepository friendsRepository) { ... }

Alright. Now what about the weather? That's something completely unrelated to our entity and that information comes from an IWeatherService. Again, I need it in my Befriend method.

void Befriend(User user, IFriendsRepository friendsRepository, IWeatherService weatherService) { ... }

This already makes me feel like this method does not belong inside the User class. I have a lot of external dependencies and I don't get Dependency Injection which sucks. But pulling this out from the User to a service (or whatever) inside my Application Layer makes my domain model anemic. I very rarely encountered methods which could either be executed without validation or contain only extremely simple validation rules, only depending on the immediately available properties on the said entity (like primitive fields for instance, such as Username string, ActiveUntil date etc.).

So I'm left asking: what kind of methods could naturally fit in the domain objects? Let's be honest, real apps often deal with huge amounts of data, many object relations and very complex validation logic. Rarely you only have to do trivial checks like "is this user over 12 years old?".

P.S.: I used that example purely for demonstration purposes. Please don't cling on it.

1
  • 2
    As an offtopic aside, I really appreciate the "raining in Bucharest" restriction being added to the example. So often we use simple examples that rely on "common sense" inferences (a good example of a flawed example is the "square vs rectangle" Liskov case), which often omit the sometimes "word of God" nature of the software requirements. So often a theoretical example results in people arguing over the specifics of the example instead of the actual question being posed, and the "raining in Bucharest" restriction really puts a stop to those kinds of distractions. – Flater Mar 22 '20 at 16:27
6

Arguably, the smallest method of encapsulation is a function.

float harmonic(int n) 
{ 
    float h = 1.0; 

    for (int i = 2; i <= n; i++) { 
        h += 1.0 / i; 
    } 

    return h; 
}

This function contains both code and data. When the function completes, it returns the data that it contains.

Classes encapsulate code and data in a similar manner. The only real difference is that you can have multiple functions (called "methods" in a class) operating on the same data, and multiple instances of that data.

Consider this partial code listing of a Complex Number class, obtained from here:

public class Complex {
    private final double re;   // the real part
    private final double im;   // the imaginary part

    // create a new object with the given real and imaginary parts
    public Complex(double real, double imag) {
        re = real;
        im = imag;
    }

    // return a new Complex object whose value is (this + b)
    public Complex plus(Complex b) {
        Complex a = this;             // invoking object
        double real = a.re + b.re;
        double imag = a.im + b.im;
        return new Complex(real, imag);
    }

    // return a new Complex object whose value is (this * b)
    public Complex times(Complex b) {
        Complex a = this;
        double real = a.re * b.re - a.im * b.im;
        double imag = a.re * b.im + a.im * b.re;
        return new Complex(real, imag);
    }
}

Both of these examples of encapsulation are, shall we say, "self-contained." They don't rely on any external dependencies to function.

The problem of encapsulating code and data gets a bit more thorny when you start designing business applications. The reason this is true is because business applications concern themselves primarily with collections of entities and the relationships between those entities. While there can and are operations that can be performed atomically on individual entities, this is rare. It is more common to perform operations that affect the relationships between entities or the state or number of entities within a collection. Consequently, most of the business logic is more likely to be found in object aggregates.

To illustrate, consider an ordinary business like Amazon. There's no particular reason to pick Amazon, other than it is unremarkably similar to other businesses in many ways: it has customers, inventory, orders, invoices, payments, credits: the usual suspects.

What can you encapsulate within a Customer entity that can be atomically executed, divorced from other entities? Well, maybe you can change their last name. That's a data change in the database that can happen automatically in a repository somewhere, using an anemic data model. Perhaps you can change their password hash. That requires some logic, but it's unlikely to live in the Customer entity. It's more likely to exist in some security module.

All of the interesting business logic lives outside of the fundamental entities. Consider an Invoice, which is not an individual entity, but rather an aggregate of several entities. What can you do inside an Invoice class, divorced from the rest of the system? Well, you can change the shipping address. That's simply a change to a foreign key in the Invoice entity. You can calculate a Total (the sum of the line item quantities and costs), and finally we get to some non-trivial logic that can be encapsulated in the entity itself. Maybe the line items have a line-item total property on them, so there's a bit of logic there.

But what if you want to calculate a balance? Now you have to go somewhere else besides the Invoice to make that calculation, because the Invoice doesn't know anything about all of the other invoices (by design). That could happen in the Customer entity, but it's just as likely to occur in some Accounting module elsewhere.

And then you have linking entities, entities whose sole purpose is to provide connections between entities at the data level. There's generally no logic in those whatsoever.

So at the bottom of your data hierarchy are simple data transfer objects. When combined into aggregate objects, they become useful from a logic standpoint, and any or all of them are subject to processing by any number of software modules, treated as simply data. When you think about it, it doesn't really make much sense to bake a lot of business logic into something like a Customer object, because now you're tightly binding that object to your specific way of doing business.

Should classes encapsulate data and logic? Of course, when it is appropriate and useful to do so. The core idea in software design is suitability. There are no absolute principles; software design techniques must always be evaluated in the context of your specific system to determine if they are appropriate for your specific functional and non-functional requirements.

7
  • Thank you for your time and your answer. So I take it that the operations you could do inside domain objects should only affect the data they own (or aggregate) and should only affect/use the data that can be eagerly loaded into memory. But given these restrictions, I would be afraid to put anything in these objects to begin with. If I have a TodoList, it might make sense to consider the TodoList and its Tasks an aggregate, where the TodoList is the root. But what if my TodoList has to support over 100.000 tasks? – Ted Chirvasiu Mar 19 '20 at 15:32
  • Unless the TodoList knows how to dynamically load its tasks when needed (and those that are needed), probably via a repository, I all of a sudden need to push the logic outside to my application layer. And the question is, why wouldn't I put it there in the first place? – Ted Chirvasiu Mar 19 '20 at 15:34
  • The ability to handle 100,000 entities is the reason we have databases. A user of the system doesn't need to know about all 100,000. You only retrieve those todos that are specific to him. – Robert Harvey Mar 19 '20 at 15:42
  • All of them might be specific to him, accumulated through time. Of course, he will never see them all at once on the front-end (he might see them in a paginated table, or only the active todos), but he might have a search function. The search function would have to search for a string inside all 100.000 of them. Should I perform this search on my web server on inside my database? I would say in my database because there I can have full-text indexes. But then what would my aggregate root look like? – Ted Chirvasiu Mar 19 '20 at 15:59
  • class TodoList { Task[] tasks; } //This wouldn't make much sense if I lazy-load my tasks when needed and possibly only partially load them (say the first 10, or the results of a search query). – Ted Chirvasiu Mar 19 '20 at 16:00
3

What kind of logic can Domain Objects realistically contain?

Think "finite state machine":

Given message a
    When I start in state X
        Then I should end up in state Y
    When I start in state Y
        Then I should end up in state Z

In effect, our domain model uses copies of input data and its own internal logic to compute its new state.


Now, I think you are close to a hard part, but that you haven't quite identified it; specifically, which data gets locked when we are updating the domain object? There's not a lot of real difference between "pass two stale copies of friend lists to the domain object, and let it compute the mutual exclusion", vs "pass a stale copy of the mutual exclusion result to the domain object".

So I could call some database query to ask for a list of all mutual friends, and then when that data is a couple of nanoseconds old pass it to my domain model, and let it decide what to do.

Of course, during those few nanoseconds, and also while the domain model is doing its own work, the data in the database might change in a way that changes the answer to the query.

If that's not good enough, then we need to start thinking about how to make sure those lists are locked against modification while the domain model is doing its thing.

If the things we need to lock are in different databases, then we are going to be vulnerable to various consistency issues.

And notice that in some cases, such a lock simply isn't possible -- there's nothing I can do in my domain model to prevent the weather in Bucharest from changing. It's outside of my control, so I have to be content with a snapshot of information about the past....

And in some cases, where things get big enough and clumsy enough, you have to go back to the subject matter experts and ask "is this constraint under our control? because if it is both expensive and arbitrary, then maybe we should change it." And sometimes you can relax the constraint, and other times you need to spend more money on your architecture to fix it.

Truth be told, to some degree "domain objects" are an illusion we use to make the logic easier to manage. The data structures we maintain in transient memory are just a convenience for calculation; when we have to defend our claims in court, it's the durable representation of that information that matters. In this framing, the real thing that we are doing in the domain model is computing what command to send to update our persistent storage.

2
  • Thank you for you answer, data consistency is a problem that's definitely worth bringing up though not exactly my main concern. Assume my persistence is consistent and whenever I need to assure this, I can open a transaction. Also assume that we are fine with whatever the weather service tells us, regardless of the information being a bit stale in reality. The problem is that even in the context of a transaction, my database and my web server might live on different machines which do not share state. – Ted Chirvasiu Mar 19 '20 at 15:48
  • As you outlined, my web app needs some input state in order to compute the new output state. However, most (if not all) of my input state needs to be initially constructed from the database state. Since I cannot pull the entire database in the web app, I need to be mindful and request as little as possible. It is very hard to know in advance exactly how much info I will need. For instance, in my example, if I find that I'm already befriended with the userB, I will not need to pull his list of friends from the database in order to compare it to mine. – Ted Chirvasiu Mar 19 '20 at 15:53
2

The first couple of paragraphs of your question are spot on. All objects should expose only business logic instead of data. This gives the application a huge gain in maintainability, since everything happens locally to the object.

I agree with your logical conclusion, that some objects may need access to database or other external systems to provide useful business logic. This is the only way it would work, and I've been building software this way for almost a decade now. It works.

This is where the confusion is coming from:

People say domain objects must not use repositories

This comes from a completely different perspective. Most projects do not in fact use object-orientation the way you describe it. Most projects are entirely comfortable with anemic "objects". These kinds of, shell we say procedural designs work completely differently, and this restriction might make sense there. It doesn't apply to object designs.

Domain objects don't benefit from Dependency Injection

There is no such rule. Is this some restriction of some framework or library? To be clear, yes, your objects should get dependencies in their constructors just like everybody else. Why wouldn't they?

Fair warning: You are trying to do object-orientation right. Most projects (all I've seen in the last more than 20 years) don't do this. So you are deviating from the de-facto standard.

If you continue on this path, you may find yourself forced to rethink "repositories", your complete design, even question layered architectures.

2

I'm going to come at this from another angle. The answers so far have been great in a "understanding the essence of a system and it's limitations" kind of way. But I believe I can provide a more concrete answer:

"Model it."

In a way, you are misunderstanding the purpose of a domain model. It is not meant to represent the "things" in a system that interact with one another (that's what a physical model is for). Rather, it is meant to be a useful abstraction of the behavioral requirements of a system.

All we are really talking about here is "which steps of a process" are suitably modeled in your domain. For example (your example), given a User entity with the functional requirements you have delineated above:

  1. The User must not already be your friend
  2. You and the other User must not have common friends
  3. In Bucharest it must be raining

We can in fact derived a model that optimizes for these conditions. What does this model look like?

    class PotentialRelationship {

        int FromUserId;
        int ToUserId;

        bool AreAlreadyFriends;
        bool HaveCommonFriends;

        void Codify(WeatherInBucharest forecast);
    }

You see what I did there? I just re-framed our internal state such that it moves the computation of our values: AreAlreadyFriends and HaveCommonFriends into our database (as opposed to moving the business logic that consumes these values). Now we can simply hydrate our new optimized PotentialRelationship entity with the appropriate values (however they are derived) and keep our invariants in our domain.

I have yet to encounter a problem that cannot be modeled using the approach above. At some point in a business process a decision has to be made or a value has to be mutated given the result of some conditions. Where that result is computed is an implementation detail. If it needs to be done in a database so be it. Just keep the "decision"/"mutating" in your domain.

Said another way, our domain does not need to be given the responsibility of computing the intermediate values that make up a business decision, rather, it should be given the responsibility of mediating the decision itself. After all these decisions are the essence of our functional requirements right? How the inputs are gathered is an implementation detail.

And to be clear. I'm not necessarily recommending the above so much as providing "prior art" on the matter. Of course there are times where it doesn't necessarily make sense to adopt the above, where doing so may be over-engineering (or worse complicating), but it is possible to model this kind of invariant should the business value make doing so prudent.

This answer is meant to exist along side those already provided.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.