Rugalmas kód írása SOLID segítségével

Rugalmas kód írása SOLID segítségével

A fordítótól: megjelent az Ön számára Severin Perez cikke a SOLID elvek programozásban való használatáról. A cikkből származó információk kezdőknek és tapasztalt programozóknak egyaránt hasznosak lesznek.

Ha a fejlesztés iránt érdeklődik, valószínűleg hallott már a SOLID elvekről. Lehetővé teszik a programozó számára, hogy tiszta, jól strukturált és könnyen karbantartható kódot írjon. Érdemes megjegyezni, hogy a programozásban többféle megközelítés létezik egy adott munka helyes végrehajtására. A különböző szakembereknek eltérő elképzeléseik és elképzeléseik vannak a „helyes útról”; minden az egyes személyek tapasztalatától függ. A SOLID-ban hirdetett ötleteket azonban az informatikai közösség szinte minden képviselője elfogadja. Ezek váltak kiindulóponttá számos jó fejlesztésirányítási gyakorlat megjelenéséhez és kidolgozásához.

Megértjük, mik a SOLID elvek, és hogyan segítenek nekünk.

A Skillbox a következőket ajánlja: Gyakorlati tanfolyam "Mobile Developer PRO".

Emlékeztetünk: a "Habr" minden olvasója számára - 10 000 rubel kedvezmény, ha a "Habr" promóciós kóddal bármely Skillbox tanfolyamra jelentkezik.

Mi az a SZILÁRD?

Ez a kifejezés egy rövidítés, a kifejezés minden betűje egy adott elv nevének kezdete:

  • Single Felelősségi Alapelv. Egy modulnak csak egy oka lehet a változtatásra.
  • A Otoll/Zárt elv (nyitott/zárt elv). Az osztályoknak és egyéb elemeknek nyitottnak kell lenniük a bővítésre, de zárva a módosításra.
  •  A Liskov helyettesítési elv (Liskov helyettesítési elve). Az alaptípust használó függvényeknek képesnek kell lenniük az alaptípus altípusainak használatára anélkül, hogy tudnák.
  • A IInterfész szegregációs elve  (interfész-elválasztási elv). A szoftverentitások nem függhetnek olyan módszerektől, amelyeket nem használnak.
  • A Dfüggőség Inverziós elv (a függőségi inverzió elve). A magasabb szinteken lévő modulok nem függhetnek az alacsonyabb szinteken lévő moduloktól.

Egységes felelősség elve


Az Egységes Felelősségi Alapelv (Single Responsibility Principle, SRP) kimondja, hogy a program minden osztálya vagy modulja a program funkcióinak csak egy részéért felelős. Ezenkívül ennek a felelősségnek az elemeit a saját osztályukhoz kell hozzárendelni, nem pedig szétszórva a nem kapcsolódó osztályok között. Az SRP fejlesztője és főevangélistája, Robert S. Martin az elszámoltathatóságot írja le a változás okaként. Eredetileg ezt a kifejezést "Principles of Object-Oriented Design" című munkájának egyik elemeként javasolta. A koncepció magában foglalja a korábban Tom DeMarco által meghatározott kapcsolódási minta nagy részét.

A koncepcióban több David Parnas által megfogalmazott koncepció is szerepelt. A két fő a beágyazódás és az információrejtés. Parnas azzal érvelt, hogy a rendszer különálló modulokra osztása nem alapulhat blokkdiagramok vagy végrehajtási folyamatok elemzésén. Bármelyik modulnak tartalmaznia kell egy speciális megoldást, amely minimális információt nyújt az ügyfelek számára.

Martin egyébként egy érdekes példát hozott egy vállalat felső vezetőivel (COO, CTO, CFO), akik mindegyike más-más célra használ speciális üzleti szoftvert. Ennek eredményeként bármelyikük végrehajthat változtatásokat a szoftverben anélkül, hogy ez más vezetők érdekeit érintené.

Isteni tárgy

Mint mindig, az SRP megtanulásának legjobb módja az, ha működés közben látja azt. Nézzük meg a program azon részét, amely NEM követi az Egységes Felelősség Elvét. Ez egy Ruby kód, amely leírja az űrállomás viselkedését és attribútumait.

Tekintse át a példát, és próbálja meghatározni a következőket:
A SpaceStation osztályban deklarált objektumok felelőssége.
Akit érdekelhet az űrállomás működése.

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

Valójában az űrállomásunk nem működik (nem hiszem, hogy egyhamar fogok hívni a NASA-tól), de van itt mit elemezni.

Így a SpaceStation osztály több különböző felelősséggel (vagy feladattal) rendelkezik. Mindegyik típusra osztható:

  • érzékelők;
  • kellékek (fogyóeszközök);
  • üzemanyag;
  • gyorsítók.

Annak ellenére, hogy az állomás egyik alkalmazottja sincs osztályhoz rendelve, könnyen elképzelhetjük, ki miért felelős. Valószínűleg a tudós irányítja az érzékelőket, a logisztikus az erőforrások ellátásáért, a mérnök az üzemanyag-ellátásért, a pilóta pedig az erősítőket.

