
From the Translator: published for you 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 .
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:
- SSingle Responsibility Principle. A module should have one and only one reason to change.
- (classes and other elements should be open for extension but closed for modification.
- (functions that use a base type should be able to use subtypes of the base type without knowing it.
- (software entities should not depend on methods they do not use.
- (modules at higher levels should not depend on modules at lower levels.
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
endIn 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
endThere 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:
- Two-Year Practical Course .
- Online Course .
- Practical Year Course .
Source: habr.com
