We write flexible code using SOLID

We write flexible code using SOLID

From the translator: published for you article by Severin Perez about using SOLID principles in programming. The information from the article will be useful for both beginners and programmers with experience.

If you're a developer, you've most likely heard of the SOLID principles. They enable the programmer to write clean, well-structured, and easily maintainable code. It is worth noting that in programming there are several approaches to how to properly perform a particular job. Different specialists have different ideas and understanding of the β€œright way”, it all depends on the experience of each. Nevertheless, the ideas proclaimed in SOLID are accepted by almost all representatives of the IT community. They have been the starting point for the emergence and development of many good development management practices.

Let's understand what the SOLID principles are and how they help us.

Skillbox recommends: Practical course "Mobile Developer PRO".

We remind you: for all readers of "Habr" - a discount of 10 rubles when enrolling in any Skillbox course using the "Habr" promotional code.

What is SOLID?

This term is an abbreviation, each letter of the term is the beginning of the name of a certain principle:

  • Single Responsibility Principle (Single Responsibility Principle). A module can have one and only one reason to change.
  • The Open/Closed Principle (principle of openness/closedness). Classes and other elements should be open for extension but closed for modification.
  • β€ŠThe Liskov Substitution Principle (the Liskov substitution principle). Functions that use a base type must be able to use subtypes of the base type without knowing it.
  • The IInterface Segregation Principleβ€Š (principle of separation of the interface). Programmatic entities should not depend on methods they do not use.
  • The Ddependency Inversion Principle (dependency inversion principle). Upper-level modules should not depend on lower-level modules.

Single Responsibility Principle

β€Š
The Single Responsibility Principle (SRP) states that each class or module in a program should be responsible for only one piece of that program's functionality. In addition, elements of this responsibility should be assigned to their own class, and not distributed to unrelated classes. SRP developer and chief evangelist, Robert C. Martin, describes accountability as a driver of change. He originally proposed the term as one of the elements of his work "Principles of Object-Oriented Design". The concept incorporates much of the connectivity pattern that was previously defined by Tom DeMarco.

The concept also included several concepts formulated by David Parnas. The two main ones are encapsulation and information hiding. Parnas argued that the division of the system into separate modules should not be based on the analysis of block diagrams or execution flows. Any of the modules should contain a specific solution that provides a minimum of information to customers.

By the way, Martin gave an interesting example with the top managers of the company (COO, CTO, CFO), each of which uses specific business software for a different purpose. As a result, any of them can implement changes in the software without affecting the interests of other managers.

divine object

As usual, the best way to learn SRP is to see it in action. Let's look at a section of the program that does NOT follow the Single Responsibility Principle. This is Ruby code describing the behavior and attributes of the space station.

Review the example and try to determine the following:
The duties of those objects that are proclaimed in the SpaceStation class.
Those who may be interested in the operation of the space station.

class SpaceStation
  def initialize
    @supplies = {}
    @fuel = 0
  end
 
  def run_sensors
    puts "----- Sensor Action -----"
    puts "Running sensors!"
  end
 
  def load_supplies(type, quantity)
    puts "----- Supply Action -----"
    puts "Loading #{quantity} units of #{type} in the supply hold."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Supply Action -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Using #{quantity} of #{type} from the supply hold."
      @supplies[type] -= quantity
    else
      puts "Supply Error: Insufficient #{type} in the supply hold."
    end
  end
 
  def report_supplies
    puts "----- Supply Report -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} avalilable: #{quantity} units"
      end
    else
      puts "Supply hold is empty."
    end
  end
 
  def load_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Loading #{quantity} units of fuel in the tank."
    @fuel += quantity
  end
 
  def report_fuel
    puts "----- Fuel Report -----"
    puts "#{@fuel} units of fuel available."
  end
 
  def activate_thrusters
    puts "----- Thruster Action -----"
    if @fuel >= 10
      puts "Thrusting action successful."
      @fuel -= 10
    else
      puts "Thruster Error: Insufficient fuel available."
    end
  end
end

