Thursday, June 4, 2020

Domain Driven Design Chapter 6 Summary

Chapter 6: The Life Cycle of a Domain Object
    • The challenges fall into two categories:
      • Maintaining integrity throughout the life cycle.
      • Preventing the model from getting swamped by the complexity of managing the life cycle.
    • Three patterns to address these issues:
      • Aggregates
        • "[T]ighten up the model itself by defining clear ownership and boundaries, avoiding a chaotic, tangled web of objects."
      • Factories
        • "[C]reate and reconstitute complex objects and aggregates, keeping their internal structure encapsulated."
      • Repositories
        • "[A]ddress the middle and end of the life cycle, providing the means of finding and retrieving persistent objects while encapsulating the immense infrastructure involved."
  • Aggregates
    • "It is difficult to guarantee the consistency of changes to objects in a model with complex associations. Invariants need to be maintained that apply to closely related groups of objects, not just discrete objects. Yet cautious lacking schemes cause multiple users to interfere pointlessly with each other and make a system unusable."
    • "[H]ow do we know where an object made up of other objects begins and ends?"
    • "Although this problem surfaces as technical difficulties in database transactions, it is rooted in the model -- in its lack of defined boundaries. A solution driven from the model will make the model easier to understand and make the design easier to communicate."
    • "An aggregate is a cluster of associated objects that we treat as a unit for the purposes of data changes. Each aggregate has a root and a boundary. The boundary defines what is inside the aggregate. The root is a single, specific entity contained in the aggregate. The root is the only member of the aggregate that outside objects are allowed to hold references to, although objects within the boundary may hold references to each other. Entities other than the root have local identity, but that identity needs to be distinguishable only within the aggregate, because no outside object can ever see it out of the context of the root entity."
    • "Invariants, which are consistency rules that must be maintained whenever data changes, will involve relationships between members of the aggregate. Any rule that spans aggregates will not be expected to be up-to-date at all times. Through event processing, batch processing, or other update mechanisms, other dependencies can be resolved within some specified time. But the invariants applied within an aggregate will be enforced with the completion of each transaction."
    • Rules to apply to all transactions
      • The root has global identity and is responsible for checking invariants.
      • Entities inside the boundary have local identity, unique only within the aggregate.
      • Nothing outside the boundary can hold a reference to anything inside, except the root entity. The root entity can hand out references to internal entities, but they can only be used transiently, and a reference cannot be held. The root may hand out a copy of a value object, and it doesn't matter what happens to it, because it's just a value and no longer has association with 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 at once.
      • When a change to any object within the aggregate boundary is committed, all invariants of the whole aggregate must be satisfied.
  • Factories
    • "When creation of an object, or an entire aggregate, becomes complicated or reveals too much of the internal structure, factories provide encapsulation."
    • "An object should be distilled until nothing remains that does not relate to its meaning or support its role in interactions. This mid-life cycle responsibility is plenty. Problems arise from overloading a complex object with responsibility for its own creation."
    • "Creation of an object can be a major operation in itself, but complex assembly operations do not fit the responsibility of the created objects. Combining such responsibilities can produce ungainly designs that are hard to understand. Making the client direct construction muddies the design of the client, breaches encapsulation of the assembled object or aggregate, and overly couples the client to the implementation of the created object."
    • The two basic requirements for any good factory are:
      • Each creation method is atomic and enforces all invariants of the created object or aggregate.
      • The factory should be abstracted to the type desired, rather than the concrete class(es) created.
    • Choosing Factories and Their Sites
      • "Generally speaking, you create a factory to build something whose details you want to hide, and you place the factory where you want the control to be. These decisions usually revolve around aggregates."
      • "A factory is very tightly coupled to its product, so a factory should be attached only to an object that has a close natural relationship with the product. When there is something we want to hide [...] yet there doesn't seem to be a natural host, we must create a dedicated factory object or service."
    • When a Constructor is All You Need
      • "[T]here are times when the directness of a constructor makes it the best choice. Factories can actually obscure simple objects that don't use polymorphism."
      • The trade-offs favor a bare, public constructor in the following circumstances.
        • The class is the type. It is not part of any interesting hierarchy, and it isn't used polymorphically by implementing an interface.
        • The client cares about the implementation, perhaps as a way of choosing a strategy
        • All of the attributes of the object are available to the client, so that no object creation gets nested inside the constructor exposed to the client.
        • The construction is not complicated.
        • A public constructor must follow the same rules as a factory: It must be an atomic operation that satisfies all invariants of the created object.
      • "Constructors should be dead simple. Complex assemblies, especially of aggregates, call for factories. The threshold for choosing to use a little factory method isn't high."
    • Designing the Interface
      • Two points to keep in mind
        • Each operation must be atomic.
          • You have to pass in everything needed to create a complete product in a single interaction with the factory. You also have to decide what will happen if creation fails, in the event that some invariant isn't satisfied. You could throw an exception or just return null. To be consistent, consider adopting a coding standard for failures in factories.
        • The factory will be coupled to its arguments.
          • If you are not careful in your selection of input parameters, you can create a rat's nest of dependencies. The degree of coupling will depend on what you do with the argument. If it is simply plugged into the product, you've created a modest dependency. If you are picking parts out of the argument to use in the construction, the coupling gets tighter.
    • Where Does Invariant Logic Go?
      • "[Y]ou should think twice before removing the rules applying to an object outside that object. The factory can delegate invariant checking to the product, and this is often best."
      • "Under some circumstances, there are advantages to placing invariant logic in the factory and reducing clutter in the product. This is especially appealing with aggregate rules (which span many objects). It is especially unappealing with factory methods attached to other domain objects."
      • "An object doesn't need to carry around logic that will never be applied in its active lifetime. In such cases, the factory is a logical place to put invariants, keeping the product simpler."
    • Entity Factories Versus Value Object Factories
      • Entity factories differ from value object factories in two ways.
        • "Value objects are immutable; the product comes out complete in its final form. So the factory operations have to allow for a full description of the product. Entity factories tend to take just the essential attributes required to make a valid aggregate. Details can be added later if they are not required by an invariant."
        • "Then there is the issues involved in assigning identity to an entity -- irrelevant to a value object. [...] When the program is assigning an identifier, the factory is a good place to control it. Although the actual generation of a unique tracking id is typically done by a database "sequence" or other infrastructure mechanism, the factory knows what to ask for and where to put it."
    • Reconstituting Stored Objects
      • A factory used for reconstitution is very similar to one used for creation, with two major differences.
        • An entity factory used for reconstitution does not assign a new tracking ID.
          • "To do so would lose the continuity with the object's previous incarnation. So identifying attributes must be part of the input parameters in a factory reconstituting a stored object."
        • A factory reconstituting an object will handle violation of an invariant differently.
          • "During creation of a new object, a factory should simply balk when an invariant isn't met, but a more flexible response may be necessary in reconstitution. If an object already exists somewhere in the system (such as in the database), this fact cannot be ignored. Yet we also can't ignore the rule violation. There has to be some strategy for repairing such inconsistencies, which can make reconstitution more challenging than the creation of new objects."
  • Repositories
    • "Associations allow us to find an object based on its relationship to another. But we must have a starting point for a traversal to an entity or value in the middle of its life cycle."
    • "A database search is globally accessible and makes it possible to go directly to any object. There is no need for all objects to be interconnected, which allows us to keep the web of objects manageable. Whether to provide a traversal or depend on a search becomes a design decision, trading off the decoupling of the search against the cohesiveness of the association."
    • "[Developers] may use queries to pull the exact data they need from the database, or to pull a few specific objects rather than navigating from aggregate roots. Domain logic moves into queries and client code, and the entities and value objects become mere data containers. The sheer technical complexity of applying most database access infrastructure quickly swamps the client code, which leads developers to dumb down the domain layer, which makes the model irrelevant."
    • Querying a Repository
      • "Although most queries return an object or a collection of objects, it also fits within the concept to return some types of summary calculations, such as an object count, or a sum of a numerical attribute that was intended by the model to be tallied."
    • Client Code Ignores Repository Implementation; Developers Do Not
      • "Encapsulation of the persistence technology allows the client to be very simple, completely decoupled from the implementation of the repository. But as is often the case with encapsulation, the developer must understand what is happening under the hood. The performance implications can be extreme when repositories are used in different ways or work in different ways.
    • Implementing a Repository
      • "The ideal is to hid all the inner workings from the client (although not from the developer of the client), so that the client code will be the same whether the data is stored in an object database, stored in a relational database, or simply held in memory. [...] Encapsulating the mechanisms of storage, retrieval, and query is the most basic feature of a repository implementation."
    • Working Within Your Frameworks
      • "You may find that the framework provides services you can use to easily create a repository, or you may find that the framework fights you all the way. [...] In general, don't fight your frameworks. Seek ways to keep the fundamentals of domain-driven design and let go of the specifics when the framework is antagonistic. [...] This is assuming that you have no choice but to use the framework. [...] If you have the freedom, choose frameworks, or parts of frameworks, that are harmonious with the style of design you want to use."
    • The Relationship with Factories
      • "Because the repository is [...] creating objects based on data, many people consider the repository to be a factory -- indeed it is, from a technical point of view. But it is more useful to keep the model in the forefront, and as mentioned before, the reconstitution of a stored object is not the creation of a new conceptual object. In this domain-driven view of the design, factories and repositories have distinct responsibilities. The factory makes new objects; the repository finds old objects. The client of a repository should be given the illusion that the objects are in memory."
      • "One other case that drives people to combine factory and repository is the desire for "find or create" functionality, in which a client can describe an object it wants and, if no such object is found, will be given a newly created one. This function should be avoided. It is a minor convenience at best. A lot of cases in which it seems useful go away when entities and value objects are distinguished. [...] Usually, the distinction between a new object and an existing object is important in the domain, and a framework that transparently combines them will actually muddle the situation."
  • Designing Objects for Relational Databases
    • "When the database is being viewed as an object store, don't let the data model and the object model diverge far, regardless of the powers of mapping tools. Sacrifice some richness of object relationships to keep close to the relational model. Compromise some formal relational standards, such as normalization, if it helps simplify the object mapping."
    • "Processes outside the object system should not access such an object store. They could violate the invariants enforced by the objects. Also, their access will lock in the data model so that it is hard to change when the objects are refactored."
    • "The tradition of refactoring that has increasingly taken hold in the object world has not really affected relational database design much. What's more, serious data migration issues discourage frequent change. This may create a drag on the refactoring of the object model, but if the object model and the database model start to diverge, transparency can be lost quickly."