Objects are the heart of object oriented design and development, but not only. Objects are the center of the enterprise applications and systems, and in this context we refer to them as Business Objects. Other authors call these many different names (e.g. business entities) when explaining different patterns, practices and recommendations, but here I don't want to create associations with other paradigms that may be the source of those terms (such as DDD). Let's just call them Business Objects, or just Objects. Reason for insisting on this is that, I don't want to create or endorse an incorrect opinion that these object design patterns and practices are only relevant to the specific approaches. Remember that DDD and other similar terms only refer to the set of recommendations, and this is not a "All or Nothing" decision whether to use these recommendations. Point of this statement is, if you have poor object design in your projects that requires improvement, don't say "but we are not following DDD", because this is a quite silly excuse.
It's unfortunate to see how the importance of Business Objects is downgraded by poor designs, and their purpose is not correctly fulfilled. Reason behind this is engineers being unaware of the proper object designs, or not willing to be better educated on the topic at all. As a consequence, most of the software that is developed as of now, is full of bugs, hard to understand, hard to maintain and extend with new capabilities. It's surprising that many engineers believe that this is the way the software is supposed to be, but it's not. My strong belief (based on my experience) is that developing a software and keeping it in the maintainable state is as simple as following correct patterns and design guidelines for the heart of your systems, and mostly for business objects. You may not see the connection between these issues and Business Objects, but I do. I believe that correctly designed business objects can solve most of the problems that I mentioned.
In this article, I will try to draw the connection between problematic software projects and incorrect objects inside them; I will also outline the ways to solve these issues.
Here is the problem I'm describing: it's very common to see objects designed as simple data containers (a.k.a. property bags, with all its properties readable and writable at the same time), and then there are numerous "smart" classes that create such objects, manipulate them in all sorts of ways, pass them around, again manipulate them, and then destroy these objects when the time comes. 99% of the software systems that I have looked into (and I have looked into many systems) are written exactly like this - with "smart" classes. I bet you have seen (and most likely have written) such software too.
The described design is easy to understand by all engineers of all levels (juniors, seniors, architects) when explained as a pattern. For the newly born software projects, this approach also helps deliver first few features faster than the "right approach" (which I'm about to explain later), and this sounds like another benefit for it, and quite tempting argument to start with it. But please don't be fooled - when the project matures and the codebase grows, the pace of delivering new features slows down exponentially, requiring constant analysis of the code by every engineer who is new to the team, or who is unaware of every little detail that happens in the system. If you have a team of couple of engineers, and if you are planning to keep them with this same project forever, then maybe this is the right choice for you. However, in real world, engineers come and leave companies, and thus the project goes through many hands before its completion. In such situations, the mentioned approach is going to suck up budget, time, and resources, so I strongly recommend against it.
Here are the major frustrations that I have faced when working on projects which had poorly designed objects in them:
Note: Just for reference, this approach is officially an anti-pattern, and is referred to as "anemic domain model" by many authors. However, since I've promised to keep away from patterns and their names, I will just continue the way I started (for your own sake, because this recommendation is relevant regardless you "follow" the patterns or not).
Do you understand the issues I've described above? have you felt these frustrations too? If so, that's already a step forward. Now, do you want to solve them too? Then read on.
I've split the solution into sub-sections so that you can grasp the concept step by step.
Let's start with foundational concepts. Any business object you write in your code should fulfill these expectations:
Above definitions sound simple and straightforward, so simple that many people simply overlook them. I'd suggest that you read them again, and then continue to the next section below.
First, let's distinguish these two beasts from each other.
I often ask this question during a technical interview for the engineering candidates: "what's the difference between business objects and DTOs?" and very often I hear answers like
It does not go too far until I ask a strict question: "so by looking at the class in the code, how can I say if it's a business object or DTO?" or "what are the design guidelines for business objects against DTOs? Hint - they are not the same". And this is where almost all candidates (even senior ones) get stuck... frustrating. But at least I understand who writes the software which scares me to death.
So, let me give you the main difference, so that you don't get stuck on interviews with people like me again:
Business objects represent business entity's state and behavior, while DTOs are just package of data. It goes even further - business objects enforce constraints (have implementation code in them) which help them stay always consistent, at any point in time, while DTOs are open for manipulations and unrestricted changes (they are just property bags).
In other words, business object is the single source of truth about itself, anytime you look at it. You can't usually say the same about DTOs.
Above statements are crucially important, and should be used when trying to delineate what is business object and what is not.
Since we're talking specifically about DTOs, they are not business objects because they don't have constraints. In other words, if you have a DTO, you can put any kind of data in it, and it won't complain, won't throw an exception, and can be sent to anybody without worrying that there's something wrong with it. At any point in time, when you receive a DTO (e.g. through the wire), you cannot be sure that it's correct and consistent, because you know it can be modified by anybody else or by yourself, without much thinking whether the data in it is correct based on any business rules. Don't even bother to have "validator" (or any other fancy mechanism) in your code which puts "only" validated data into the DTO - that does not make the DTO consistent, because nothing forces the producer of a DTO to use your validator, because DTO can be constructed and modified without your validator too - that's it, it's not consistent.
Now that we are clear that there is a difference between DTOs and business objects, let's see how exactly we should design and implement business objects. Basically, let's see what goes into business objects that does not go into DTOs.
If you follow these design guidelines, you will be able to avoid all the issues described above. These guidelines are just step by step implementation of the business object's definition statements which we already mentioned above at a higher level.
Rules for designing business objects are simple to say, but can be a bit challenging to implement, especially if you are used to designing them poorly. I will go through the rules and will provide you some guidelines on how to specifically implement them in real life.
State means object's fields. To encapsulate it, Business objects should be mostly read-only classes, and that does not mean immutable necessarily. Read-only means that the class appears to be read-only when we look at it from the outside. To achieve this, make all fields private, and avoid exposing writable properties which do nothing other than writing to the field (no cheating!).
When done with this step, your fields (your object's state) should already feel much more secured than before.
Behavior is how the state can mutate (change). Obviously, if the fields are going to be private, we need to expose a way to change them, which means to change the state. For this purpose, a Business Object should expose methods (behavior) that change the state, and these methods should be the only way to change the state.
Inside such methods, we will have several things happening - validation of the input data (method parameters), enforcement of the constraints (method parameters - even valid values - should not put the object in the incorrect state), other routine calls, dispatching events (I will discuss this part below in more details), and finally changing the actual state (fields). Basically, method is ensuring that the business object's state transitions from the current correct state to the next correct state, and nothing else.
Assignment of a property (through the writable property) is fine in rare cases, when it's guaranteed that the assignment keeps the state in consistency. In other words, if changing a field associated with the property can be done in isolation, and if this always results in the correct state of the object (as a whole), then it's reasonable to have such property exposed as writable; otherwise, this field should be changed as part of the method implementation, together with other fields, whatever makes sense in combination, from business requirements standpoint. e.g. if you have fields for the Employee object's start date, duration, and end date, duration and end date should be changed only together, otherwise object can be put into an invalid state by changing only one of these without the other (if duration changes, end date should be recalculated at the same time). Because of this, you can't just have writable properties for these two fields (thus allowing assignment to one without the other). Instead, it's better to have a method (e.g. SetDuration) which takes duration, and then calculates the project's end date accordingly before it returns. Thus, object moves from one correct state into another correct state - exactly as expected.
This one is easy, and the above sections already covered big part of it. Basically, you need to envision your object between method calls, where it's always in the correct state. Method call moves your object from one correct state into another correct state.
Eventually, every method has logic that somehow changes the state, but only so that at the end of this operation, the state is correct again. This is achieved by all the instruments already mentioned: input validation, domain logic encapsulation into the method, constraints, and so on. Just don't let your method make the state inconsistent. And since the methods are the only way to manipulate the state, then you have already ensured that the object's state can never be incorrect, as long as its own methods don't corrupt it.
Point of this section is that you still need to do the thinking, but that effort goes toward defining methods that change fields (state).
Above I mentioned that events should be dispatched by the business objects. I meant events that describe some specific situation that the business is interested in. I've heard many arguments against business objects dispatching events, which is just a result of the confusion or not completely understanding what an event is.
Many people describe an event as "when something happens", which can be interpreted as "when something happens anywhere in the system". However, let's rethink this statement - what can happen in the business-oriented software system? Answer is - only a state change of a business object, and nothing else.
For example, let's imagine couple of different situations ("something happened"), and see how they translate into events:
So, in short, anything that happens in a business software system, is a result of the business object changing its state. If the event needs to be fired, but nothing was changed prior to this, it just means that there's something wrong with the business object's design. Any change which results in an event, should be expressed as a state change.
Alright, now that we've decided that every event is a result of the business object's state change, then next question is - how do we fire the event to the rest of the world?
If you recall, business object's method is responsible to change the state, and nothing else has this capability. So, it's obvious that the method itself can fire the event when the state was changed, because that's the only place where we can guarantee the state was changed. If you are firing an event about a state change of the object from outside of this object, then your code is lying to the rest of the system that something was changed. Code that fires events on behalf of some other objects, is brittle and tightly coupled to those other objects. Start firing events from where the state is really changing, otherwise the state might stop changing (somebody changed the source object), but your code will continue firing this event (somebody forgot to change your code too, because it was not in the same place where the state was changing).
No more special classes that can change business object's state without asking business object to do so.
As I've described above, traditional and completely incorrect approach is to have many "smart" classes (e.g. Controllers, Managers, Repositories, etc.), each of them carrying on a different kind of business operation, while also directly manipulating states of the business objects. You might be used to this approach, and I encourage you to stop doing it... but I don't need to try too hard. If you follow all the above guidelines, you won't be able to design such special classes anymore, because your business objects won't allow direct manipulations from the outside, as opposed to DTOs for instance.
So, this guideline is really just a consequence - you get it for free if you follow the previous guidelines.
One last thing to mention is, these "smart" classes used to hold all the business logic before. With the right approach, business logic will be expressed where it's supposed to be - inside business objects. And this is an important and valuable change: remember that business objects are the house of the business logic, they are the owners of their own state, and they are the source of the business (domain) events. Centralizing all these concepts into business objects makes it much easier to understand how the system functions - everything's built around business objects.
Now that we've agreed that business objects manage their own state, natural question comes to mind - where do they originate from with that state which is always correct? Where is that magic hole?
That's where repositories come into picture - they give you the business objects with the state that they had when they were last persisted. And since the persistence part is also controlled by the same repositories, it's never a problem for the repository to give you back exactly what you gave to it once - an object with the correct state.
In other words, your repositories should be able to load and save business objects, from whichever specific storage you want, without losing or corrupting their state between saves and loads.
And since I've touched repositories, you will (and should) start thinking about things that are happening inside repositories - retrieving data from some kind of storage (e.g. database table rows), then mapping it to the business objects, and then returning resulting business objects back... wait, did I say "mapping"? Did things like "automapper" come to mind? but how would you map from the database rows (which are almost like DTOs - with all properties supporting reads and writes) into business objects when these business objects won't anymore expose the writable properties (based on the above guidelines)?
And this brings up next guideline.
Period. These frameworks have a great purpose to map from simple objects to other simple objects as long as the property names match and all the target properties are writable, but don't take it too far - our business objects are not going to expose writable properties as we've already agreed.
I've seen incorrectly designed business objects that are suitable for automapper mappings (i.e. with all writable properties), but that smells like fitting your design under frameworks, rather than using the correct tools for the job. If you go with all writable properties, then how do you ensure all the goodness we've discussed above? - consistency, state encapsulation, no "smart" objects... Stop the madness, keep the design right, and then only choose the right framework to accomplish what you want.
In other words, I will not say that automapper and other similar frameworks encourage you to design your business objects incorrectly; instead, they don't mention where they should not be used (not their problem, yours!).
So, use automatic mapping frameworks (such as automapper) with great caution. Specifically, don't design your business objects poorly just because you want to automatically map property values - you're compromising too much for a minor convenience.
I guess I've covered it all.
If you just try and implement your software with all the guidelines I mentioned, you will feel relieved when learning it, debugging it, or adding new functionalities to it. It just becomes very simple:
Oh, and by the way, don't think that I'm a dreamer, because this is exactly how I implement my code at all times, so I've tried it myself many times already. Every time I write software, I enjoy it because of the approach described above, and all the benefits it gives me back.
Unfortunately, I have also tasted the incorrect designs too, when working with teams that had already implemented it poorly - so it was too late in the game to redesign everything they had "invented". But I hope those teams will change their minds by discovering and reading papers like this.
Obviously, you can use better terms for things I've written here (specifically if you are fond of DDD, or design patterns, or many other software theories and practices), but I kept it mostly unrelated to those, because these recommendations should not be taken as part of other guidelines or standards necessarily.
Nothing here conflicts with any other patterns, paradigms, frameworks, or approaches that I know and follow, or respect and believe in. They are too many, and will never fit in a single article like the one above. However, you can either read them or book a technical training for your team, delivered by myself. Happy learning!
Author of the above content is Tengiz Tutisani - owner and technical leader at tutisani.com.
If you agree with the provided thoughts and want to make it part of your team's culture, we can help.
We provide in-person, immersive technical workshop training courses around Software Architecture, Domain-Driven Design, and Extreme Programming topics. We also develop software solutions.