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. In doing so, they depend upon and have knowledge of those other Abstractions. We want to ensure that that the concrete implementations for these Abstractions are resolved consistently.
Cohesion and Coupling
Cohesion and Coupling are related and somewhat confusing terms. Before I describe the consistent management of Cohesive Abstractions, make sure you understand Cohesion and Coupling. The two previous blogs focus upon them:
- What Are Cohesion and Coupling? - Cohesion and coupling address when things should and should not be too sticky with one another
- Coupling and Cohesion – Take 2 - Let’s revisit these concepts one more time
Cohesive Abstractions
Cohesive Abstractions are when coupled elements are abstractions, such as interfaces. Our goal is to ensure that the concrete classes implementing the abstractions are consistent in their given context. 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 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 paired 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 interact with aProjectile
and itsLauncher
. - 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 loaded Projectile
were a Bullet
or Arrow
.
The concrete implementations for Launcher
and Projectile
must be consistent matching pairs.
Abstract Factory Design Pattern
One way to ensure consistent 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 returns concrete reference to an abstract definition. The concrete classes that implement the Abstract Factory must implement each of those Factory methods and return concrete references for each, such that those concrete references are consistent. Do you see what I mean about Abstract Factory being challenging as the first pattern presented 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 a 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
as a weapon system does not replace or invalidate the Rifle
weapon system. I did not show the previous Rifle
related classes due to space constraints. 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 while flying to their destination.
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. The system included a TRAINER mode so that the guns would not actually fire the shells. I didn’t work on the TRAINER feature directly; therefore, what I describe is mostly hypothetical.
Code like this was 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 behavior, but given other code I experienced on the project, I feared it might look something like the design below, where an if
statement would check the mode
and then only fire the TankGun
when mode
is not TRAINER
.
This looks like a reasonable choice, but 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. Anelse
statement could handle the nonTRAINER
mode, but there’s an issue with that as described in the next bullet. - 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. And inevitably one of those checks will be omitted somehwere critical.
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 was presisted only in the user/video table. 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 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.