Mondhatjuk, hogy ez a program nem kompatibilis az SRP-vel? Igen, persze. De a SpaceStation osztály egy tipikus "isten objektum", amely mindent tud és mindent megtesz. Ez egy jelentős anti-minta az objektum-orientált programozásban. Egy kezdő számára az ilyen tárgyak karbantartása rendkívül nehéz. A program eddig nagyon egyszerű, igen, de képzeljük el, mi lesz, ha új funkciókat adunk hozzá. Talán az űrállomásunknak szüksége lesz egy orvosi állomásra vagy egy tárgyalóteremre. És minél több funkció van, annál nagyobb lesz a SpaceStation. Nos, mivel ez a létesítmény össze lesz kötve másokkal, az egész komplexum kiszolgálása még nehezebbé válik. Emiatt megzavarhatjuk például a gyorsítók működését. Ha egy kutató változtatásokat kér az érzékelőkön, az nagyon nagy hatással lehet az állomás kommunikációs rendszerére.

Az SRP elv megsértése rövid távú taktikai győzelmet hozhat, de a végén „elveszítjük a háborút”, és nagyon nehéz lesz fenntartani egy ilyen szörnyet a jövőben. A legjobb, ha a programot külön kódrészletekre osztja, amelyek mindegyike egy adott művelet végrehajtásáért felelős. Ha ezt megértjük, változtassuk meg a SpaceStation osztályt.

Osszuk el a felelősséget

A fentiekben négyféle műveletet határoztunk meg, amelyeket a SpaceStation osztály vezérel. Ezeket szem előtt tartjuk a refaktorálás során. A frissített kód jobban illeszkedik az SRP-hez.

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

Sok változás van, a program most határozottan jobban néz ki. Mostanra a SpaceStation osztályunk inkább egy olyan konténerré vált, amelyben a műveleteket elindítják a függő alkatrészekhez, beleértve az érzékelőket, a fogyóanyag-ellátó rendszert, az üzemanyagtartályt és a nyomásfokozókat.

Bármelyik változóhoz most már van egy megfelelő osztály: Sensors; SupplyHold; Üzemanyag tartály; Tolómotorok.

A kód ezen verziójában számos fontos változás található. A lényeg az, hogy az egyes funkciókat nem csak saját osztályaikban foglalják össze, hanem úgy szervezik, hogy kiszámíthatóvá és következetessé váljanak. A hasonló funkcionalitású elemeket csoportosítjuk a koherencia elvét követve. Most, ha módosítanunk kell a rendszer működését, áttérve a hash-struktúráról egy tömbre, csak használja a SupplyHold osztályt; nem kell más modulokat érintenünk. Így ha a logisztikai tiszt változtat valamit a szakaszán, az állomás többi része érintetlen marad. Ebben az esetben a SpaceStation osztály nem is lesz tudatában a változásoknak.

Az űrállomáson dolgozó tisztjeink valószínűleg örülnek a változásoknak, mert kérhetik a szükségeseket. Figyelje meg, hogy a kód olyan metódusokkal rendelkezik, mint a report_supplies és a report_fuel, amelyek a SupplyHold és FuelTank osztályokban találhatók. Mi történne, ha a Föld azt kérné, hogy változtassák meg a jelentések módját? Mindkét osztályt, a SupplyHoldot és a FuelTank-t meg kell változtatni. Mi a teendő, ha meg kell változtatnia az üzemanyag és a fogyóeszközök szállításának módját? Valószínűleg újra meg kell változtatnia ugyanazt az osztályt. Ez pedig már sérti az SRP-elvet. Javítsuk ki.

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.

A program legújabb verziójában a felelősségi köröket két új osztályra osztották, a FuelReporterre és a SupplyReporterre. Mindketten a Riporter osztály gyermekei. Ezenkívül példányváltozókat adtunk a SpaceStation osztályhoz, hogy szükség esetén inicializálható legyen a kívánt alosztály. Ha a Föld úgy dönt, hogy valami máson változtat, akkor az alosztályokon fogunk változtatni, nem a főosztályon.

Természetesen néhány osztályunk továbbra is függ egymástól. Így a SupplyReporter objektum a SupplyHoldtól, a FuelReporter pedig a FuelTanktól függ. Természetesen a nyomásfokozókat az üzemanyagtartályhoz kell kötni. De itt már minden logikusnak tűnik, és a változtatások végrehajtása nem lesz különösebben nehéz - az egyik objektum kódjának szerkesztése nem befolyásolja jelentősen a másikat.

Így létrehoztunk egy moduláris kódot, ahol az egyes objektumok/osztályok felelőssége pontosan meg van határozva. Az ilyen kóddal dolgozni nem probléma, karbantartása egyszerű feladat lesz. A teljes „isteni tárgyat” SRP-vé alakítottuk.

A Skillbox a következőket ajánlja:

Forrás: will.com

Hozzászólás