- "In the earlier examples, the patterns were mostly applied one at a time, but on a real project you have to combine them. This chapter presents one elaborate example (still drastically simpler than a real project, of course)."
- Introducing the Cargo Shipping System
- Initial Requirements for cargo shipping company software:
- Track key handling of customer cargo
- Book cargo in advance
- Send invoices to customers automatically when the cargo reaches some point in its handling
- A diagram for an initial model is given, defining relationships between Customer, Role, Cargo, Delivery History, Delivery Specification, Handling Event, Carrier Movement, and Location
- "A Handling Event is a discrete action taken with the Cargo, such as loading it onto a ship or clearing it through customs."
- "Delivery Specification defines a delivery goal, which at minimum would include a destination and an arrival date, but it can be more complex."
- "A role distinguishes the different parts played by Customers in a shipment. One is the “shipper,” one the “receiver,” one the “payer,” and so on. Because only one Customer can play a given role for a particular Cargo, the association becomes a qualified many-to-one instead of many-to-many."
- "Carrier Movement represents one particular trip by a particular Carrier (such as a truck or a ship) from one Location to another."
- "Delivery History reflects what has actually happened to a Cargo, as opposed to the Delivery Specification, which describes goals. A Delivery History object can compute the current Location of the Cargo by analyzing the last load or unload and the destination of the corresponding Carrier Movement."
- Isolating the Domain: Introducing the Applications
- Three user-level application functions:
- A Tracking Query that can access past and present handling of a particular Cargo
- A Booking Application that allows a new Cargo to be registered and prepares the system for it
- An Incident Logging Application that can record each handling of the Cargo (providing the information that is found by the Tracking Query)
- Distinguishing Entities and Value Objects
- In this section each of the objects are examined and identified as either entities or value objects.
- Customer, Cargo, Handling Event, Carrier Movement, Location, and Delivery History are identified as entities.
- There is a note on Delivery history that it has a one to one relationship with its Cargo, and doesn't have an identity on its own.
- Delivery Specification is identified as a value object
- Role and Other Attributes
- "Role says something about the association it qualifies, but it has no history or continuity. It is a value object, and it could be shared among different Cargo/Customer associations."
- "Other attributes such as time stamps or names are value objects."
- Designing Associations in the Shipping Domain
- The association between Customer and Cargo should go from Cargo to Customer:
- "It would be cumbersome for the Customer entity to cart around every Cargo it was ever involved in. A repository could provide access in the other direction."
- "[The association between Cargo and Delivery History] remains bidirectional. Tracking is core to Cargo in this application. A history must refer to its subject."
- There should be a one directional association from Cargo to Delivery Specification:
- "Value objects usually shouldn’t reference their owners. The concept of Delivery Specification actually relates more to Delivery History than to Cargo."
- The association goes from Handling Event to Carrier Movement:
- "The directionality of this association relieves developers from dealing with the multiplicity in the untraversed direction."
- "An ENTITY as basic as Location may be used by many objects for many reasons. This is only practical if it is unburdened of tracking its users."
- Aggregate Boundaries
- Cargo, Role, Delivery History, and Delivery Specification are grouped into a single aggregate.
- Cargo
- "Root of aggregate. Globally unique identifier."
- Delivery History
- "Internal to aggregate. Delivery History has meaning and identity only in association with Cargo."
- Handling Event was kept separate
- "A Handling Event needs to be created in a low-contention transaction—one reason to make it the root of its own aggregate."
- Selecting Repositories
- Needs are identified for a Customer Repository, a Cargo Repository, a Location Repository, and a Carrier Movement Repository.
- "For now there is no Handling Event Repository, because we decided to implement the association with Delivery History as a collection in the first iteration, and we have no application requirement to find out what has been loaded onto a Carrier Movement. Either of these reasons could change; if they did, then we would add a repository."
- Walking Through Scenarios
- "To cross-check all these decisions, we have to constantly step through scenarios to confirm that we can solve application problems effectively."
- Sample Application Feature: Changing the Destination of a Cargo
- "Occasionally a Customer calls up and says, “Oh no! We said to send our cargo to Hackensack, but we really need it in Hoboken.” We are here to serve, so the system is required to provide for this change."
- "Delivery Specification is a value object, so it would be simplest to just to throw it away and get a new one, then use a setter method on Cargo to replace the old one with the new one."
- Sample Application Feature: Repeat Business
- "The users say that repeated bookings from the same Customers tend to be similar, so they want to use old Cargoes as prototypes for new ones."
- The example goes through in detail the necessary steps of using a previous cargo as a prototype, evaluating what to copy and what to modify.
- "Notice that we have copied everything inside the Cargo aggregate boundary, we have made some modifications to the copy, but we have affected nothing outside the aggregate boundary at all."
- Object Creation
- Factories and Constructors for Cargo
- For the "Repeat Business" we could consider a factory method on Cargo like this:
public Cargo copyPrototype(String newTrackingID)
Or perhaps a standalone factory:
public Cargo newCargo(Cargo prototype, String newTrackingID)
The standalone factory could perhaps encapsulate the process of getting the auto generated id, and could be simplified to this:
public Cargo newCargo(Cargo prototype) - Adding a Handling Event
- "Every class must have primitive constructors. Because the Handling Event is an entity, all attributes that define its identity must be passed to the constructor. As discussed previously, the Handling Event is uniquely identified by the combination of the ID of its Cargo, the completion time, and the event type."
- Constructor could look like this:
public HandlingEvent(Cargo c, String eventType, Date timeStamp) {
handled = c;
type = eventType;
completionTime = timeStamp;
} - "In this case, all attributes of the Handling Event are going to be
set in the initial transaction and never altered (except possibly for correcting a data-entry error), so it could be convenient, and make client
code more expressive, to add a simple factory method to Handling
Event for each event type, taking all the necessary arguments. For example, a “loading event” does involve a Carrier Movement:"
public static HandlingEvent newLoading(
Cargo c, CarrierMovement loadedOnto, Date timeStamp) {
HandlingEvent result = new HandlingEvent(c, LOADING_EVENT, timeStamp);
result.setCarrierMovement(loadedOnto);
return result;
} - Pause for Refactoring: An Alternative Design of the Cargo Aggregate
- There are complications that arise from the circular reference from Cargo to Delivery History to Handling Event, and back to Cargo again. Specifically, any time that a Handling Event is added, it must traverse the Cargo to the Delivery History and update the Delivery History to add the Handling Event.
- This can be fixed by adding a Handling Event Repository, removing the persistent Delivery History reference from the Cargo, and instead recreate the Delivery History through calls to the repository on an as needed basis.
- Modules in the Shipping Model
- To show the importance of modules, the cargo delivery software is further built out with more moving pieces. The author the proceeds to show example module groupings in two diagrams to illustrate the point that poor grouping leads to high coupling.
- In one diagram, he groups modules as Entities, Values, and Services, and the diagram has a multiplicity of connections moving between each of the packages.
- In a subsequent diagram, the same objects are instead grouped in the modules Customer, Billing, and Shipping, with a max of two connections between each module.
- Introducing a New Feature: Allocation Checking
- At this point, a new feature is requested that wasn't part of the original specification. This feature deals with allocating how much of any given product can be sold so that more profitable business transactions will not be crowded out by less profitable transactions. This will involve integrating the software with the software used in the sales department.
- Connecting the Two Systems
- "The Sales Management System was not written with the same model in mind that we are working with here. If the Booking Application interacts with it directly, our application will have to accommodate the other system’s design, which will make it harder to keep a clear model-driven design and will confuse the ubiquitous language. Instead, let’s create another class whose job it will be to translate between our model and the language of the Sales Management System. It will not be a general translation mechanism. It will expose just the features our application needs, and it will reabstract them in terms of our domain model. This class will act as an anticorruption layer (discussed in Chapter 14)."
- Enhancing the Model: Segmenting the Business
- Our system does not yet define "type" for Cargo. We could point blank use the information coming from the sales system, but there's a potentially better way.
- An analysis pattern that could be used is the enterprise segment.
- "An enterprise segment is a set of dimensions that define a way of breaking down a business. These dimensions could include all those mentioned already for the shipping business, as well as time dimensions, such as month to date."
- This will be further discussed in chapter 11.
- Performance Tuning
- Depending on various situations or limitations with the other system that we must integrate with, various trade offs may need to be made in order to ensure performance.
- A Final Look
- "That’s it. This integration could have turned our simple, conceptually consistent design into a tangled mess, but now, using an anticorruption layer, a service, and some enterprise segments, we have integrated the functionality of the Sales Management System into our booking system cleanly, enriching the domain."
- "A final design question: Why not give Cargo the responsibility of deriving the Enterprise Segment? At first glance it seems elegant, if all the data the derivation is based on is in the Cargo, to make it a derived attribute of Cargo. Unfortunately, it is not that simple. Enterprise Segments are defined arbitrarily to divide along lines useful for business strategy. The same entities could be segmented differently for different purposes. We are deriving the segment for a particular Cargo for booking allocation purposes, but it could have a completely different Enterprise Segment for tax accounting purposes. Even the allocation Enterprise Segment could change if the Sales Management System is reconfigured because of a new sales strategy. So the Cargo would have to know about the Allocation Checker, which is well outside its conceptual responsibility, and it would be laden with methods for deriving specific types of Enterprise Segment. Therefore, the responsibility for deriving this value lies properly with the object that knows the rules for segmentation, rather than the object that has the data to which those rules apply. Those rules could be split out into a separate “Strategy” object, which could be passed to a Cargo to allow it to derive an Enterprise Segment. That solution seems to go beyond the requirements we have here, but it would be an option for a later design and shouldn’t be a very disruptive change."
Technology is always changing. It makes the industry interesting and exciting to work in, but it also makes it hard for you, as a developer, to keep up with the changes, let alone get ahead. And yet staying on top of these changes, and thriving because of them, is a rewarding and worthwhile goal, because by doing so, you unlock the potential of what you can accomplish. Here, I explore the how of doing just that.
Friday, June 12, 2020
Domain Driven Design Chapter 7 Summary
Chapter 7: Using the Language: An Extended Example
Labels:
domain-driven-design
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."
Labels:
domain-driven-design
Wednesday, May 27, 2020
Domain Driven Design Chapter 5 Summary
Chapter 5: A Model Expressed in Software
- "Connecting model and implementation has to be done at the detail level."
- Associations
- "For every traversable association in the model, there is a mechanism in the software with the same properties."
- "In real life, there are lots of many-to-many associations, and a great number are naturally bidirectional. The same tends to be true of early forms of a model as we brainstorm and explore the domain. But these general associations complicate implementation and maintenance. Furthermore, they communicate very little about the nature of the relationship."
- Three ways of making associations more tractable
- Imposing a traversal direction
- "The United States has had many presidents, as have many other countries. This is a bidirectional, one-to-many relationship. Yet we seldom would start out with the name "George Washington" and ask, "Of which country was he president?" Pragmatically, we can reduce the relationship to a unidirectional association, traversable from country to president. This refinement actually reflects insight into the domain, as well as making a more practical design. It captures the understanding that one direction of the association is much more meaningful and important than the other. It keeps the "Person" class independent of the far less fundamental concept of "President."
- Adding a qualifier, effectively reducing multiplicity
- "Very often, deeper understanding leads to a "qualified" relationship. Looking deeper into presidents, we realize that (except in civil wars, perhaps) a country has only one president at a time. This qualifier reduces the multiplicity to one-to-one, and explicitly embeds an important rule into the model. Who was the president of the United States in 1790? George Washington."
- Eliminating nonessential associations
- "Of course, the ultimate simplification is to eliminate an association altogether, if it is not essential to the job at hand or the fundamental meaning of the model objects."
- Entities (A.K.A. Reference Objects)
- "Many objects are not fundamentally defined by their attributes, but rather by a thread of continuity and identity."
- "many things are defined by their identity, and not by any attribute. In our typical conception, a person [...] has an identity that stretches from birth to death and even beyond. That person's physical attributes transform and ultimately disappear. The name may change. Financial relationships come and go. There is not a single attribute of a person that cannot change; yet the identity persists."
- "Some objects are not defined primarily by their attributes. They represent a thread of identity that runs through time and often across distinct representations. Sometimes such an object must be matched with another object even though attributes differ. An object must be distinguished from other objects even though they might have the same attributes. Mistaken identity can lead to data corruption.
- Modeling Entities
- "[T]he most basic responsibility of entities is to establish continuity so that behavior can be clear and predictable. They do this best if they are kept spare. Rather than focusing on the attributes or even the behavior, strip the entity object's definition down to the most intrinsic characteristics, particularly those that identify it or are commonly used to find or match it. Add only behavior that is essential to the concept and attributes that are required by that behavior. Beyond that, look to remove behavior and attributes into other objects associated with the core entity. [...] Beyond identity issues, entities tend to fulfill their responsibilities by coordinating the operations of objects they own."
- Designing the Identity Operation
- "Each entity must have an operational way of establishing its identity with another object -- distinguishable even from another object with the same descriptive attributes. An identifying attribute must be guaranteed to be unique within the system however that system is defined -- even if distributed, even when objects are archived."
- Value Objects
- "Tracking the identity of entities is essential, but attaching identity to other objects can hurt system performance, add analytical work, and muddle the model by making all objects look the same.
"Software design is a constant battle with complexity. We must make distinctions so that special handling is applied only where necessary.
"However, if we think of this category of object as just the absence of identity, we haven't added much to our toolbox or vocabulary. In fact, these objects have characteristics of their own and their own significance to the model. These are the objects that describe things." - "When you care only about the attributes of an element of the model, classify it as a value object. Make it express the meaning of the attributes it conveys and give it related functionality. Treat the value objects as immutable. Don't give it any identity and avoid the design complexities necessary to maintain entities."
- Designing Value Objects
- "We don't care which instance we have of a value object. This lack of constraints gives us design freedom we can use to simplify the design or optimize performance. This involves making choices about copying, sharing, and immutability."
- To safely share or copy value objects, they must be immutable.
- You can choose either to share or to copy, depending on what constraints you are trying to optimize for.
- Designing Associations that Involve Value Objects
- Bidirectional associations between two value objects make no sense.
- Without identity, it is meaningless to say that an object points back to the same value object that points to it.
- "Try to completely eliminate bidirectional associations between value objects. If in the end such associations seem necessary in your model, rethink the decision to declare the object a value object in the first place. Maybe it has an identity that hasn't been explicitly recognized yet."
- Services
- "Some concepts from the domain aren't natural to model as objects. Forcing the required domain functionality to be the responsibility of an entity or value either distorts the definition of a model-based object or adds meaningless artificial objects."
- A service is an operation offered as an interface that stands alone in the model, without encapsulating state, as entities and value objects do.
- 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 interface is defined in terms of other elements of the domain model.
- The operation is stateless.
- Services and the Isolated Domain Layer
- Services are used in other layers than just the domain layer.
- "It takes care to distinguish services that belong to the domain layer from those of other layers, and to factor responsibilities to keep that distinction sharp."
- Granularity
- The service pattern can be used as a mean of controlling granularity in the interfaces of the domain layer.
- "Medium-grained, stateless services can be easier to reuse in large systems because they encapsulate significant functionality behind a simple interface."
- Access to Services
- "Distributed system architectures [...] provide special publishing mechanisms for services, with conventions for their use, and they add distribution and access capabilities. [These] architectures should be used only when there is a real need to distribute the system or otherwise draw on the framework's capabilities."
- Modules (A.K.A Packages)
- "Everyone uses modules, but few treat them as a full-fledged part of the model. Code gets broken down into all sorts of categories, from aspects of the technical architecture to developers' work assignments. Even developers who refactor a lot tend to content themselves with modules conceived early in the project.
"It is a truism that there should be low coupling between modules and high cohesion within them. [...] There is a limit to how many things a person can think about at once (hence low coupling). Incoherent fragments of ideas are as hard to understand as an undifferentiated soup of ideas (hence high cohesion)." - "Choose modules that tell the story of the system and contain a cohesive set of concepts."
- "Give modules names that become part of the ubiquitous language. Modules and their names should reflect insight into the domain."
- Agile Modules
- "Modules need to coevolve with the rest of the model. This means refactoring modules right along with the model and code."
- This refactoring often doesn't happen because there are many difficulties in refactoring modules.
- "Whatever development technology the implementation will be based on, we need to look for ways of minimizing the work of refactoring modules, and minimizing clutter in communicating to other developers."
- The Pitfalls of Infrastructure-Driven Packaging
- Many frameworks encourage package structures that reflect the infrastructure.
- This can lead to a business object being split across multiple packages, causing low cohesion.
- This can also lead to packages that have a high number of calls between them, causing high coupling.
- "Use packaging to separate the domain layer from other code. Otherwise, leave as much freedom as possible to the domain developers to package the domain objects in ways that support their model and design choices."
- Modeling Paradigms
- Why the Object Paradigm Predominates
- Object modeling strikes a nice balance of simplicity and sophistication.
- It also has significant circumstantial advantages deriving from maturity and widespread adoption.
- It has a mature developer community.
- Nonobjects in an Object World
- "Whatever the dominant model paradigm may be on a project, there are bound to be parts of the domain that would be much easier to express in some other paradigm."
- "When there are just a few anomalous elements of a domain that otherwise works well in a paradigm, developers can live with a few awkward objects in an otherwise consistent model."
- "But when major parts of the domain seem to belong to different paradigms, it is intellectually appealing to model each part in a paradigm that fits."
- "[M]aking a coherent model that spans paradigms is hard, and making the supporting tools coexist is complicated."
- Sticking with Model-Driven Design When Mixing Paradigms
- "Without a seamless environment, it falls on the developers to distill a model made up of clear, fundamental concepts to hold the whole design together."
- "The most effective tool for holding the parts together is a robust ubiquitous language that underlies the whole heterogeneous model."
Labels:
domain-driven-design
Tuesday, May 26, 2020
Domain Driven Design Chapter 4 Summary
Part II: The Building Blocks of a Model-Driven Design
- "Developing a good domain model is an art. But the practical design and implementation of a model's individual elements can be relatively systematic. Isolating the domain design from the mass of other concerns in the software system will greatly clarify the design's connection to the model. Defining model elements according to certain distinctions sharpens their meanings. Following proven patterns for individual elements helps produce a model that is practical to implement."
- "Elaborate models can cut through complexity only if care is taken with the fundamentals, resulting in detailed elements that the team can confidently combine."
Chapter 4: Isolating the Domain
- "The part of the software that specifically solves problems from the domain usually constitutes only a small portion of the entire software system, although its importance is disproportionate to its size. [...] We must not be forced to pick them out of a much larger mix of objects [...] We need to decouple the domain objects from other functions of the system, so we can avoid confusing the domain concepts with other concepts related only to software technology or losing sight of the domain altogether in the mass of the system."
- Layered Architecture
- "In an object-oriented program, UI, database, and other support code often gets written directly into the business objects. Additional business logic is embedded in the behavior of UI widgets and database scripts. This happens because it is the easiest way to make things work, in the short run.
"When the domain-related code is diffused through such a large amount of other code, it becomes extremely difficult to see and to reason about. Superficial changes to the UI can actually change business logic. To change a business rule may require meticulous tracing of UI code, database code, or other program elements. [...] [A] program must be kept very simple or it becomes impossible to understand." - Most successful architectures use some version of these four conceptual layers:
- User Interface (or Presentation Layer)
- Responsible for showing information to the user and interpreting the user's commands
- Application Layer
- Defines the jobs the software is supposed to do and directs the expressive domain objects to work out problems.
- Domain Layer (or Model Layer)
- Responsible for representing concepts of the business, information about the business situation, and business rules.
- Infrastructure Layer
- Provides generic technical capabilities that support the higher layers.
- "Partition a complex program into layers. Develop a design within each layer that is cohesive and that depends only on the layers below. Follow standard architectural patterns to provide loose coupling to the layers above. Concentrate all the code related to the domain model in one layer and isolate it from the user interface, application, and infrastructure code. The domain objects, free of the responsibility of displaying themselves, storing themselves, managing application tasks, and so forth, can be focused on expressing the domain model. This allows a model to evolve to be rich enough and clear enough to capture essential business knowledge and put it to work."
- Relating the Layers
- Layers should be loosely coupled, with dependencies only in one direction.
- Upper layers can use or manipulate elements of lower ones by calling public interfaces
- When a lower level needs to communicate upward (beyond answering a direct query), patterns such as callbacks or observers should be used.
- Architectural Frameworks
- "When infrastructure is provided in the form of services called on through interfaces, it is fairly intuitive how the layering works and how to keep the layers loosely coupled."
- Some technical problems call for more intrusive forms of architecture.
- Architectural frameworks often require the other layers to be implemented in very particular ways.
- "The best architectural frameworks solve complex technical problems while allowing the domain developer to concentrate on expressing a model. But frameworks can easily get in the way, either by making too many assumptions that constrain domain design choices or by making the implementation so heavyweight that development slows down."
- "A lot of the downside of frameworks can be avoided by applying them selectively to solve difficult problems without looking for a one-size-fits-all solution."
- The Domain Layer is Where the Model Lives
- "The "domain layer" is the manifestation of [the domain model] and all directly related design elements. The design and implementation of business logic constitute the domain layer. In a model-driven design, the software constructs of the domain layer mirror the model concepts."
- The Smart UI "Anti-Pattern"
- "Many software projects do take and should continue to take a much less sophisticated design approach that I call the smart UI. But smart UI is an alternate, mutually exclusive fork in the road, incompatible with the approach of domain driven-design. If that road is taken, most of what is in this book is not applicable."
- If a project needs to deliver simple functionality, dominated by data entry and display, with few business rules, and the staff is not composed of advanced object modelers, it may warrant using the smart UI pattern.
- Other Kinds of Isolation
- "Unfortunately, there are influences other than infrastructure and user interfaces that can corrupt your delicate domain model."
- Chapters 14 and 15 will deal with a number of these issues.
Labels:
domain-driven-design
Monday, May 11, 2020
Renaming Tables and Columns in PostgreSQL with a Zero-Downtime Pipeline
As any programmer will tell you, one of the hardest things in programming is naming things. There are many reasons for this, and we don't need to get into the weeds with it, but this leads to a fairly common scenario where something is given a poor name, and later on it needs to be refactored to be given a better name. (Or possibly it had a great name to begin with, but as the code evolved, the name became outdated. This leads to the same scenario. It needs to be renamed.)
But when it comes to databases, programmers frequently find that renaming things is hard. In addition to writing the migration to rename the table or column, you'll also need to scour all the queries in your code and make sure that you've changed all references. And if you missed something, you won't know about it until you or someone else happens to hit that query during runtime. (If things were done right, you'll have an automated test suite that you can run that will instantly tell you whether or not you missed something, but in the real world, less than ideal situations are common, and you very well might not have this luxury.)
And with zero-downtime deploys, things become even trickier. While in many programmers' minds they believe that all changes in a commit go out simultaneously, this actually is not the case. The process of deploying, which involves running migrations, bringing down old servers, starting up new servers, etc., takes time, and these parts can't happen all at once. As such, a zero-downtime deploy pipeline will often look something like this:
Some programmers, knowing about these issues, will wait until off hours to run the deploy. In best case scenarios, this has you up late at night, outside of normal work hours, running a deploy. In worst case scenarios, where you have a global audience, this isn't even an option.
As a result of all these issues, I've frequently run into situations where programmers simply forgo renaming things in the database, which results in most of the code having the cleaner, clearer refactored name, but once you reach the database level, you're looking at the old ugly name.
To be quite frank, there had to be a better way to deal with this, so that sent me doing some looking and researching, and something that I discovered is that in Postgres views are updatable if they are kept simple enough. An article on this can be found here.
So with this being the case, we can use views to create aliases of sorts, and I'll give examples for table names and column names below.
But before I get to the examples, I'd like to address a concern that I've heard regarding views. The concern is that through using views the database will be drastically slowed down because the views do not have access to the indexes on the table. This is simply not true. Views in Postgres are implemented through rules, and the query planner is more than capable of taking a query referencing the view and the query embedded inside the view and combining and optimizing them. Now, if you have some rather complex views, or views referencing views, or other such things, then the query planner might not be intelligent enough to properly optimize it in the same way that a hand crafted query would, but that won't be a concern for the very simplistic views that we use below.
Let's walk through an example of renaming a table. In the query below I create a table with the name "old_table_name" which I'll plan on refactoring to the name "new_table_name". I also insert some basic data in to this table.
This will have a number of similarities to table renaming, along with some key differences. So to set up our example, we'll create a table with a column named "old_column_name" that we intend to update to "new_column_name", and we'll add some data.
So with the how to out of the way, let's discuss a few benefits. The first benefit is that by breaking thing into at least two separate deploys, the first with the first migration and the changes to the queries, and then a follow up with the final migration, we are able to rename a table or column, and have a zero-downtime deploy go off without a hitch. During that short period of time where there are still old services making database calls in the old format as well as new services using the new format, everything will work without issue.
But what's perhaps even more beneficial here is the fact that you actually don't need to update all the queries at once. You could have one deploy that only runs the first migration that adds the view, then after that you could have one or many deploys updating the queries in the code, and these updates can be spread over time. Then once you know that all necessary queries have been updated, you could run the final migration removing the view and making the final changes to the table. There are multiple benefits that can come from this.
The first of these benefits is small batch size. There are many sources out there that discuss the benefits of small batch size, which involve fewer bugs, code that is easier to review and deploy, and the ability to be more responsive to changing priorities. And this gives us the ability to do small batches. Instead of being forced to update all queries in the database all at once, I can instead make smaller changes, where perhaps a deploy simply changes one or two queries. It gives the developer more control and say over how to handle things.
Another benefit that can come from this is that it can help with those scenarios where you're not sure if you managed to find and update all of the references. There is nothing that says that that final migration needs to be run immediately. It could instead make sense to have a period of time where you wait and you monitor logs to see if there are any queries coming through that are referencing the old names (this is assuming that you are logging the queries that are run against your database). Once you've had a period of time where no references have been made to the old name, then you can run the final migration with some level of confidence that nothing is going to break.
And so with that, we have a strategy for renaming things at the database level that is fairly straightforward and simple to implement that gives us the flexibility that we need to do the job with a level of confidence that we won't be breaking things in the process.
But when it comes to databases, programmers frequently find that renaming things is hard. In addition to writing the migration to rename the table or column, you'll also need to scour all the queries in your code and make sure that you've changed all references. And if you missed something, you won't know about it until you or someone else happens to hit that query during runtime. (If things were done right, you'll have an automated test suite that you can run that will instantly tell you whether or not you missed something, but in the real world, less than ideal situations are common, and you very well might not have this luxury.)
And with zero-downtime deploys, things become even trickier. While in many programmers' minds they believe that all changes in a commit go out simultaneously, this actually is not the case. The process of deploying, which involves running migrations, bringing down old servers, starting up new servers, etc., takes time, and these parts can't happen all at once. As such, a zero-downtime deploy pipeline will often look something like this:
- The migration is run against the database
- The old application containers (of which there are many instances) are brought down one at a time and replaced with the new application containers in turn.
Some programmers, knowing about these issues, will wait until off hours to run the deploy. In best case scenarios, this has you up late at night, outside of normal work hours, running a deploy. In worst case scenarios, where you have a global audience, this isn't even an option.
As a result of all these issues, I've frequently run into situations where programmers simply forgo renaming things in the database, which results in most of the code having the cleaner, clearer refactored name, but once you reach the database level, you're looking at the old ugly name.
To be quite frank, there had to be a better way to deal with this, so that sent me doing some looking and researching, and something that I discovered is that in Postgres views are updatable if they are kept simple enough. An article on this can be found here.
So with this being the case, we can use views to create aliases of sorts, and I'll give examples for table names and column names below.
But before I get to the examples, I'd like to address a concern that I've heard regarding views. The concern is that through using views the database will be drastically slowed down because the views do not have access to the indexes on the table. This is simply not true. Views in Postgres are implemented through rules, and the query planner is more than capable of taking a query referencing the view and the query embedded inside the view and combining and optimizing them. Now, if you have some rather complex views, or views referencing views, or other such things, then the query planner might not be intelligent enough to properly optimize it in the same way that a hand crafted query would, but that won't be a concern for the very simplistic views that we use below.
Renaming a Table
Let's walk through an example of renaming a table. In the query below I create a table with the name "old_table_name" which I'll plan on refactoring to the name "new_table_name". I also insert some basic data in to this table.
create table old_table_name (
id serial primary key,
some_value text not null
);
insert into old_table_name (some_value) values
('Value 1'),
('Value 2'),
('Value 3'),
('Value 4'),
('Value 5'),
('Value 6'),
('Value 7'),
('Value 8'),
('Value 9'),
('Value 10');
Now as a way verify that the migrations that we'll write won't be causing problems, let's insert a large amount of data into this table. By running the following query 19 times we'll end up with over 5 million rows in the table.insert into old_table_name (some_value) select some_value from old_table_name; select count(*) from old_table_name;Additionally, here are some crud operation queries that we can use to represent the kind of queries that we'll find in the code. We'll want to make sure that these don't break during the migration. Note that I've intentionally designed these four queries so that if they are run in order, it will return the table back to it's initial state. This gives us a way to repeatedly test our crud operations.
select id, some_value from old_table_name where id = 1; update old_table_name set some_value = 'New Value' where id = 1 returning id, some_value; delete from old_table_name where id = 1; insert into old_table_name (id, some_value) values (1, 'Value 1') returning id, some_value;Now, for our first migration. We'll add a view with the "new_table_name" that we're wanting to use and have it reference the table using the "old_table_name". This should put us in a situation where queries using the "old_table_name" and queries using the "new_table_name" will both work. After running this migration, we can rerun the above crud operation queries and verify that they still work. Note that this migration ran in 50 milliseconds (times will of course vary).
create view new_table_name as select id, some_value from old_table_name;This then allows us to update the queries in the code at our leisure, whether that's part of the deploy with the above migration, or in a subsequent migration, or even many subsequent migrations. This puts us in a nice situation where all renames are not immediately required. We'll discuss the benefits of this later. In any case, over time you'll want to update all of your queries. We can take the above four crud operation queries and update them to use the "new_table_name" and verify that the queries work:
select id, some_value from new_table_name where id = 1; update new_table_name set some_value = 'New Value' where id = 1 returning id, some_value; delete from new_table_name where id = 1; insert into new_table_name (id, some_value) values (1, 'Value 1') returning id, some_value;Once you're certain that you've updated all queries referencing the table to use the "new_table_name", then you can run the final migration to drop the view and update the table name. Note that you'll want to run these two queries inside of a transaction. Be sure that you understand how your migration tool works and how it handles transactions, because it may not mean explicitly running "begin;" and "commit;" as I'm showing below. In my tests dropping the view took 45 milliseconds, and renaming the table took 45 milliseconds, so even though this migration will lock up the table with an access exclusive lock, everything will still work fine, because the amount of time is inconsequential.
begin; drop view new_table_name; alter table old_table_name rename to new_table_name; commit;After running the migration, the queries referencing the "new_table_name" work, and any queries referencing the "old_table_name" will not.
Renaming a Column
This will have a number of similarities to table renaming, along with some key differences. So to set up our example, we'll create a table with a column named "old_column_name" that we intend to update to "new_column_name", and we'll add some data.
create table table_name (
id serial primary key,
old_column_name text not null
);
insert into table_name (old_column_name) values
('Value 1'),
('Value 2'),
('Value 3'),
('Value 4'),
('Value 5'),
('Value 6'),
('Value 7'),
('Value 8'),
('Value 9'),
('Value 10');
Once again, just to verify that our migrations won't cause issues with large amounts of data, we'll run the following query 19 times to give ourselves over 5 million rows.insert into table_name (old_column_name) select old_column_name from table_name; select count(*) from table_name;And here are our crud operation queries referencing the "old_column_name", which will be representative of queries in our application code:
select id, old_column_name from table_name where id = 1; update table_name set old_column_name = 'New Value' where id = 1 returning id, old_column_name; delete from table_name where old_column_name = 'New Value'; insert into table_name (id, old_column_name) values (1, 'Value 1') returning id, old_column_name;At this point we can run a migration that gives the table a temporary name, and creates a view with the table name, that provides columns with both the "old_column_name" and the "new_column_name", where the "new_column_name" is just an alias to the "old_column_name". Note that These two queries should be run inside of a transaction. In my tests, the table rename ran in 45 milliseconds, and the create view ran in 47 milliseconds. After the migration runs, you can verify that the above crud operation queries still work.
begin; alter table table_name rename to temp_table_name; create view table_name as select id, old_column_name, old_column_name as new_column_name from temp_table_name; commit;At this point we can change the queries above to reference the "new_column_name" and verify that they still work.
select id, new_column_name from table_name where id = 1; update table_name set new_column_name = 'New Value' where id = 1 returning id, new_column_name; delete from table_name where new_column_name = 'New Value'; insert into table_name (id, new_column_name) values (1, 'Value 1') returning id, new_column_name;Once we are certain that all queries have been updated to reference the "new_column_name", we can then run a migration to drop the view, rename the table back to its original name, and rename the column to the "new_column_name". Once again, these queries should be run inside of a transaction. In my tests, dropping the view took 45 milliseconds, altering the table name took 46 milliseconds, and renaming the column took 45 milliseconds.
begin; drop view table_name; alter table temp_table_name rename to table_name; alter table table_name rename old_column_name to new_column_name; commit;After this migration is run, then only the queries referencing the "new_column_name" will work, and the ones referencing the "old_column_name" will no longer work.
So with the how to out of the way, let's discuss a few benefits. The first benefit is that by breaking thing into at least two separate deploys, the first with the first migration and the changes to the queries, and then a follow up with the final migration, we are able to rename a table or column, and have a zero-downtime deploy go off without a hitch. During that short period of time where there are still old services making database calls in the old format as well as new services using the new format, everything will work without issue.
But what's perhaps even more beneficial here is the fact that you actually don't need to update all the queries at once. You could have one deploy that only runs the first migration that adds the view, then after that you could have one or many deploys updating the queries in the code, and these updates can be spread over time. Then once you know that all necessary queries have been updated, you could run the final migration removing the view and making the final changes to the table. There are multiple benefits that can come from this.
The first of these benefits is small batch size. There are many sources out there that discuss the benefits of small batch size, which involve fewer bugs, code that is easier to review and deploy, and the ability to be more responsive to changing priorities. And this gives us the ability to do small batches. Instead of being forced to update all queries in the database all at once, I can instead make smaller changes, where perhaps a deploy simply changes one or two queries. It gives the developer more control and say over how to handle things.
Another benefit that can come from this is that it can help with those scenarios where you're not sure if you managed to find and update all of the references. There is nothing that says that that final migration needs to be run immediately. It could instead make sense to have a period of time where you wait and you monitor logs to see if there are any queries coming through that are referencing the old names (this is assuming that you are logging the queries that are run against your database). Once you've had a period of time where no references have been made to the old name, then you can run the final migration with some level of confidence that nothing is going to break.
And so with that, we have a strategy for renaming things at the database level that is fairly straightforward and simple to implement that gives us the flexibility that we need to do the job with a level of confidence that we won't be breaking things in the process.
Labels:
postgresql
Saturday, May 9, 2020
Domain Driven Design Chapter 3 Summary
Chapter 3: Binding Model and Implementation
- "The first thing I saw [...] was a complete class diagram [...] that covered a large wall. [...] As large as the wall size diagram was, the model did capture some knowledge. After a moderate amount of study, I learned quite a bit (though that learning was hard to direct [...]). I was more troubled to find that my study gave no insight into the application's code and design. [...] Because the model was "correct", the result of extensive collaboration between technical analysts and business experts, the developers reached the conclusion that conceptually based objects could not be the foundation of their design. So they proceeded to develop an ad hoc design."
- "The project had a domain model, but what good is a model on paper unless it directly aids the development of running software? [...] Domain-driven design calls for a model that doesn't just aid early analysis but is the very foundation of the design."
- Model-Driven Design
- "Tightly relating the code to an underlying model gives the code meaning and makes the model relevant."
- An analysis model, a model meant for understanding only and where mixing in implementation concerns is considered bad practice, fails to accomplish its goals:
- It is not created with design issues in mind, and is impractical for those needs.
- While some knowledge crunching happens, it is lost when coding begins.
- It will go into depth about some irrelevant subjects, while it overlooks some important subjects.
- Discoveries always emerge during the design/implementation effort.
- "Model-Driven Design discards the dichotomy of analysis model and design to search out a single model that serves both purposes."
- To make the model relevant:
- Design a portion of the software system to reflect the domain model in a very literal way, so that mapping is obvious.
- Revisit the model and modify it to be implemented more naturally in software, even as you seek to make it reflect deeper insight into the domain.
- Demand a single model that serves both purposes well, in addition to supporting a robust Ubiquitous Language.
- Draw from the model the terminology used in the design and the basic assignment of responsibilities.
- The code becomes an expression of the model, so a change to the code may be a change to the model. Its effect must ripple through the rest of the project's activities accordingly.
- To tie the implementation slavishly to a model usually requires software development tools and languages that support a modeling paradigm, such as object-oriented programming.
- Modeling Paradigms and Tool Support
- "Object-oriented programming is powerful because it is based on a modeling paradigm, and it provides implementations of the model constructs. [...] Although many developers benefit from just applying the technical capabilities of objects to organize program code, the real breakthrough of object design comes when code expresses concepts of a model."
- Example: From Procedural to Model Driven
- The example discusses the use of a PCB layout tool, and how it will try to find the optimal paths for PCB nets. But the software does not support the concept of buses, which are groupings of nets that follow the same path, essentially connecting multiple pins between two components. Procedural code has been written to process the layout tool's data files and use a naming convention to define buses, but what this code can do is limited and messy, because of it's procedural nature. By using an object oriented paradigm, much more powerful and flexible concepts are able to emerge.
- Letting the Bones Show: Why Models Matter to Users
- "In theory, perhaps, you could present a user with any view of a system, regardless of what lies beneath. But in practice, a mismatch causes confusion at best -- bugs at worst."
- Microsoft Internet Explorer Favorites Example:
- "A user of Internet Explorer thinks of "Favorites" as a list of names of Web sites that persist from session to session. But the implementation treats a Favorite as a file containing a URL, and whose filename is put in the Favorites list. That's a problem if the Web page title contains characters that are illegal in Windows filenames. Suppose a user tries to store a Favorite and types the following name for it: "Laziness: The Secret to Happiness". An error message will say: "A filename cannot contain any of the following characters: \/:*?"<>|". What filename? On the other hand, if the Web page title already contains an illegal character, Internet Explorer will just quietly strip it out. The loss of data may be benign in this case, but not what the user would have expected. Quietly changing data is completely unacceptable in most applications."
- Either expose the fact that Favorites are just a collection of shortcut files, and let users leverage what they know about the file system to their benefit, or store the Favorites in a different way, so that they can be subject to their own rules, which rules would presumably be the naming rules that apply to Web pages. Either option would provide a single model that tells the user everything that he needs to know.
- Hands-On Modelers
- "Manufacturing is a popular metaphor for software development. One inference from this metaphor: highly skilled engineers design: less skilled laborers assemble the products. This metaphor has messed up a lot of projects for one simple reason == software development is all design."
- "If the people who write the code do not feel responsible for the model, or don't understand how to make the model work for an application, then the model has nothing to do with the software. If developers don't realize that changing code changes the model, then their refactoring will weaken the model rather than strengthen it. Meanwhile, when a modeler is separated from the implementation process, he or she never acquires, or quickly loses, a feel for the constraints of implementation. The basic constraint of Model-Driven Design -- that the model supports an effective implementation and abstracts key domain knowledge -- is half-gone, and the resulting models will be impractical. Finally, the knowledge and skills of experienced designers won't be transferred to other developers if the division of labor prevents the kind of collaboration that conveys the subtleties of coding a Model-Driven Design."
- "Any technical person contributing to the model must spend some time touching the code, whatever primary role he or she plays on the project. Anyone responsible for changing code must learn to express a model through the code. Every developer must be involved in some level of discussion about the model and have contact with domain experts. Those who contribute in different ways must consciously engage those who touch the code in a dynamic exchange of model ideas through the Ubiquitous Language."
Labels:
domain-driven-design
Friday, May 8, 2020
Domain Driven Design Chapter 2 Summary
Chapter 2: Communication and the Use of Language
- "A domain model can be the core of a common language for a software project."
- "The model is a set of concepts built up in the heads of people on the project, with terms and relationships that reflect domain insight."
- "To make most effective use of a model, it needs to pervade every medium of communication."
- Ubiquitous Language
- Domain experts:
- have limited understanding of the technical jargon of software development.
- use the jargon of their field.
- Developers:
- may understand and discuss the system in descriptive, functional terms, devoid of the meaning carried by the experts' language.
- may create abstractions that support their design, but are not understood by the domain experts.
- This causes a linguistic divide, where domain experts vaguely describe what they want, and developers vaguely understand.
- With conscious effort, the domain model can provide the backbone of a common language.
- Ubiquitous language:
- includes names of classes and prominent operations.
- includes terms to discuss rules that have been made explicit in the model.
- is supplemented with terms from high-level organizing principles imposed on the model.
- is enriched with the names of patterns the team commonly applies to the domain model.
- meanings of words and phrases echo the semantics of the model.
- "The more pervasively the language is used, the more smoothly understanding will flow."
- Points to apply:
- Use the model as the backbone of a language.
- Commit the team to exercise that language in all communication, including:
- Code
- Diagrams
- Writing
- Speech (especially)
- Iron out difficulties by experimenting with alternative expressions, which reflect alternative models.
- Then refactor the code to conform to the new model.
- Resolve confusion over terms in conversation.
- Recognize that a change to the ubiquitous language is a change to the model.
- Domain experts should object to terms or structures that are awkward or inadequate to convey domain understanding.
- Developers should watch for ambiguity or inconsistency that will trip up design.
- Example: Working Out a Cargo Router
- Gives two examples of a conversation between a developer and a domain expert, one where the developer primarily uses software technical terms, and one where the code reflects a model and a shared language, demonstrating the conciseness of the conversation.
- Modeling Out Loud
- "One of the best ways to refine a model is to explore with speech, trying out loud various constructs from possible model variations. Rough edges are easy to hear."
- As an addendum to the ubiquitous language pattern:
- Play with the model as you talk about the system.
- Describe scenarios out loud using the elements and interactions of the model, combining concepts in ways allowed by the model.
- Find easier ways to say what you need to say, and then take those ideas back down to the diagrams and the code.
- One Team, One Language
- "Technical people often feel the need to "shield" the business experts from the domain model. [...] Of course there are technical components of the design that may not concern the domain experts, but the core of the model had better interest them. Too abstract? Then how do you know the abstractions are sound? Do you understand the domain as deeply as they do? [...] [A] domain expert is assumed to be capable of thinking somewhat deeply about his or her field. If sophisticated domain experts don't understand the model, there is something wrong with the model."
- "The domain experts can use the language of the model in writing use cases, and can work even more directly with the model by specifying acceptance tests."
- "Multiplicity of languages is often necessary, but the linguistic division should never be between the domain experts and the developers."
- Documents and Diagrams
- "Simple, informal UML diagrams can anchor a discussion. Sketch a diagram of three to five objects central to the issue at hand, and everyone can stay focused."
- "The trouble comes when people feel compelled to convey the whole model or design through UML. A lot of object model diagrams are too complete and, simultaneously, leave too much out."
- People feel it needs to show all the detail they will code.
- With all that detail, no one can see the forest for the trees.
- Yet in spite of that detail, important information is still missing.
- Behavior and constraints are not so easily illustrated.
- This falls to supplemental text or conversation
- "Diagrams are a means of communication and explanation, and the facilitate brainstorming. They serve these ends best if they are minimal."
- "The vital detail about the design is captured in the code."
- Written Design Documents
- "[M]aking written documents that actually help the team produce good software is a challenge."
- "Once a document takes on a persistent form, it often loses its connection with the flow of the project. It is left behind by the evolution of the code, or by the evolution of the language of the project."
- Two general guidelines for evaluating a document:
- Documents Should Complement Code and Speech
- "A document shouldn't try to do what the code already does well. The code already supplies the detail. It is an exact specification of program behavior."
- "Other documents need to illuminate meaning, to give insight into large-scale structures, and to focus attention on core elements."
- "Documents can clarify design intent when the programming language does not support a straightforward implementation of a concept."
- Documents Should Work for a Living and Stay Current
- "A document must be involved in project activities."
- If a document in not read or found to be compelling or is being left behind, then the document is not relevant or not important enough to update.
- "It could be safely archived as history, but left active it could create confusion and hurt the project. And if a document isn't playing an important role, keeping it up to date through sheer will and discipline wastes effort."
- Executable Bedrock
- Well written code can be very communicative, but to ensure that it communicates the correct message takes effort.
- The behavior of the code is indisputable, but that does not mean that what the written code says reflects this behavior. Misnamed or unclear class, function, and variable names can pollute the meaning.
- "It takes fastidiousness to write code that doesn't just do the right thing but also says the right thing."
- "To communicate effectively, the code must be based on the same language used to write the requirements -- the same language that the developers speak with each other and with domain experts."
- Explanatory Models
- "The model that drives the design is one view of the domain, but it may aid learning to have other views, used only as educational tool, to communicate general knowledge of the domain."
- "One particular reason that other models are needed is scope. The technical model that drives the software development process must be strictly pared down to the necessary minimum to fulfill its functions. An explanatory model can include aspects of the domain that provide context that clarifies the more narrowly scoped model."
- Example: Shipping Operations and Routes
- The example starts by showing a UML diagram of part of the model. While accurate the meaning isn't readily transparent.
- Next a more free form diagram following a timeline is given, which much more clearly conveys some ideas that were originally shown in the UML diagram.
- Together they are easier to understand than either view alone.
Labels:
domain-driven-design
Subscribe to:
Posts (Atom)