Zápis flexibilného kódu pomocou SOLID

Zápis flexibilného kódu pomocou SOLID

Od prekladateľa: zverejnené pre vás článok Severina Pereza o používaní princípov SOLID v programovaní. Informácie z článku budú užitočné pre začiatočníkov aj skúsených programátorov.

Ak sa venujete vývoju, s najväčšou pravdepodobnosťou ste už počuli o princípoch SOLID. Umožňujú programátorovi písať čistý, dobre štruktúrovaný a ľahko udržiavateľný kód. Stojí za zmienku, že v programovaní existuje niekoľko prístupov, ako správne vykonávať konkrétnu prácu. Rôzni špecialisti majú rôzne predstavy a chápanie „správnej cesty“; všetko závisí od skúseností každého človeka. Myšlienky proklamované v SOLID však akceptujú takmer všetci predstavitelia IT komunity. Stali sa východiskovým bodom pre vznik a rozvoj mnohých dobrých praktík riadenia rozvoja.

Poďme pochopiť, čo sú princípy SOLID a ako nám pomáhajú.

Skillbox odporúča: Praktický kurz "Mobile Developer PRO".

Pripomíname vám: pre všetkých čitateľov „Habr“ - zľava 10 000 rubľov pri registrácii do akéhokoľvek kurzu Skillbox pomocou propagačného kódu „Habr“.

Čo je SOLID?

Tento výraz je skratkou, každé písmeno výrazu je začiatkom názvu konkrétneho princípu:

  • Sjednotný princíp zodpovednosti. Modul môže mať jeden a len jeden dôvod na zmenu.
  • Opero/uzavretý princíp (princíp otvorený/zatvorený). Triedy a ďalšie prvky by mali byť otvorené na rozšírenie, ale zatvorené pre úpravy.
  • Liskov substitučný princíp (Liskov substitučný princíp). Funkcie, ktoré používajú základný typ, by mali byť schopné používať podtypy základného typu bez toho, aby o tom vedeli.
  • IPrincíp segregácie rozhrania  (princíp oddelenia rozhrania). Softvérové ​​entity by nemali závisieť od metód, ktoré nepoužívajú.
  • DPrincíp inverzie závislosti (princíp inverzie závislosti). Moduly na vyšších úrovniach by nemali závisieť od modulov na nižších úrovniach.

Princíp jednotnej zodpovednosti


Princíp jednotnej zodpovednosti (SRP) uvádza, že každá trieda alebo modul v programe by mal byť zodpovedný iba za jednu časť funkčnosti tohto programu. Navyše, prvky tejto zodpovednosti by mali byť priradené ich vlastnej triede, a nie rozptýlené medzi nesúvisiace triedy. Vývojár a hlavný evanjelista SRP Robert S. Martin popisuje zodpovednosť ako dôvod zmeny. Pôvodne tento termín navrhol ako jeden z prvkov svojej práce „Principles of Object-Oriented Design“. Koncept zahŕňa veľkú časť vzoru pripojenia, ktorý predtým definoval Tom DeMarco.

Súčasťou konceptu bolo aj niekoľko konceptov, ktoré sformuloval David Parnas. Dve hlavné sú zapuzdrenie a skrytie informácií. Parnas tvrdil, že rozdelenie systému na samostatné moduly by nemalo byť založené na analýze blokových diagramov alebo tokov vykonávania. Ktorýkoľvek z modulov musí obsahovať špecifické riešenie, ktoré klientom poskytne minimum informácií.

Mimochodom, Martin uviedol zaujímavý príklad s vysokými manažérmi spoločnosti (COO, CTO, CFO), z ktorých každý používa špecifický biznis softvér na iné účely. Vďaka tomu môže ktorýkoľvek z nich implementovať zmeny v softvéri bez toho, aby to ovplyvnilo záujmy ostatných manažérov.

Božský predmet

Ako vždy, najlepší spôsob, ako sa naučiť SRP, je vidieť ho v praxi. Pozrime sa na časť programu, ktorá NEDOdržiava princíp jednotnej zodpovednosti. Toto je Ruby kód, ktorý popisuje správanie a atribúty vesmírnej stanice.

Pozrite si príklad a skúste určiť nasledovné:
Zodpovednosti tých objektov, ktoré sú deklarované v triede SpaceStation.
Tých, ktorých môže zaujímať prevádzka vesmírnej stanice.

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

V skutočnosti je naša vesmírna stanica nefunkčná (nemyslím si, že mi v blízkej dobe zavolajú z NASA), ale je tu čo analyzovať.

Trieda SpaceStation má teda niekoľko rôznych zodpovedností (alebo úloh). Všetky z nich možno rozdeliť do typov:

  • senzory;
  • zásoby (spotrebný materiál);
  • palivo;
  • urýchľovače.