Actually, our space station is non-functional (I think I won't get a call from NASA in the foreseeable future), but there is something to analyze here.

So, the SpaceStation class has several different responsibilities (or tasks). All of them can be divided into types:

  • sensors;
  • supplies (consumables);
  • fuel;
  • accelerators.

Even though none of the station staff is identified in the class, we can easily imagine who is responsible for what. Most likely, the scientist controls the sensors, the logistician is responsible for supplying resources, the engineer is responsible for fuel supplies, and the pilot controls the boosters.

Can we say that this program is not SRP compliant? Yes, sure. But the SpaceStation class is a typical "divine object" that knows everything and does everything. This is the main anti-pattern in object-oriented programming. For a beginner, such objects are extremely difficult to maintain. So far, the program is very simple, yes, but imagine what happens if we add new features. Perhaps our space station will need an infirmary or a meeting room. And the more features there are, the stronger SpaceStation will grow. Well, since this object will be connected with others, the maintenance of the entire complex will become even more difficult. As a result, we can disrupt the operation of, for example, accelerators. If a research officer requests changes to the operation of the sensors, then this may well affect the station's communication systems.

Violation of the SRP principle can give a short-term tactical victory, but in the end we will β€œlose the war”, it will become very difficult to serve such a monster in the future. It is best to divide the program into separate sections of code, each of which is responsible for performing a specific operation. With this in mind, let's change the SpaceStation class.

Let's share the responsibility

Above, we have defined four types of operations that are controlled by the SpaceStation class. When refactoring, we will keep them in mind. The updated code better matches the SRP.

class SpaceStation
  attr_reader :sensors, :supply_hold, :fuel_tank, :thrusters
 
  def initialize
    @supply_hold = SupplyHold.new
    @sensors = Sensors.new
    @fuel_tank = FuelTank.new
    @thrusters = Thrusters.new(@fuel_tank)
  end
end
 
class Sensors
  def run_sensors
    puts "----- Sensor Action -----"
    puts "Running sensors!"
  end
end
 
class SupplyHold
  attr_accessor :supplies
 
  def initialize
    @supplies = {}
  end
 
  def load_supplies(type, quantity)
    puts "----- Supply Action -----"
    puts "Loading #{quantity} units of #{type} in the supply hold."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Supply Action -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Using #{quantity} of #{type} from the supply hold."
      @supplies[type] -= quantity
    else
      puts "Supply Error: Insufficient #{type} in the supply hold."
    end
  end
 
  def report_supplies
    puts "----- Supply Report -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} avalilable: #{quantity} units"
      end
    else
      puts "Supply hold is empty."
    end
  end
end
 
class FuelTank
  attr_accessor :fuel
 
  def initialize
    @fuel = 0
  end
 
  def get_fuel_levels
    @fuel
  end
 
  def load_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Loading #{quantity} units of fuel in the tank."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Using #{quantity} units of fuel from the tank."
    @fuel -= quantity
  end
 
  def report_fuel
    puts "----- Fuel Report -----"
    puts "#{@fuel} units of fuel available."
  end
end
 
class Thrusters
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Thruster Action -----"
    if @linked_fuel_tank.get_fuel_levels >= 10
      puts "Thrusting action successful."
      @linked_fuel_tank.use_fuel(10)
    else
      puts "Thruster Error: Insufficient fuel available."
    end
  end
end

There are many changes, the program now looks definitely better. Now our SpaceStation class has become more like a container in which operations are initiated for dependent parts, including a set of sensors, a consumable supply system, a fuel tank, boosters.

For any of the variables there is now a corresponding class: Sensors; SupplyHold; fuel tank; Thrusters.

There are several important changes in this version of the code. The fact is that individual functions are not only encapsulated in their own classes, they are organized in such a way as to become predictable and consistent. We group elements that are similar in functionality to follow the principle of connectivity. Now, if we need to change the way the system works by moving from a hash structure to an array, just use the SupplyHold class, there is no need to affect other modules. Thus, if the logistics officer changes something in his section, the rest of the station elements will remain intact. In this case, the SpaceStation class will not even be aware of the changes.

Our space station officers are probably happy with the changes as they can request the ones they need. Note that the code includes methods such as report_supplies and report_fuel contained in the SupplyHold and FuelTank classes. What happens if the Earth asks to change the way reports are generated? You will need to change both the SupplyHold and FuelTank classes. But what if you need to change the way fuel and consumables are delivered? You will probably have to change all the same classes again. And this is a violation of the SRP principle. Let's fix this.

class SpaceStation
  attr_reader :sensors, :supply_hold, :supply_reporter,
              :fuel_tank, :fuel_reporter, :thrusters
 
  def initialize
    @sensors = Sensors.new
    @supply_hold = SupplyHold.new
    @supply_reporter = SupplyReporter.new(@supply_hold)
    @fuel_tank = FuelTank.new
    @fuel_reporter = FuelReporter.new(@fuel_tank)
    @thrusters = Thrusters.new(@fuel_tank)
  end
end
 
class Sensors
  def run_sensors
    puts "----- Sensor Action -----"
    puts "Running sensors!"
  end
end
 
class SupplyHold
  attr_accessor :supplies
  attr_reader :reporter
 
  def initialize
    @supplies = {}
  end
 
  def get_supplies
    @supplies
  end
 
  def load_supplies(type, quantity)
    puts "----- Supply Action -----"
    puts "Loading #{quantity} units of #{type} in the supply hold."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Supply Action -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Using #{quantity} of #{type} from the supply hold."
      @supplies[type] -= quantity
    else
      puts "Supply Error: Insufficient #{type} in the supply hold."
    end
  end
end
 
class FuelTank
  attr_accessor :fuel
  attr_reader :reporter
 
  def initialize
    @fuel = 0
  end
 
  def get_fuel_levels
    @fuel
  end
 
  def load_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Loading #{quantity} units of fuel in the tank."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Using #{quantity} units of fuel from the tank."
    @fuel -= quantity
  end
end
 
class Thrusters
  FUEL_PER_THRUST = 10
 
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Thruster Action -----"
    
    if @linked_fuel_tank.get_fuel_levels >= FUEL_PER_THRUST
      puts "Thrusting action successful."
      @linked_fuel_tank.use_fuel(FUEL_PER_THRUST)
    else
      puts "Thruster Error: Insufficient fuel available."
    end
  end
end
 
class Reporter
  def initialize(item, type)
    @linked_item = item
    @type = type
  end
 
  def report
    puts "----- #{@type.capitalize} Report -----"
  end
end
 
class FuelReporter < Reporter
  def initialize(item)
    super(item, "fuel")
  end
 
  def report
    super
    puts "#{@linked_item.get_fuel_levels} units of fuel available."
  end
end
 
class SupplyReporter < Reporter
  def initialize(item)
    super(item, "supply")
  end
 
  def report
    super
    if @linked_item.get_supplies.keys.length > 0
      @linked_item.get_supplies.each do |type, quantity|
        puts "#{type} avalilable: #{quantity} units"
      end
    else
      puts "Supply hold is empty."
    end
  end
end
 
iss = SpaceStation.new
 
iss.sensors.run_sensors
  # ----- Sensor Action -----
  # Running sensors!
 
iss.supply_hold.use_supplies("parts", 2)
  # ----- Supply Action -----
  # Supply Error: Insufficient parts in the supply hold.
iss.supply_hold.load_supplies("parts", 10)
  # ----- Supply Action -----
  # Loading 10 units of parts in the supply hold.
iss.supply_hold.use_supplies("parts", 2)
  # ----- Supply Action -----
  # Using 2 of parts from the supply hold.
iss.supply_reporter.report
  # ----- Supply Report -----
  # parts avalilable: 8 units
 
iss.thrusters.activate_thrusters
  # ----- Thruster Action -----
  # Thruster Error: Insufficient fuel available.
iss.fuel_tank.load_fuel(100)
  # ----- Fuel Action -----
  # Loading 100 units of fuel in the tank.
iss.thrusters.activate_thrusters
  # ----- Thruster Action -----
  # Thrusting action successful.
  # ----- Fuel Action -----
  # Using 10 units of fuel from the tank.
iss.fuel_reporter.report
  # ----- Fuel Report -----
# 90 units of fuel available.

In this latest version of the program, responsibilities have been broken down into two new classes, FuelReporter and SupplyReporter. They are both children of the Reporter class. In addition, we have added instance variables to the SpaceStation class so that we can initialize the desired subclass as needed. Now, if the Earth decides to change something else, then we will make changes to the subclasses, and not to the main class.

Of course, some classes we still depend on each other. For example, the SupplyReporter object depends on SupplyHold, and the FuelReporter depends on FuelTank. Of course, the boosters must be connected to the fuel tank. But here everything already looks logical, and making changes will not be particularly difficult - editing the code of one object will not affect the other too much.

Thus, we have created a modular code, where the responsibilities of each of the objects / classes are precisely defined. Working with such code is not a problem, maintaining it will be a simple task. We have converted the entire "divine object" into SRP.

Skillbox recommends:

Source: habr.com

Add a comment