DRAFT – What Is Cohesive Abstraction?
How to manage cohesion among abstractions consistently
Introduction
This blog is a continuation of the previous Abstract blog where I follow up with Cohesive Abstractions.
Abstractions may reference other Abstractions, depend upon other Abstractions or have knowledge of them. We want to ensure that when this occurs that the concrete implementations for these Abstracts are resolved consistently.
Cohesion and Coupling
Before I describe the consistent management of Cohesive Abstractions, I need to explain Cohesion and Coupling.
Cohesion
It took me a while before I had a solid understanding of Cohesion.
Elements are cohesive when there’s an intrinsic relationship that connects them. The relationship is often gestalt in that the elements only have meaningful utility when combined as a whole. When an update is needed, most or all elements in the cohesive relationship will require updating.
The intrinsic relationship between screw and screwdriver is an example. Neither is functional without the other. The screwdriver must also match the type of screw. For example, a flathead screwdriver won’t work on a Phillips head screw. If the design changes from a flathead screw to a Phillips head screw, then the screwdriver will need to change from a flathead to a Phillips head likewise.
Nuts and bolts are cohesive. They only function when screwed together, and when their sizes match.
We want our designs to have high cohesion. That is, software elements with intrinsic relationships should be near one another in the design.
Low cohesion occurs when related software elements have been distributed across the design. For example, you’re enhancing an existing behavior, and it requires changes to a dozen files distributed throughout the design. Additionally, the files being updated also contain code that’s not directly related to the behavior that you’re updating.
Low cohesion has at least two concerns:
- Did you update all the files that need to be updated?
- What if your update breaks the other content that’s not directly related to the behavior you’re updating?
Another common example of low cohesion I’ve seen is state machine behavior (blog TBD) spread across the implementation.
This usually happens when a class has a private
attribute named something like status
or state
.
Its type is often an enum
, but I’ve also seen it as a boolean
and even a String
. Its implied privacy is violated with get
and set
accessors, which provide direct access to the private
attribute.
Rather than keeping the state machine behavior encapsulated within the class, the class becomes the place where state
or status
reside. Other classes will access status
/state
via the get
accessor, update status
/state
with a new value via the set
accessor based upon their own business logic code. Not only does this low cohesive design distribute the state machine implementation across far flung regions of the design, it distributes behavior. This makes it more difficult to know what the state machine will do as a whole when the status
/state
is updated from any place in the implementation.
With cohesion software elements, a change to one element may require a change to the other related cohesive elements, like how a change in screw head causing a change in the screwdriver. Highly cohesive software elements are close to one another, such as in the same package, making it more likely that all required updates will occur consistently.
I consider the methods of an interface
cohesive when:
- The methods are functionally related and support each other.
- The removal of one of the methods diminishes the whole.
Traditional Create, Read, Update and Delete (CRUD) operations are cohesive in my mind. Remove any of them, and the remaining operations are incomplete. The requirements for one of my features contained Create, Update and Delete requirements for a set of domain elements, but there were no Read requirements. I asked my requirements creator why I was implementing features to manage data when there were no requirements to read the data. Oops.
A cohesive interface tends to follow the Interface Segregation Principle (ISP). An interface that violates ISP contains methods that can be separated into their own cohesive interfaces.
Coupling
Coupling is often uttered in the same breath with cohesion. It took me a while to gain a solid understanding of coupling, especially when used with cohesion. They seemed similar yet different, and I wasn’t sure how to distinguish them.
Coupling is also about connected elements as well. However, unlike cohesive elements, coupled elements do not intrinsic relationships.
A Reese’s Peanut Butter Cup couples chocolate and peanut butter together. I know for many, a peanut butter cup would be an excellent example of cohesion, but I’m the odd duck who doesn’t like peanut butter cups, even though I like chocolate and peanut butter separately.
We want our designs to have loose coupling. That is, we don’t want unrelated elements to be stuck together too tightly.
A Reese’s Cup would be tightly coupled if it were the only form from which we could get chocolate or peanut butter. Imagine scraping the insides from a Reese’s Cup as the only means to make a peanut butter sandwich.
Tight coupling occurs when disparate software concepts are stuck together. For example, the communication framework, business logic and persistence implementations are intertwined in the same method. A change to the communication framework may affect the business logic. A change in the persistence my affect the communication framework, etc. Reusing the business logic in another context becomes impossible.
Many modern software practices, such as Design Patterns, Hexagonal Architecture/Ports & Adapters, and others address tight coupling. They promote loose coupling. The first Design Pattern Principle addresses this, even if it doesn’t mention coupling directly: Program to an interface, not an implementation.
Coupling and Cohesion
Tight coupling and low cohesion, as described above, are undesirable design traits.
A good design features loose coupling of software elements that don’t change together and high cohesion for those software elements that do change together.
This blog will focus upon high cohesion among abstractions and how to ensure consistency.
Cohesive Abstractions
Cohesive Abstractions are when the coupled elements are abstractions, such as interfaces. I’ll continue with an example to demonstrate these concepts.
Call To Duty
Let’s assume that you’re a developer on a military game, such as Call to Duty, and you’re in the Weapons development team.
You’ll want to design a gun that supports behaviors, such as: ready, aim and fire.
You realize there can be multiple guns, so you want some degree of abstraction.
But not everything is a traditional gun. What about bazookas? Or what about previous weapon technology, such as the musket, or even the bow and arrow?
These can all be concrete examples of cohesive abstractions of a Launcher and Projectile. Some concrete pairs could be: Gun/Bullet, Bazooka/Shell, Musket/Ball, Bow/Arrow, Sling/Shot, etc.
Ready, aim and fire don’t quite work for some of these, so let’s generalize them with: load, aim and launch. This first design describes the cohesive abstractions:
- The
Warrior
represents the player, and we’re going to only focus upon theWarrior
’s ability to launch aProjectile
via aLauncher
. - The
Launcher
declares theload(Projectile projectile)
,aim()
andlaunch()
methods. - The
Launcher
interface has dependency upon and knowledge of theProjectile
interface.Projectile
does not have dependency or knowledge ofLauncher
.
I think this is the first time in any of my blogs where I show one interface referencing another interface. Launcher
has dependency knowledge of Projectile
.
Launcher
and Projectile
are cohesive. A Launcher
isn’t much use with a Projectile
, and Projectile
isn’t much use with a Launcher
. They must be consistent. A Bazooka
would not be of much use if its Projectile
were a Bullet
or Arrow
.
The concrete implementations for Launcher
and Projectile
must be matching pairs.
Abstract Factory Design Pattern
One way to ensure matching pairs is via the Abstract Factory Design Pattern. I wrote about this pattern briefly in the Abstract Factory section of the Factory Design Patterns blog.
The Abstract Factory Design Pattern is the first pattern presented by the Gang of Four (GoF) in their Design Pattern book, because it’s alphabetically first. It’s a tough pattern to understand, which is one reason why I think the GoF’s book is difficult to learn design patterns.
Here are two good Abstract Factory Design Pattern online resources:
The Abstraction of the Abstract Factory Pattern
An Abstract Factory Pattern is an extension of the Factory Pattern concept.
A Factory is a class that returns a concrete instance for an abstract reference. An Abstract Factory is an interface that declares a set of Factory methods each of which return concrete instances for those Factory methods such that the set of returned references are consistent. Its abstraction supports different sets of instances consistently. See what I mean about this being a difficult pattern as the first one in the GoF.
It’s not quite as confusing as it sounds at first blush, but it requires some thought before one reaches understanding. I’ll layer in the concepts a few at a time.
Let’s start with the Abstract Factory interface that’s needed for the Launcher
/Projectile
pair. WeaponsSystem
is the Abstract Factory in this example. It declares two methods that virtually create a Launcher
and a Projectile
. That is, these two methods declare an abstract contract to create references for these abstractions, but they don’t create the concrete instances themselves.
Completing the Abstract Factory Design with Concrete Factories
The above only declares the abstractions. This diagram adds the concrete Factories that define the instantiate the concrete instances for Launcher
and Projectile
, which will be resolved in this example by a Rifle
and Bullet
respectively:
Launcher
is implemented byRifle
.Projectile
is implemented byBullet
, since this is the only type ofProjectile
that’s consistent with aRifle
.RifleWeaponsSystem
implementsWeaponsSystem
. Its methods instantiate aLauncher
/Rifle
andProjectile
/Bullet
respectively. The two are ensured to be consistent.RifleWeaponSystemConfigurer
isn’t technical part of the Abstract Factory Pattern. It’s the Configurer, which is a missing concept in most GoF patterns. It creates theRifleWeaponSystem
instance and injects it into theWarrior
.- This design adds the red dashed lines used to designate ABSTRACT, CONCRETE and CONFIGURE architecture boundaries, which were introduced in What vs How in the previous blog.
Another WeaponSystem Abstract Factory
The Abstract Factory requires more design elements. It’s an investment. Here is part of the return on that investment. When we want to add a Bazooka, we update the design as follows:
The Rifle
and Bazooka
designs are identical in their Abstraction regions above the red dashed horizontal line. The distinctions below the line are almost boilerplate.
The addition of Bazooka
system does not replace or invalidate the Rifle
system. I did not show the previous Rifle
related classes due to space concerns. This design can support as many Launcher
/Projectile
weapon system as the team imagines.
And Testing Too
We can easily test the code in the Abstract regions with the same design. The distinction is that it uses Test specific concrete classes:
Warrior with Multiple WeaponSystems
Rarely would a Warrior
have only one WeaponSystem
. Different WeaponSystem
s will have different attributes, such as:
- Range
- Accuracy
- Stealthiness
- Firing repeatability
- Etc.
The following design shows how a few updates to the previous designs can support a Warrior
with multiple WeaponSystem
s with the assurance that the Launcher
and Projectile
will be consistent if each concrete WeaponSystem
concrete class implements them consistently:
- Rather than a single
WeaponSystem
attribute, theWarrior
has multipleWeasonSystem
s as aList
. - Each
WeaponSystem
has attributes, listed above, but not shown in theWeaponSystem
interface due to space constraints. - The
getBestWeapon(Target target)
method evaluates theWeaponSystem
s in theList
against thetarget
and determines which one would be the best choice. WarriorConfigurer
addsWeaponSystem
s to theWarrior
. The set ofWeaponSystem
references could change at any time.- I don’t have enough space to list the
WeaponSystem
,Launcher
andProjectile
concrete classes, but they follow the previous designs. - Except for the
List
ofWeaponSystem
s,getBestWeapon(Target target)
method and theWarrierConfigurer
updates, the rest of this design is the same as the previous designs.
Bigger Boom
I’m going to stay with the same design structure, but I’ll modify the context slightly. Instead of a Warrior
with a hand weapon, this design features a CombatVehicle
as a tank
with a TankGun
/TankGunShell
pair.
Safety Critical
I moved from Warrior
to CombatVehicle
to feature a real-world example.
I worked on a military project where the goal was to move combat vehicles, such as tanks, with their crew via large military cargo planes. The crew could train by running simulations within their vehicles to familiarize themselves with terrain, landmark features, etc. of their destination during the hours in transit.
The simulations could involve spotting targets and firing their weapons. It would be very bad to fire a tank shell from inside the cargo plane. Our software would include a TRAINER mode so that the guns would not actually fire the shells. I didn’t work on these features directly.
Code like this would be considered Safety Critical, meaning that if there’s a bug in the code, it could seriously harm or kill people. Blowing a hole in the side of the plane during transit would definitely qualify as a Safety Critical concern.
Since I didn’t work directly on this code, I don’t know how they planned to implement this, but given other code I experienced on the project, I feared it might look something like the design below, where an if
statement checks the mode
and it only fires the TankGun
when mode
is not TRAINER
.
I have several issues with this design:
Mode
feels like behavior associated with theTankGun
, but it resides in theCombatVehicle
. It will work, but what if other code interacts with theTankGun
as well. It will need to have the same logic.TRAINER
logic will be distributed across the codebase with low cohesion. How confident will we be that all theTRAINER
cases have been covered? NOTE: In case you haven’t realized it,mode
is starting to describe a state machine.- There’s no behavior when in
TRAINER
mode. While we don’t want to fire the gun while inTRAINER
mode, we want code to behave as if we had fired it for a more realistic training simulation for the crew. - The
if
statement only checks forTRAINER
. What if there’s another mode, such asMAINTENANCE
, for which we don’t want to fire the gun as well. This design will fire the gun in any nonTRAINER
mode whether desired or not. We can always enhance the implementation withelse if
orelse
statements for othermode
values, but given the distributed low cohesive design, it would clutter the applications and the maintenance will be more difficult.
Concrete Trainer Classes
We can think of TRAINER
mode like Test Doubles. They are different versions of Launcher
and Projectile
. Here’s how they would appear in the design:
This design is more cohesive. The if
logic has been removed from the CombatVehicle
.
However, there are still a few items that bug me:
- How do I know that
TankGun
andTankGunTrainer
will stay in sync with respect to behavior that’s not associated with firing the shell? The same question lingers forTankShell
andTankShellTrainer
too. That is, if there’s an update toTankGun
, will there be a corresponding update toTankGunTrainer
? Mode
doesn’t appear in the design. It’s implied in the CONFIGURE region, but there are no details. This needs a bit more work.
Final Design For Now
I continued to think about the design. I have made a few enhancements to the design, which I will describe one design region at a time.
ABSTRACT
There are zero changes in the ABSTRACT region. That’s one of benefits of this design. Throughout this blog, the ABSTRACT region has barely changed.
CONCRETE
I wanted to address the complete separation of TankGun
and TankGunTrainer
in the previous designs. Often, we want complete separation of concrete classes, but I have this nagging feeling that these two classes, along with the TankShell
/TankShellTrainer
classes, should be cohesive.
TankGun
and TankGunTrainer
should exhibit nearly identical behaviors, with the distinction that TankGun
launches the TankShell
, whereas TankGunTrainer
emulates launching the TankShell
. If the two classes are separated, we run the risk of updating TankGun
without updating TankGunTrainer
. Their core behavior would be inconsistent. This could result a training simulation that behaves differently than it would when active in combat. We don’t want our tank crew to encounter any surprises when combat behaviors are different than training behaviors.
The previous design only used the Strategy Design Pattern for TankGun
and TankGunTrainer
. This updated design adds two more design patterns: Template Method and Adapter.
TankGun
has been modified from a concrete class to an abstract class. Any previous TankGun
implementation that accessed the Actual Tank Gun would be extracted as an abstract protected method declared in the abstract TankGun
and defined and implemented in the new TankGunAdapter
. TankGunAdapter
would delegate to the Actual Tank Gun directly. TankGunTrainer
would also have to define and implement the same abstract protected methods in TankGun
, but TankGunTrainer
would have no access the Actual Tank Gun. Its implementations could be empty NO-OP methods, or it could record the Actual Tank Gun request, much like a Spy would.
This design retains the bulk of the Tank Gun behavior implementation in TankGun
. TankGunAdapter
would fulfill the behavior with the Actual Tank Gun, whereas TankGunTrainer
would not. This segregation will help ensure that the Actual Tank Gun will not be engaged while in TRAINER mode.
CONFIGURE
Mode
resides within the TankGunWeaponSystem
concrete factory. TankGunConfigurer
has no concern about mode
. Its single responsibility is to create the TankGunWeaponSystem
and inject it into the tank
instance of the CombatVehicle
.
TankGunWeaponSystem
creates the appropriate concrete TankGun
and TankGunShell
objects based upon ACTIVE or TRAINER mode
s.
This is the only place where mode
is considered, which limits the scope of incorrect configurations. All possible mode
values are considered in the code snippet. If a new mode
is added, such as MAINTENANCE, then this method will throw an Exception. NOTE: Each concrete WeaponSystem
will need to consider mode
within its own context.
I listed this as the final design for now, because I’m still not 100% satisfied with it, but I’m not going to continue beyond this design in this blog.
A future consideration includes: How is the CombatVehicle
tank
updated when the mode
changes?
I would consider using the Observer Design Pattern, which I have not presented yet (blog TBD):
Just to give a hint of what I’d do, I’d consider an Observer, probably in the CONFIGURE region, which would receive mode
update notifications and make adjustments to the tank
as needed.
I’m still not convinced that the fire()
method works in all concrete occurrences of the abstraction. Does fire()
accurately model a bow and arrow as well as an assault weapon machine gun?
Summary
Cohesive Abstraction occurs when multiple Abstractions have relationships that need to remain consistent. The Abstract Factory Design Pattern is one mechanism that helps maintain consistency.
One could argue that my designs are overengineered. That’s possible, but the design is modular with good dependency management. It can be modified without a complete redesign as has been shown throughout this blog as I’ve tweaked it.
A Cautionary Tale
Is consistency and all this effort really worth the effort? Let me close with this cautionary tale.
One of the products I worked on had a video feature where customers could request that their users upload a short video. Our product stored the video in the cloud with an external video vendor. The vendor would provide a URL to the video resource on their system, and we’d store that in a user/video database table.
One day multiple customers started contacting customer support stating that they couldn’t access any of the videos. Customer support confirmed the behavior, but they were unable to find a workaround.
Development got involved. It didn’t take us too long to realize that the user/video table was gone. Poof! Nada! Bupkis!
No. No. No. Oh God. This is really bad. This is really, really bad.
We confirmed that the videos still resided with the external vendor, but we didn’t know which video was associated with which user.
Database backups restored most of the missing user/video connections; however, the backups were either two weeks or two months old. This was years ago, and I don’t remember which. Regardless, the most recent and relevant user/video connections were still missing.
After some more investigation, we found a command in the video vendor’s API that returned the URL of our entire set of videos. Part of their URL contained our user ID that we had submitted when uploading the video. Because our vendor provided that really important nugget of information, only by chance, we were able to extract the user ID and reconstruct most of the user/video connections information. It wasn’t a complete restoration, since the table had additional context information. That information was completely gone and unrecoverable, but we were happy to give our customers the ability to view their user videos once more.
How did this happen? We never found the root cause. We think it was a comedy of errors. This is mostly speculation, but we think that it involved one of our user/video automated tests. The test manipulated the database. The test designer didn’t want to leave any test-created artifacts in the database after completion, since they could affect the next execution of the test. The test created a clean slate for each execution by deleting the user/video table upon completion. We think the automated test was executed against the production environment. When the test cleaned the test environment, it deleted the production table.
I thought I was going to lose my job. I had been recently repremanded for another vendor issue that affected customers. However, we were commended for finding the issue and resolving it well enough in a few hours. We took additional steps to prevent future disasters, such as removing developer and tester write-access to the production database.
The user/video feature wasn’t a major feature in our product. This could have a much worse disaster. Core business tables could have been deleted, and most of them would not have had an option for reconstruction.
This personal experience is the main reason why I feel that consistency is worth the effort.