Aj keď nikto zo zamestnancov stanice nemá pridelenú triedu, vieme si ľahko predstaviť, kto je za čo zodpovedný. S najväčšou pravdepodobnosťou vedec ovláda senzory, logistik je zodpovedný za zásobovanie zdrojmi, inžinier je zodpovedný za zásoby paliva a pilot ovláda posilňovače.

Môžeme povedať, že tento program nie je v súlade so SRP? Áno samozrejme. Trieda SpaceStation je ale typický „boží objekt“, ktorý všetko vie a všetko robí. Toto je hlavný anti-vzor v objektovo orientovanom programovaní. Pre začiatočníka sú takéto predmety mimoriadne náročné na údržbu. Zatiaľ je program veľmi jednoduchý, áno, ale predstavte si, čo sa stane, ak pridáme nové funkcie. Možno bude naša vesmírna stanica potrebovať lekársku stanicu alebo zasadaciu miestnosť. A čím viac funkcií bude, tým viac SpaceStation porastie. No a keďže toto zariadenie bude prepojené s ostatnými, bude obsluha celého komplexu ešte zložitejšia. V dôsledku toho môžeme narušiť činnosť napríklad urýchľovačov. Ak výskumník požaduje zmeny v senzoroch, môže to veľmi dobre ovplyvniť komunikačné systémy stanice.

Porušenie princípu SRP môže priniesť krátkodobé taktické víťazstvo, ale nakoniec „prehráme vojnu“ a bude veľmi ťažké udržať takéto monštrum v budúcnosti. Najlepšie je rozdeliť program na samostatné časti kódu, z ktorých každá je zodpovedná za vykonanie konkrétnej operácie. Keď to pochopíme, zmeňme triedu SpaceStation.

Rozdeľme zodpovednosť

Vyššie sme definovali štyri typy operácií, ktoré sú riadené triedou SpaceStation. Pri refaktorizácii ich budeme mať na pamäti. Aktualizovaný kód lepšie zodpovedá 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

Zmien je veľa, program teraz vyzerá určite lepšie. Teraz sa naša trieda SpaceStation stala viac kontajnerom, v ktorom sa spúšťajú operácie pre závislé časti vrátane sady senzorov, systému zásobovania spotrebným materiálom, palivovej nádrže a zosilňovačov.

Pre ktorúkoľvek z premenných teraz existuje zodpovedajúca trieda: Senzory; SupplyHold; Palivová nádrž; Trysky.

V tejto verzii kódu je niekoľko dôležitých zmien. Ide o to, že jednotlivé funkcie nie sú len zapuzdrené vo svojich vlastných triedach, ale sú organizované tak, aby sa stali predvídateľnými a konzistentnými. Prvky s podobnou funkcionalitou zoskupujeme, aby sme dodržiavali princíp koherencie. Ak teraz potrebujeme zmeniť spôsob, akým systém funguje, prejsť z hašovacej štruktúry na pole, stačí použiť triedu SupplyHold; nemusíme sa dotýkať iných modulov. Týmto spôsobom, ak dôstojník logistiky niečo zmení na svojom úseku, zvyšok stanice zostane nedotknutý. V tomto prípade si trieda SpaceStation zmeny ani neuvedomí.

Naši dôstojníci pracujúci na vesmírnej stanici sú pravdepodobne spokojní so zmenami, pretože môžu požiadať o tie, ktoré potrebujú. Všimnite si, že kód obsahuje metódy ako report_supplies a report_fuel obsiahnuté v triedach SupplyHold a FuelTank. Čo by sa stalo, keby Zem požiadala o zmenu spôsobu hlásenia? Obe triedy, SupplyHold a FuelTank, bude potrebné zmeniť. Čo ak potrebujete zmeniť spôsob dodávky paliva a spotrebného materiálu? Pravdepodobne budete musieť znova zmeniť všetky rovnaké triedy. A to už je porušenie zásady SRP. Poďme to napraviť.

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.

V tejto najnovšej verzii programu boli zodpovednosti rozdelené do dvoch nových tried, FuelReporter a SupplyReporter. Obaja sú deťmi z triedy Reportér. Okrem toho sme do triedy SpaceStation pridali premenné inštancie, aby bolo možné v prípade potreby inicializovať požadovanú podtriedu. Teraz, ak sa Zem rozhodne zmeniť niečo iné, urobíme zmeny v podtriedach a nie v hlavnej triede.

Samozrejme, niektoré z našich tried sú na sebe stále závislé. Objekt SupplyReporter teda závisí od SupplyHold a FuelReporter závisí od FuelTank. Samozrejme, posilňovače musia byť pripojené k palivovej nádrži. Ale tu už všetko vyzerá logicky a vykonávanie zmien nebude obzvlášť ťažké - úprava kódu jedného objektu výrazne neovplyvní druhý.

Takto sme vytvorili modulárny kód, kde sú presne definované zodpovednosti každého z objektov/tried. Práca s takýmto kódom nie je problém, jeho údržba bude jednoduchá úloha. Premenili sme celý „božský objekt“ na SRP.

Skillbox odporúča:

Zdroj: hab.com

Pridať komentár