📜 ⬆️ ⬇️

We write flexible code using SOLID



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

If you are developing, you’ve probably heard about the principles of SOLID. 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 this or other work. Different specialists have different ideas and an understanding of the “right way”; everything depends on the experience of each. Nevertheless, the ideas proclaimed in SOLID are accepted by almost all representatives of the IT community. They became the starting point for the emergence and development of many good development management methods.
')
Let's understand what SOLID principles are and how they help us.

Skillbox recommends: Practical course "Mobile Developer PRO" .

We remind: for all readers of "Habr" - a discount of 10,000 rubles when recording for any Skillbox course on the promotional code "Habr".

What is SOLID?


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

Principle of sole responsibility


The principle of single responsibility (SRP) states that each class or module in a program should be responsible only for one part of the functionality of this program. In addition, the elements of this responsibility should be assigned to their class, and not distributed to unrelated classes. SRP developer and chief evangelist, Robert S. Martin, describes responsibility as the cause of change. Initially, he proposed this term as one of the elements of his work, Principles of Object-Oriented Design. The concept includes much of the pattern of connectivity, which was previously defined by Tom DeMarco.

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

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

Divine object


As usual, the best way to learn SRP is to see everything in action. Let's look at the site of the program, which is NOT consistent with the principle of common responsibility. This is a Ruby code that describes the behavior and attributes of the space station.

See an example and try to determine the following:
Responsibilities of those objects that are declared in the class SpaceStation.
Those who may be interested in the work 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 that I will not receive a call from NASA in the near foreseeable future), but here there is something to analyze.

So, the SpaceStation class has several different responsibilities (or tasks). All of them can be broken down by type:

Despite the fact that none of the station staff is defined in the classroom, we can easily imagine who is responsible for what. Most likely, the scientist controls the sensors, the logistician is responsible for the supply of resources, the engineer is responsible for the fuel reserves, and the pilot controls the accelerators.

Can we say that this program does not comply with the SRP? Yes of course. But the SpaceStation class is a typical “divine object,” who 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 a medical station or a meeting room. And the more functions there are, the more 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 work, for example, accelerators. If a researcher asks for changes in the work with the sensors, 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 be 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. Understanding this, let's change the SpaceStation class.

Assign responsibility

Above, we defined four types of operations that are controlled by the SpaceStation class. When refactoring, we will have them in mind. Updated code is more consistent with 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 a lot of changes, the program now looks definitely better. Now our SpaceStation class has become, rather, a container in which operations for dependent parts are initiated, including a set of sensors, a supply system for consumables, a fuel tank, accelerators.

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

There are some 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 with similar functionality in order to follow the principle of connectivity. Now, if we need to change the principle of the system, moving from a hash structure to an array, just use the class SupplyHold, we will not have to affect other modules. Thus, if the logistics officer changes something in his section, the remaining elements of the station will remain intact. However, the SpaceStation class will not even be aware of the changes.

Our space station officers are probably excited about the changes, as they may be requesting those they need. Please note that the code has methods such as report_supplies and report_fuel contained in the classes SupplyHold and FuelTank. What happens if the Earth asks to change the way reports are generated? It will be necessary to change both classes, SupplyHold and FuelTank. And what if you need to change the method of delivery of fuel and consumables? You may have to change all the same classes again. And this is a violation of the SRP principle. Let's fix it.

 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, the responsibilities were divided into two new classes, the FuelReporter and the SupplyReporter. They are both children of the Reporter class. In addition, we added instance variables to the SpaceStation class so that, if necessary, initialize the desired subclass. Now, if the Earth decides to change something else, we will edit the subclasses, not the main class.

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

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

Skillbox recommends:

Source: https://habr.com/ru/post/442928/


All Articles