Depend on DDD Entities or Interfaces

dependency-inversiondomain-driven-designentityinterfaces

My understanding is that the dependency inversion principle does not apply to entities, because entities basically are your application – it generally doesn't make sense to swap out a different implementation of, for example, Student.

However, one of my coworkers brought up a point. We work for an umbrella company that owns other companies. Often, the logic is very similar and the entities can just have a few "if(Company==1){…}else{…}" in them. Occasionally, however, there are many differences between them.

In this case, it was suggested to have, for example, a Class Teacher entity that contains IEnumerable, as well as two separate entities, StudentCompany1 and StudentCompany2 that implement IStudent.

In this case, would it make sense to do such a thing? Or should the logic all go into Student, even if complicated? Or maybe some other solution, such as inheritance? Or is my original assumption wrong, and entities should not depend on each other but rather on abstractions of each other?

Best Answer

Well, well, let's get something that is bothering me out of the way first...

if (Company == 1)
{
    //This is how code should NEVER look like
}

OK, now that I took it out of me it, let's get to the point!

Short Answer

The short answer to your question is yes, it does make sense to make entities depend on abstractions in some occasions. Once you translate your Domain Design to an Object-Oriented library, different rules come into play and, by these rules, there are no "Entities"... it's just values, abstractions and behaviors, so anything is allowed (as long as it makes your life easier, of course)!

Long Answer

Object-Oriented Programming and Domain-Driven Design are not the same thing, nor are they each other's "blessing". They are distinct philosophies, dealing with distinct problem domains. Entities represent something almost entirely philosophical in Domain-Driven Design, as do aggregate roots, value objects, etc. Putting them all together using Object-Oriented Programming is simply using a means (one of many, really) to achieve an end.

Entities happen to be representable as state-holding units (what we call classes) and these can be aggregated in other such units to enforce a desired consistency between otherwise unrelated elements (or elements related to each other only through philosophical descriptions and meanings). In that sense, a Student and a Teacher are expected to be found close to some School or Department entity conceptually. These relations are not always straightforward and, more often than not, they are not unique.

Using Object-Oriented Programming techniques helps you define your domain model and describe the state of all entities, as well as the relations between different entities. The point here is that you must remember that when talking about O-OP and D-DD, you are actually talking about two different things and, therefore, there is no "natural" way to translate or exchange concepts or ideas between the two. That is to say, just because Object-Oriented Programming thrives on abstractions and is heavily based on contracts (interfaces), it is not necessary that D-DD can also benefit from these concepts (all the more as D-DD speaks of contracts as the relations between concrete elements of reality). Think about, for example, how useful an abstraction of a Point with 2 coordinates would be, in almost any domain. What else can play the role of a Point besides... a Point? Why make a Point an interface, then?

Because...

Entities have state but they can also have behavior. And while state is tangibly defined, behavior can always be abstracted away! This is where interfaces come into play! Let's get back to your original question and thought:

[...] it generally doesn't make sense to swap out a different implementation of, for example, Student.

If you think of a Student entity as, for example, a human with a Name, an ID, an Age, a Sex, the Courses they are enrolled in, etc. indeed, it does not. Then you don't need O-OP to achieve anything. You can create an elaborate database and get away with equally elaborate queries about everything.

But how about thinking of a Student entity as a dynamic element with behavior. What if the Student object is tasked to give you information useful for your problem, rather than act as a data container? What may not appear very intuitive right now, it just might when you consider some use cases. Compare, for example, the following two definitions (C#-like code):


public class StudentEntity
{
    ...

    //OK, you could have a dedicated type for Age,
    //but let's keep it simple for the moment.
    public int Age { get; }

    ...
}

public interface IStudent
{
    bool IsAdult();
}

If the StudentEntity is your implementation, you need a number of Service objects to perform, for example, queries with respect to the StudentEntity. Compare, then, the following two excerpts:

Excerpt 1 is based on the StudentEntity class to retrieve information and make "judgments".

public class StudentQueryService
{
    public bool IsStudentAdult(StudentEntity student)
    {
        if (Country == Countries.Germany)
        {
            return student.Age >= 18;
        }
        else if (Country == Countries.Japan)
        {
            //It may get even more complicated...
            if (date of check is before 31 March 2022)
            {
                return student.Age >= 20;
            }
            else
            {
                return student.Age >= 18;
            }
        }
        else if (...)
        {
            ..
        }
        //and so forth!
    }
}

Excerpt 2 simply relies on asking the interface, so the unknown implementation behind the interface answers.

public abstract class StudentBase : IStudent
{
    protected int _age;

    public abstract bool IsAdult();
}

public class SwedishStudent : StudentBase
{
    public override bool IsAdult() => _age >= 18;
}

public class JapaneseStudent : StudentBase
{
    public override bool IsAdult()
    {
        if (date of check is before 31 March 2022)
        {
            return _age >= 20;
        }
        else
            return _age >= 18;
    }
}

//And so forth...

How well does the first excerpt fare in comparison to the second in terms of maintainability? Well, frankly, not too well. If the law changes, you have to inspect ALL ### lines to ensure that the new legislation is now correctly represented. Compare that to just focusing on ThaiStudent and simply correcting 1-2 lines of code, so they can keep using it in Thailand after your latest update patch!

OK, I have to admit that the example above is extraordinarily detached from reality, but the purpose is to make it clear that interfaces are just a means to an end (i.e. to abstract behavior) and it doesn't matter if you employ them to abstract away the specifics of an OS service, a web-request or the behavior of a Domain Entity. They always have a potential to be useful!

In short, if you want a truly useful (and far from anemic) Domain Model, you can "drag" your rules, limitations, interaction constraints, etc. inside the Domain elements, as behavior (i.e. methods). "Blessing" some of your Domain Entities with behavior will lead you to wonder whether these could be enacted differently based on circumstances. So, in these cases, if you want to stay as clear as possible of if{...} else if {...} else if {...} chaos zones, you would create different implementations of domain entity interfaces for your domain entities, at least where it makes sense to do so.

Just because, in the real world, we (hopefully) don't discriminate between students of different nationalities, let alone we don't really ask students whether they are of legal age and just trust their answers, in the O-OP world, tasking the relevant entities with the management of such complications and details effectively hides them from us and lets us focus on developing the rest of the code with somewhat less of an information burden.

Remember, we borrowed ideas from O-OP and applied them to D-DD. It is was totally unnecessary but it offered a layer of flexibility that has the real potential to ease maintenance in the long run!

Related Topic