I have been coding for many years. I read many books about clean coding and practising them myself. I found that the rule for clean code is Decoupling. Another the name is Isolation. You might know Big Ball of Mud. That is about architecture. That being said, there are a lot of connections in a ball.
Many classes are coupling together. Many connections prevent us from expanding system. If we put new class in this ball. It’s hard to choose the connection for this class. That is open/close principle. But what happen if we refactor code. That means we break the connection. Classes related other classes, relying on other classes. We could not calculate the boundary of impact. We even get stressful when look at this ball.
Thus, Isolation is the solution. Isolation is a word but makes us think about other techniques such as Naming, Domain Driven Design, Dependency Injection, Single Responsibility etc. We can’t isolate code if:
- We don’t know the boundary of Domain: separating domains is separating concerns.
- We get hard to find a proper name for new class or method: a proper name makes it clear to understand quickly what object does.
- We don’t know how to make loose coupling. Cause we can’t make isolation completely. Things should work together to make an application.
- The intensity of Isolation. We can just make 10% isolation for example. We actually can not make 100% isolation. That is impossible cause a whole program need components communicate with each other.
Let’s take a look on this code. We have two models:
- Domain
- Guru
Creating Guru is a take-time process. It might take 5 minutes to get done. However, we don’t use socket for realtime effects. If process is in progress so users just see empty result and message ‘In progress’ for example.
class GuruListBuilder
attr_reader :domain
initialize(domain)
@domain = domain
end
def all
gurus
end
private
def gurus
create_gurus if domain.guru_at.nil?
Redis.current.set(redis_key) != 'inprogress' ? Guru.all : []
end
def create_gurus
Redis.current.set(redis_key, 'inprogress')
domain.update(guru_at: Time.current)
CreateGuru.call
Redis.current.set(redis_key, 'done')
end
def redis_key
'create-guru'
end
end
Guru_at attribute to prevent second request calling create_gurus method. We use redis to temporarily set status. Now looking at this code we see it’s a bit messy. With each class at first glance we should have a concept in mind. That is a sentence to describe this class in Single Responsibility Pattern (SRP). Thus, GuruBuilder for building Guru list. But in this code, the second responsibility appears. That is ‘Create Process State’ – this is a short sentence we need to figure out (SRP).
We should isolate them and make loose connection. Because they can’t work together if no connection. We don’t use Dependency Injection because the second responsibility does not change regularly. No need to make it as a instance to inject to first SR. So we isolate them by private method.
def create_process_state
Gurus::CreateProcessState.new(domain)
end
But we also want to make an instance like singleton in this class.
def create_process_state
@process_state ||= Gurus::CreateProcessState.new(domain)
end
However, what we can do with this messy code?
def create_gurus
Redis.current.set(redis_key, 'inprogress')
domain.update(guru_at: Time.current)
CreateGuru.call
Redis.current.set(redis_key, 'done')
end
Regarding to SP, what we want for this method is:
def create_gurus
domain.update(guru_at: Time.current)
CreateGuru.call
end
So we think about template method for CreateProcessState class.
def call
if block_given?
set_in_progress
yield
delete_state
end
end
We can see this is SP with set in progress and delete state. Because yield is not the implementation. Yield is like a parameter, and we don’t treat parameters like responsibility but delegation. If we delegate to other person to do thing, it means that thing is not our business. Now we know another Pattern “Delegation“. We use a ton of patterns to reach Isolation goal.
Thank for reading.