Why does the code in so many robust enterprise system solutions become hard to understand, maintain, and reuse? The main reason is that development teams do not apply best practices in a disciplined way when performing use-case realizations.
According to the IBM Rational Unified Process®, or RUP®, "A use-case realization describes how a particular use case is realized within the design model, in terms of collaborating objects." The goal of a use-case realization is to produce a well-structured object-oriented design for implementing behavior defined in the use case.
Fundamentally, to use object-oriented design means to properly assign responsibilities to objects that compose the system and define the messages that pass among them. This requires skill in assigning responsibilities as well as using design patterns.
Two critical principles for responsibility assignment
Two responsibility assignment principles play a major role in achieving good object-oriented design: high cohesion and low coupling. Cohesion is a measure of the strength and focus of an object's responsibilities. It is said that an object is low in cohesion when it takes on responsibilities that should be performed by other objects; in other words, high cohesion requires limiting an object's responsibilities. High coupling results when an object relies on too many other objects. Therefore, low coupling requires limiting the dependency level among objects.
In theory, these two responsibility assignment principles seem easy to understand. In practice, they are hard to comprehend and master, especially for programmers who are making a transition to object-oriented application development. In part, this is what "thinking in objects" refers to; it is the most difficult skill to learn in the object-oriented world and also the most critical. Failure to apply the principles of high cohesion and low coupling while performing use-case realizations represents a serious risk to a software development project.
Why? Enterprise system solutions nowadays commonly adopt a multi-layered system architecture, with separate layers for presentation, controller, application, domain, and persistence. A use-case realization may require work on each of these layers. However, the most complex and critical application behaviors -- such as assigning responsibilities for business rules, business logic, or business entities -- fall within the application and domain layers. If the logic is not distributed in a correct and balanced way within these layers, there is a big risk that the system will not evolve properly as more and more functionality is added. Over time, the system (or subsystem -- the same issues apply) may become so complex that the code is hard to understand, maintain, and reuse. That is why it is so important for developers to become proficient in applying the principles of high cohesion and low coupling to the application and domain layers of a system when performing use-case realizations (see Figure 1).
Figure 1: Applying the principles of high cohesion and low coupling is especially critical within the application and domain layers of an enterprise system architecture.
Processes do not address the problem
The reason that applications frequently fail to embody the principles of high cohesion and low coupling relates to real-world software development processes. After a team defines goals for a current iteration, the first step is often to analyze their requirements and document them in use cases. After the use cases are reviewed and considered stable, the next step is to do an object-oriented analysis to support them; this involves creating a domain model (which quickly gets outdated as the project evolves). After that, the final step is to prioritize the use cases and assign them for implementation.
From this point on, use cases are normally delegated completely to the developers, with total reliance on their object-oriented design skills. Typically, they write code directly from the use-case flow of events, implementing object responsibilities and collaborations on the fly. Unfortunately, there are no quality measures for their use-case implementations except system tests, which are conducted later on. If these are successful, the new features are released into production without any verification that they were designed in accordance with high cohesion/low coupling principles.
This is a serious gap in quality control. Simply ensuring that an implementation complies with use-case requirements is not enough; unless teams pay special attention to the implementation's design, the consequences may be dire.
Some software development processes suggest modeling and designing software visually as a best practice to avoid this problem. However, some vendors promote the use of their visual modeling tools without emphasizing the importance of keeping models in synch with design decisions and changes during implementation. When the models become dated, teams often abandon them, failing to take advantage of their code-generation capability. For them, models are simply an informal way to think about or communicate a solution. They do not create an accurate set of model and design diagrams for the whole system, so there is no documentation for reviewing or controlling the quality of its internals.
Code reviews are designed to target this issue. They are normally conducted by the system architect or delegated to the team's more skilled developers. However, they are typically conducted too late in the development process, when use-case implementations are already complete, and it is time-consuming and expensive to engage in a big refactoring effort or a complete reimplementation -- if the review indicates that such an effort is needed.
Rather than trying to fix an implementation after the fact, teams need a set of practices to drive quality throughout the development effort.
Use-case realization best practices
In this section, I will describe a proven set of practices that teams can apply to build quality into use-case implementations and ensure that they adhere to the principles of high cohesion and low coupling. Briefly, these practices are:
- Design use-case realizations in team sessions.
- Communicate design ideas using interaction diagrams.
- Apply design and responsibility patterns.
- Code responsibility abstractions during design sessions.
- Get clearance before making major design changes.
- Audit changes that occurred during design implementations.
Perhaps you are asking yourself, "Does my team really need to pursue these best practices? We are all highly skilled senior developers with plenty of object-oriented experience."
The answer is "Yes"! Even on such a team, each member cannot possibly know all the tips and tricks for every tool or framework the development organization is using. Team members should always be learning from each other; this promotes both quality and consistency in the technical solution. By following these best practices, collectively, team members should be able to expose all risks and then target them effectively.
Design use-case realizations in team sessions
Use-case realizations should never be done in solitary by a single developer. This raises the risk that the person will assign and implement responsibilities on the fly, neglecting to ensure that they are cohesive and coupled correctly. It is far more effective to do use-case realizations in team sessions that include, at a minimum, the system architect and the developers responsible for the implementation. Other technical stakeholders can attend as needed (see Figure 2), but the focus should remain on object-oriented design. Business analysts, for example, need not participate unless they are required to clarify something in the use case. Doing use-case realizations in team sessions will also help less experienced developers improve their design skills and increase their likelihood of exposing potential implementation risks.
Figure 2: Use-case realization team sessions should include the system architect and developers responsible for implementing the use case.
On many teams, a single member might perform multiple roles. For example, it is perfectly acceptable for the system architect to also act as a developer to implement part of the realization. However, even if one person is capable of doing every part of the design and implementation for a use case on a small project, it is imperative that creating the use-case realization be a team effort.
This effort should take place in two installments:
- Use-case realization team session to discuss and design the use-case realizations.
- Follow-up team session to discuss required design changes. Developers working on the use-case implementation should meet with the system architect to review current findings and revisit previous design decisions.
Communicate design ideas using interaction diagrams.
How many times have you seen design ideas scribbled on a whiteboard, using informal drawings that lack consistent semantics or notation? Designing objects visually is a great practice, but doing it without a well-defined modeling notation can do more damage than good when you attempt to communicate design decisions. Drawings can quickly become incoherent if the design is quite complex (see Figure 3). Well-defined UML interaction diagrams are far more practical than informal drawings. They provide an organized way to express ideas cleanly and succinctly -- and eliminate the risk of producing complex drawings that may not be possible to interpret correctly later.
Interaction diagrams are informal documents used to communicate design ideas that come up during the use-case realization team sessions. Their purpose is to aid in assigning responsibilities to objects and not to describe in great detail every aspect of a complex process. It does not matter whether you produce the diagrams with a visual modeling tool or merely draw them by hand; either way, you can use them effectively to communicate about an object-oriented design (see Figure 4).
The UML defines two types of interaction diagrams: sequence and collaboration. In a sequence diagram, object interactions are illustrated in a kind of fence format, adding objects from left to right. Collaboration diagrams can expand both vertically and horizontally to accommodate additional objects. Each diagram has its pros and cons, but both are relatively easy to draw.
Kent Beck and Ward Cunningham proposed the adoption of CRC cards to perform object-object design during team sessions. These cards are an excellent alternative to interaction diagrams, the only drawback being that they lack a visual representation of how messages flow among objects. However, development teams should be able to compensate for this during a design session.
Figure 3: An informal system drawing without well-defined modeling notation can quickly become incoherent.
Figure 4: UML sequence diagrams provide a simple, coherent way to represent systems.
Apply design and responsibility patterns.
A design pattern is a solution to a problem within the context of a particular architecture and framework; it reflects a way of doing things that bears repeating. Applying patterns can make a design and implementation more consistent, understandable, and of higher quality.
High cohesion and low coupling are the main responsibility assignment patterns to take into account when designing use-case realizations, but design patterns also play a key role in object-oriented design. Focusing on their application to the design in context during a team session provides an excellent opportunity to exchange knowledge and reaffirm design skills. In addition, teams can create their own patterns and speed up design sessions. For example, if a development team has to design a simple CRUD (create, read, update, delete) process several times for different use cases, they might find themselves creating the same object responsibilities each time. When this happens, the team can decide to create a pattern and document it. Then, they can reference the pattern in interaction diagrams, eliminating the need to repeat the same details for each use case.
Each pattern should have a name that is shorthand for a particular design solution. This provides a rich and tailored vocabulary for the technical team -- for both verbal and modeling communication. For example, suppose a team has a shared vision for a pattern called "Entity Retrieval." When a designer says "We will apply Entity Retrieval here to get the Invoice for the Purchase," everyone on the team will understand the design and coding implications. A designer can also annotate an interaction diagram with a marker that calls out a pattern, eliminating the need to communicate fine-grained relationships and behavior involved in applying the pattern.
Code responsibility abstractions during design sessions.
Use-case realization sessions should produce a main output artifact; software development processes often suggest a design model composed of interaction and design class diagrams. However, as we noted, many times teams treat models as an informal way to think about or communicate a solution rather than as a set of specifications that must be implemented. Therefore, a formal a design model may not be the best output artifact for a use-case realization session.
Responsibility abstractions may be a better choice, unless the project source code is not accessible from the meeting location. Ideally, teams should code responsibility abstractions during their use-case realization sessions, in parallel with the design decisions they make (see Figure 5). Then, the developer or developers in charge of the use case should be responsible for completing their implementations afterwards.
This approach allows the team to reach consensus on a design that they will actually follow rather than just generating ideas that may never be implemented. This practice also exercises a minor degree of oversight for the design implementation; without it, there is no assurance that the design discussed during the session will be realized.
The focus of this session should be on coding responsibility abstractions that belong to the objects in the system's application and domain layers; as we noted earlier, this is where achieving the right levels of cohesion and coupling is most critical. Whether it is necessary to code responsibilities in other layers will be driven mainly by the system's architectural style and design decisions made during the use-case realization session. Code comments should also be used in the abstractions to detail specific ideas or behavior for a method to implement.
Figure 5: Sample code for responsibility abstractions
Get clearance before making major design changes.
No matter how careful a designer you are or how many times you revisit your design decisions, there is a risk that you will overlook something or fail to uncover a hidden problem. Designs are never one hundred percent correct; they can only be proven right or wrong during implementation. If a developer discovers a problem during implementation that the team overlooked during their sessions -- something that could lead to major design changes or pose serious risks -- then the developer has the responsibility to bring this to the system architect's attention by calling for a follow-up team session.
During such a session, the system architect and other team members can revisit the design and carefully assess the discovery. Ideally, the output artifact for such a session will be either code for correct responsibility abstractions or an informal design document with necessary corrections. Depending on the severity of the design issues, the system architect might even want to hold another meeting to revisit the whole use-case realization in context.
Developers should not call follow-up team sessions if they find only minor discrepancies between the design and its implementation that can be fixed with minor adjustments. These are to be expected; only the experience a team acquires over time as it conducts use-case realization sessions can minimize or even eliminate these problems.
Audit changes that occurred during design implementations.
Given the inevitability of these discrepancies between expressed designs and their implementation, the developer implementing the use-case realization should take responsibility for making minor adjustments. However, once the use-case implementations have been completed and unit tested, the system architect must audit the design implementations for accuracy. This is similar to performing a code review, but the focus is on verifying that the principles of high cohesion and low coupling have been properly applied, and that design decisions have been implemented in conformance with decisions made during the team sessions. Especially important is to ensure that the developers did not introduce major design changes without first calling for a follow-up team session. This architectural audit should be the last measure for verifying code quality before moving on to system testing.
In recent years, software engineering processes, in combination with object-oriented programming and improved development environments, have given software projects a greater level of control and therefore better probabilities for success. However, the processes are still not granular enough to enforce adherence to the principles of high cohesion and low coupling. It is still up to individual developers to apply these basic principles of good object-oriented design; compliance cannot be measured or automated by a software tool.
This article describes best practices that have worked well in targeting this issue for several projects. However, the main intent is to expose a problem that may be affecting many current software projects and encourage readers to work on it. My hope is that more teams will either follow the best practices described, develop new ones, or create new tools and techniques to enforce the correct application of the high cohesion and low coupling principles when performing use-case realizations.
Craig Larman, Applying UML and Patterns, Second Edition. Prentice-Hall, 2001. Excerpts online at:
IBM Rational Unified process product information:
Extreme Programming (XP) Web site:
Kent Beck and Ward Cunningham, "A Laboratory for Teaching Object-Oriented Thinking." Online at:
- A new forum has been created specifically for Rational Edge articles, so now you can share your thoughts about this or other articles in the current issue or our archives. Read what your colleagues the world over have to say, generate your own discussion, or join discussions in progress. Begin by clicking HERE.
- Global Rational User Group Community