During the last few lectures on Advance Software Engineering we talked about concepts such as layered architecture, entity objects and value objects etc which all come under the term which was coined by Eric Evans as Domain Driven Design. It is an approach for software design which is based on arguments such as;
1. For most software projects, the primary focus should be on the domain and domain logic. (Not driven by a particular technology)
2. Complex domain designs should be based on a model.(Making understandability high)
For a sophisticated real world software to be well satisfying to its clients, it should be domain driven rather technology driven which ultimately will address the heart of (the sole existence) software. The heart of software is its ability to solve domain-related problems for its user. All other features, vital though they may be, support this basic purpose (such as notification and statistical reporting systems etc as added functionality).
How can we make the software fit harmoniously with the domain? The best way to do it is to make software a reflection of the domain. Software needs to incorporate the core concepts and elements of the domain, and to precisely realize the relationships between them. Software has to model the domain. A domain is something of this world, a particular area of discipline or a sphere of knowledge, influence, or activity.
A model is a simplification. It is an interpretation of reality that abstracts the aspects relevant to solving the problem at hand and ignores extraneous detail. According to Eric Evans, a domain model is not a particular diagram or a document/specification; it is the idea that the diagram is intended to convey. It is not just the knowledge in a domain expert’s head; it is a rigorously organized and selective abstraction of that knowledge. This can be understandable among all parties involved in the requirement gathering to implementation and so forth.
When choosing a model we can consider the following principles,
• The model is distilled knowledge.
• The model is the backbone of a language used by all team members.
• The model and the heart of the design shape each other.
Ubiquitous has a meaning widespread, constantly encountered; and language has a feature of being vague. It is absolutely necessary to develop a model of the domain by having the software specialists work with the domain experts; however, that approach usually has some initial difficulties due to a fundamental communication barrier. The developers have their minds full of classes, methods, algorithms, patterns, and tend to always make a match between a real life concept and a programming artifact.
The domain model should form a common language for describing system requirements, that works equally well for the business users or sponsors and for the software developers.
A project faces serious problems when its language is fractured. Domain experts use their jargon while technical team members have their own language tuned for discussing the domain in terms of design.Services
In the ubiquitous language the Nouns are mapped in to objects, and Verbs that are associated with corresponding nouns become the behaviors of those objects. But there are some actions in the domain, some verbs, which do not seem to belong to any object. Adding such behavior to an object would spoil the object, making it stand for functionality which does not belong to it. Perhaps those behaviors can occur in several objects across different classes. Best practice is to declare such behaviors as Services
A service is an operation offered as an interface that stands alone in the model; it provides functionality for the domain. Services tend to be named for what they can do (verbs rather than nouns). A good service has three characteristics
- The operation relates to a domain concept that is not a natural part of an entity or value object
- The operation performed refers to other objects in the domain.
- The operation is stateless (does not maintain or update its own internal state in response to being invoked)
While using Services, is important to keep the domain layer isolated. Because of it is easy to get confused between services which belong to the domain layer, and those belonging to the infrastructure layer. There can also be services in the application layer Domain and application SERVICES collaborate with the infrastructure SERVICES. It can be harder to distinguish application SERVICES from domain SERVICES. Following is the example that describes ho the different services can be divided in to layers.
Partitioning Services into Layers
Application Funds Transfer App Service
– Digests input (such as an XML request).
– Sends message to domain service for fulfillment.
– Listens for confirmation.
– Decides to send notification using infrastructure service.
• Domain Funds Transfer Domain Service
– Interacts with necessary Account and Ledger objects, making appropriate debits and credits.
– Supplies confirmation of result (transfer allowed or not, and so on).
• Infrastructure Send Notification Service
– Sends e-mails, letters, and other communications as directed by the application.
In a complex system the model tends to grow bigger and bigger. It is necessary to organize the model into modules. Modules are used as a method of organizing related concepts and tasks in order to reduce complexity. They provide two views on a model
· One view provides details within an individual module
· The second view provides information about relationships between modules
Using modules in design is a way to increase cohesion and decrease coupling. It is recommended to group highly related classes into modules to provide maximum cohesion possible. Widely used cohesions
Communicational cohesion: Parts of the module work with same data
Functional cohesion: Parts of the module work together to perform well-defined tasks
Models need to refine until it partitions according to high-level domain concepts and the corresponding code is decoupled as well. Modules and their names should reflect insight into the domain becoming part of the ubiquitous language. After the role of the module is decided, it usually stays unchanged, because module refactoring may be more expensive than a class refactoring
Domain objects go through a set of states during their life time. They are created, placed in memory and used in computations, and they are destroyed. In some cases they are saved in permanent locations, like a database, where they can be retrieved from some time later, or they can be archived. At some point they can be completely erased from the system, including database and the archive storage. Managing the life cycle of a domain object constitutes a challenge in itself, and if it is not done properly, it may have a negative impact on the domain model. Aggregates, Factories and Repositories are three patterns which help us deal with it. Aggregate is a domain pattern used to define object ownership and boundaries. Factories and Repositories are two design patterns which help us deal with object creation and storage.
It is difficult to guarantee the consistency of changes to objects in a model with complex associations such as one-many and many to many associations. Many times invariants apply to closely related objects, not just discrete ones. Yet cautious locking schemes cause multiple users to interfere pointlessly with each other and make a system unusable.
Therefore, we use Aggregates.
An Aggregate is a group of associated objects which are considered as one unit with regard to data changes. Each Aggregate has one root. The root is an Entity, and it is the only object accessible from outside. The root Entity has global identity, and is responsible for maintaining the invariants. If there are other Entities inside the boundary, the identity of those entities is local, making sense only inside the aggregate. Only Aggregate roots can be obtained directly with database queries. All other objects must be found by traversal of associations. Objects within the Aggregate can hold references to other Aggregate roots. A delete operation must remove everything within the Aggregate boundary at once. When a change to any object within the Aggregate boundary is committed, all invariants of the whole Aggregate must be satisfied.
Nothing outside the Aggregate boundary can hold a reference to anything inside, except to the root entity. The root entity can hand references to the internal entities to other objects, but those objects can use them only transiently, and they may not hold on to the reference. The root may hand a copy of a value object to another object, and it doesn't matter what happens to it, because it's just a value and no longer will have any association with the Aggregate.
How is the Aggregate ensuring data integrity and enforcing the invariants?
Since other objects can hold references only to the root, it means that they cannot directly change the other objects in the aggregate. All they can do is to change the root, or ask the root to perform some actions. And the root will be able to change the other objects, but that is an operation contained inside the aggregate, and it is controllable. If the root is deleted and removed from memory, all the other objects from the aggregate will be deleted too, because there is no other object holding reference to any of them. When any change is done to the root which indirectly affects the other objects in the aggregate, it is simple to enforce the invariants because the root will do that. It is much harder to do so when external objects have direct access to internal ones and change them. Enforcing the invariants in such a circumstance involves putting some logic in external objects to deal with it, which is not desirable.
As an example, the customer is the root of the Aggregate, and all the other objects are internal. If the Address is needed, a copy of it can be passed to external objects.
It is too complex to create in the constructor of the root entity as Entities and Aggregates can often be large and complex. Object creation involves a lot of knowledge about the internal structure of the object, about the relationships between the objects contained, and the rules applied to them. This means that each client of the object will hold specific knowledge about the object built. This breaks encapsulation of the domain objects and of the Aggregates. Therefore we use concept of Factories; Factories are used to encapsulate the knowledge necessary for object creation, and they are especially useful to create Aggregates. When the root of the Aggregate is created, all the objects contained by the Aggregate are created along with it, and all the invariants are enforced. It is important for the creation process to be atomic.
There are several design patterns used to implement Factories. Two patterns among others are: Factory Method, Abstract Factory. A Factory Method is an object method which contains and hides knowledge necessary to create another object. This is very useful when a client wants to create an object which belongs to an Aggregate. The solution is to add a method to the Aggregate root, which takes care of the object creation, enforces all invariants, and returns a reference to that object, or to a copy of it.
There are times when the construction of an object is more complex, or when the creation of an object involves the creation of a series of objects. For example: the creation of an Aggregate. Hiding the internal construction needs of an Aggregate can be done in a separate Factory object which is dedicated to this task.
When creating a Factory, we are forced to violate an object’s encapsulation, which must be done carefully. Whenever something changes in the object that has an impact on construction rules or on some of the invariants, we need to make sure the Factory is updated to support the new condition. Factories are tightly related to the objects they are created. That can be a weakness, but it can also be strength.
Entity Factories and Value Object Factories are different. Values are usually immutable objects, and all the necessary attributes need to be produced at the time of creation. When the object is created, it has to be valid and final. It won’t change. Entities are not immutable. They can be changed later, by setting some of the attributes with the mention that all invariants need to be respected. Another difference comes from the fact that Entities need identity, while Value Objects do not.
There are times when a Factory is not needed, and a simple constructor is enough. Use a constructor when:
- The construction is not complicated.
- The creation of an object does not involve the creation of others, and all the attributes needed are passed via the constructor.
- The client is interested in the implementation, perhaps wants to choose the Strategy used.
- The class is the type. There is no hierarchy involved, so no need to choose between a list of concrete implementations.
A constructor or a Factory takes care of object creation. The entire purpose of creating objects is to use them.
In an object-oriented language, one must hold a reference to an object in order to be able to use it. It becomes a problem because one must make sure the client always has a reference to the object needed, or to another which has a reference to the respective object. Such a rule will enforce the objects to hold on a series of references. This increases coupling, creating a series of associations which are not really needed.
Repository, the purpose of which is to encapsulate all the logic needed to obtain object references. The domain objects won’t have to deal with the infrastructure to get the needed references to other objects of the domain. They will just get them from the Repository
The Repository may store references to some of the objects. When an object is created, it may be saved in the Repository, and retrieved from there to be used later. If the client requested an object from the Repository, and the Repository does not have it, it may get it from the storage. Either way, the Repository acts as a storage place for globally accessible objects.
The Repository may also include a Strategy. It may access one persistence storage or another based on the specified Strategy. It may use different storage locations for different type of objects. The overall effect is that the domain model is decoupled from the need of storing objects or their references, and accessing the underlying persistence infrastructure.
There is a relationship between Factory and Repository. They are both patterns of the model-driven design, and they both help us to manage the life cycle of domain objects. While the Factory is concerned with the creation of objects, the Repository takes care of already existing objects. The Repository may cache objects locally, but most often it needs to retrieve them from a persistent storage. Objects are either created using a constructor or they are passed to a Factory to be constructed. For this reason, the Repository may be seen as a Factory, because it creates objects. It is not a creation from scratch, but a reconstitution of an object which existed. We should not mix a Repository with a Factory. The Factory should create new objects, while the Repository should find already created objects.
Refactoring is the process of redesigning the code to make it better without changing application behavior. Traditionally, refactoring is described in terms of code transformations with technical motivations. It’s required to look at the code from time to time during the design and development process. Because refactoring may be a requirement. It is usually done in small steps which are controllable. The result is also a series of small improvements to the code. With refactoring we should not break the functionality of the system or should not introduce some bugs. The purpose of refactoring is to make the code better.
Code refactoring can be done in many ways. Even there exist refactoring patterns which represent an automated approach to refactoring. Another type of refactoring which is related to the domain and its model bring some insights to the domain, it gives things clearer, or discover relationship between two elements. Technical refactoring is another type of refactoring. It is also based on patterns & it can be organized and structured.
A solid design resists refactoring, so that the design needs to be flexible. Otherwise whenever a change is needed, you will find it difficult to deal with.