How to tackle a task and following maintainable dimension
Fri 22 Jan 2021

Each task is like a problem solving in this article https://www.guruonrails.com/problem-solving-approach

First, we read the task to get to know the input and output. Before understanding completely the problem, do not write code. This step will train us to think and get big picture in our mind. When we get ready, let's go to next step.

Second, we create some concrete data. This time we might create some unit tests. We make sure that the data created is correct like expected. Because this will measure our correctness and prove that we understand what we are doing.

Third, we separate responsibility, concerns. This step we start writing interfaces. Do not dive deep in details at this step. What we need to do is getting big picture of solution. This is time for designing. A little bit of coding those are interfaces. 

  • Single Responsibility must be applied perfectly in this step. Those are like our cells. Writing these ones will help us solving whole problem when we make responsibilities work together. Obviously, we are solving smaller problems (responsibilities). However, do not write implementation now. A clear design should focus on interfaces first.

Fourth, implementing these responsibilities. We dive deep in implementation and staying focused on only one responsibility at same time. Focus on class or method we are developing. If we are coding and we have to switch files regularly, that means the logic is not centralized. Same logic (abstract level) should be in one file. What we do is the middle between input and output. So once we have data structure for input, we have enough info to make output.

Fifth, making responsibilities work together. Running tests, fixing and refactoring. Rethink about maintainability. 

So those are five steps for tackling tasks I applied myself. There are two things we should do best: Understanding problem & Separating concerns. The first one to keep us on the right track. The second one helps us to develop a maintainable system. Besides, we apply Design Patterns and Best Practices according to specific implementation by specific language programming or database.

So how much we do that is perfect? The code should solve the current problem first. Because we don't know what happen in future. Which features will be added to current code, class, method etc. So we code somehow solving current problem but easy for extending in future.

In SOLID, I found there are two principles which are most important: Single Responsibility and Open/Close. First one helps us to build maintainable code. Second one helps us to avoid tasking risks of changing code.

Because Interfaces are the most reliable so let's focus on interfaces, not implementation. Limiting number of interfaces as small as we can, by focusing on abstraction instead. For example, instead of writing two interfaces like this.

def fly; end
def walk; end
def swim; end

We create a method with high abstraction.

def travel; end

So each day passed, we have less interface in legacy system. Because system relies in Interfaces so less interfaces means less risks. In ruby on rails, we encourage to implement to interfaces. Ruby don't really have interfaces and implementation separated like Java. We we do is implementing to interfaces but flexible changing implementation. For example:

def travel
    subject.walk
end

We change way of traveling in legacy system by injecting implementation to travel interface.

#---------------------- Legacy --------------------------
Class Person
   attr_accessor :activity

   def initialize(**options)
       @activity = options[:activity]
   end
 
   def travel
       @activity.call
   end
end

Class Walking 
   def call
   end
end

person = Person.new(activity: walking)
person.travel # walking

# -------------------- New -------------------------
Person.new(activity: flying)
person.travel # flying

Thus, we add new behavior but clearly we don't change legacy code. Flying class is new interface, we don't change Person or Walking class but Person now has new ability "flying". Design pattern we applied in here are Dependency Injection, Duck Typing, Delegation.

We can not create three method at beginning such as walk, fly and swim. Because at that time, person can only walk. We didn't have Helicopter or something can help us to fly. We might think that we we start developing right now, we know Person can fly. But the truth is person might be disabled so he can't walk or swim but can fly (we pretend that lol). I know that there are a lot of devs think if we only involve "fly" method "if" blah blah. I means avoiding using "swim" and "walk".

if person.has_helicopter
   person.fly
end
# blah blah

This case, he only flies but the truth is we give him ability to swim and walk that is incorrect. Someone might give a dot like person.walk so it goes wrong, bugs appear. We can take control this by adding an IF in implementation of each method but it does not follow OOP anymore. An object identified by Attributes and Behaviors. Many IF ELSE can lead to many potential issues and unreadable code.

So at least we can do this instead.

# inherit and override
Class DisabledPerson < Person
   def travel; end
end

# or if walk method existed, do not inherit.
Class DisabledPerson
   def fly
   end
end

# DO NOT. It is ok but hard to read. Readability focuses on interfaces, not care implementation.
def travel
   disabled? ? fly : walk
end

Eventually, the questions are:

  • How we know that we need to create a variable like `activity`? Separating concerns, Isolating what changes regularly from things staying same. So we will change ways to travel. But travel behavior is same.
  • Or we should create travel method instead of walk method? Programing to interfaces in abstraction level. Close interface but Open for implementation. Programing to high level of abstraction.

We might think that those example are simple. But actually it's like we solve problem. We need to simplify problem and solve them with small dataset. Then we delegate computer to solve with big dataset. That is a rule. When we refactor code, we don't see messy code but we see responsibilities. We found that many IF ELSE leads to many responsibilities in same class. Do not focus on messy code, simplify them by seeing responsibilities. So we can separate them then. If above code is Robot class instead of Person, we might have big implementation. It's not simple code anymore appearently. Programing is actually using IF ELSE but there are IF ELSE containing more than one responsibilities. Stay focused.

def travel
   disabled? ? fly : walk
end

Look at above code. If we have 50 lines for fly method and 50 lines walk method. I say this flies and walk too much. We instantly see this is complex class. I meant do not underestimate simple example. Let's see simple things in complex ways and complex things in simple ways. Changing our viewing angle to think out of the box.

So let's think more about this. Looking for more chances to practise and improving our coding skill as well as design skill.