How to have clean code with Isolation or Decoupling pattern
Wed 01 Jan 2020

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.
  • We get hard to find a proper name for new class or method.
  • 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.

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.