Single Responsibility & Domain Design: Action and Agent

Manna Mahmud
3 min readApr 23, 2022

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.

--

--