Single Responsibility & Domain Design: Action and Agent
Clean architecture (logical design) and clean code (physical design) both have a common success key and that is Separation of Concerns/Responsibilities and maintaining their independence. If the logical design is not clean in this perspective so the physical design will follow.
A Single Responsibility
Let’s think of — A statement of work (or Specification of a work or Requirements of a Work): Customer states: As a user, I can tag on different documents.
The Agent
In the system the user will see, he is putting tags whereas in reality there is a class/module that is performing for him. And this we call “Agent”, who is responsible with that single responsibility to make that happen. In this case, we call this agent “Tagger” who implements the concept of “Tagging” with an object called “Tag” on objects that are called “Taggable”.
Death of Logical Design
The basic problem happens when we make this tagging happen with 100–500 lines of codes and do not care to create objects, thinking about what is the action, who is the agent, and what are his objects. As the requirement changes each time we have to adjust the code and applying object-orientation methods becomes impossible.
Why this happens, as we clearly avoid logical design and start immediately the physical design. Currently, I see in the name of architecture it is all about designing deployment and technology stack and the communication is among them. I rarely see people give time to the logical design of the domain and straight jump to code after the WBS.
Action to Agent Noun
The verb to its Noun to Interface
Identifying a verb from the requirements and converting it to a concept with a gerund (ing) is not that hard.
I want to Tag/Ability to tag — — → Tagging → Interface
Tagging { doTag(Tag t, Taggable object);}
I want to assign types/Ability to assign types — — → Classifying
Classifying { doClassify(Type t, Classifiable object);}
I want to create a version/Ability to create versions — — → Versioning
Versioning { doVersion(Version t, Versionable object);}
Agent Noun ( suffix — or, er )
Now whoever implements this concept of Action becomes the Agent (Implementer).
Tagging — — -> Tagger — -> Implementation
Tagger Implements Tagging {
doTag(Tag t, Taggable object){
##TODO: any business logic e.g. check if tag exists
object.tag(t);
}
}
Action Concept/Interface vs Object Traits/Interface ( Tagging vs Taggable )
To perform the action by the agent on an object, the object must be compatible with that action. This compatibility of the object is its ability/trait/interface to interact with the agent.
Taggable { tag(Tag t);}
And when the object implements it
Object implements Taggable {
tag(Tag t){
this.setTagId(t.getId()); // any other business logics.
}
}
Reusability and Requirement changes
Now when we see there are different kinds of Tagging concepts, e.g. Manual Tagging, Auto-Tagging then just extend the concept/interface and extend the implementation as follows. And when we see there are different kinds of taggable then do the same with the Taggable concept and its implementations. By creating Abstract and Base classes we can make reusable code and even apply patterns if requires.
Thinking/Concept is always first
What is the core takeaway here, if we can not define an action concept and name it, there is a huge possibility that we are not understanding the single responsibility and we will end up with a messy design and code.
###
Disclaimer: The views reflected in this article are the views of the author and do not necessarily reflect the views of any past or present employer of the author.