We write flexible code using SOLID

We write flexible code using SOLID

From the Translator: published for you the article by Severin Perez on the use of SOLID principles in programming. The information in this article will be useful for both beginners and experienced programmers.

If you are involved in development, you have likely heard of the SOLID principles. They allow a programmer to write clean, well-structured, and easily maintainable code. It is worth noting that there are various approaches in programming about how to properly carry out a given task. Different specialists have different ideas and understandings of the 'right way,' depending on each individual's experience. Nevertheless, the ideas proclaimed in SOLID are accepted by nearly all representatives of the IT community. They have become a starting point for the emergence and development of many good management methods in development.

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

Skillbox recommends: Practical Course "Mobile Developer PRO".

Reminder: for all readers of 'Habr' - a discount of 10,000 rubles when enrolling in any Skillbox course with the promo code 'Habr'.

What is SOLID?

This term is an acronym, where each letter represents the beginning of a specific principle's name:

Single Responsibility Principle


The Single Responsibility Principle (SRP) states that every class or module in a program should be responsible for only one part of the functionality of that program. Moreover, the elements of this responsibility should be assigned to its class, rather than distributed across unrelated classes. The developer and chief evangelist of SRP, Robert C. Martin, describes responsibility as the reason for change. He originally proposed this term as one of the elements in his work "Principles of Object-Oriented Design." This concept incorporated much of the cohesion principles previously defined by Tom DeMarco.

Several concepts articulated by David Parnas were also integrated into this framework. The two main concepts are encapsulation and information hiding. Parnas asserted that the division of a system into separate modules should not be based on an analysis of flowcharts or execution paths. Each of the modules should encapsulate a specific solution that provides minimal information to the clients.

Interestingly, Martin provided an example involving the senior managers of a company (COO, CTO, CFO), each of whom uses specific business software for different purposes. Ultimately, any of them can implement changes to the software without affecting the interests of other managers.

God Object

As usual, the best way to learn about SRP is to see everything in action. Let's take a look at a piece of code that does NOT adhere to the Single Responsibility Principle. This is Ruby code that describes the behavior and attributes of a space station.

Review the example and try to identify the following:
The responsibilities of the objects declared 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} available: #{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

In fact, our space station is not functional (I don't think I'll be getting a call from NASA anytime soon), but there’s plenty to analyze here.

So, the SpaceStation class has several different responsibilities (or tasks). They can all be categorized by type:

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

Even though no staff members are assigned to the class, we can easily imagine who is responsible for what. Likely, the scientist controls the sensors, the logistician oversees resource supplies, the engineer manages fuel reserves, and the pilot operates the thrusters.

Can we say that this program does not adhere to the SRP? Yes, certainly. However, the SpaceStation class is a typical 'God object' that knows everything and does everything. This is a fundamental anti-pattern in object-oriented programming. For beginners, such objects are extremely difficult to maintain. So far, the program is very simple, yes, but imagine what will happen if we add new features. Perhaps our space station will need a medical station or a conference room. And the more features there are, the larger the SpaceStation will grow. Since this object will be connected to others, maintaining the entire system will become even more complicated. Ultimately, we might disrupt operations, for instance, of the accelerators. If a researcher requests changes in how the sensors work, it can significantly affect the station's communication systems.

Violating the SRP principle may yield a short-term tactical victory, but in the end, we will 'lose the war,' as maintaining such a monster will become quite difficult in the future. It is best to break the program into separate code segments, each responsible for a specific operation. Understanding this, let's modify the SpaceStation class.

Distribute responsibilities

Above, we defined four types of operations controlled by the SpaceStation class. During refactoring, we will keep these in mind. The updated code better adheres to 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} available: #{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. Our SpaceStation class has become more like a container that initiates operations for its dependent parts, including a set of sensors, a supply system, a fuel tank, and thrusters.

There is now a corresponding class for each of the variables: Sensors; SupplyHold; FuelTank; Thrusters.

This version of the code includes several important changes. Specifically, individual functions are not only encapsulated in their own classes, but they are also organized to be predictable and consistent. We group similar functional elements to adhere to the principle of cohesion. Now, if we need to alter the system's operation, moving from a hash structure to an array, we can simply use the SupplyHold class without affecting other modules. This way, if the officer responsible for logistics makes a change in their section, the other station elements will remain untouched. Furthermore, the SpaceStation class will not even be aware of the changes.

Our officers working on the space station are likely pleased with the changes, as they can request exactly what they need. Note that the code features methods like report_supplies and report_fuel, which are found in the SupplyHold and FuelTank classes. What if Earth requests a change in how reports are generated? Both classes, SupplyHold and FuelTank, would need to be modified. And what if the method for delivering fuel and supplies needs to change? We would likely need to alter the same classes again. This already violates the SRP principle. Let's fix that.

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  0
      @linked_item.get_supplies.each do |type, quantity|
        puts "#{type} available: #{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 available: 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 divided into two new classes, FuelReporter and SupplyReporter. Both are subclasses of the Reporter class. Additionally, we have added instance variables to the SpaceStation class to initialize the required subclass when necessary. Now, if Earth decides to change something else, we will make adjustments to the subclasses rather than the main class.

Of course, some classes still depend on each other. For instance, the SupplyReporter object depends on SupplyHold, while FuelReporter depends on FuelTank. Naturally, the boosters must be linked to the fuel tank. However, everything looks logical here, and making changes won't be particularly difficult — editing the code of one object won't significantly affect another.

Thus, we have created modular code where the responsibilities of each object/class are clearly defined. Working with such code is not a problem, and its maintenance will be a straightforward task. We transformed the entire "god object" into SRP.

Skillbox recommends:

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster