Píšeme flexibilný kód s použitím SOLID

Píšeme flexibilný kód s použitím SOLID

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

Ak sa zaoberáte vývojom, určite ste už počuli o princípoch SOLID. Tieto princípy umožňujú programátorom písať čistý, dobre štruktúrovaný a ľahko udržiavateľný kód. Je dôležité poznamenať, že v programovaní existuje niekoľko prístupov k tomu, ako správne vykonávať rôzne úlohy. Rôzni odborníci majú rôzne názory a chápania „správnej cesty“, závisí to od skúseností každého jednotlivca. Napriek tomu sú myšlienky, ktoré sú zakotvené v SOLID, akceptované takmer všetkými členmi IT komunity. Stali sa východiskovým bodom pre vznik a rozvoj mnohých dobrých metód riadenia vývoja.

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

Skillbox odporúča: Praktický kurz „Mobilný vývojár PRO“.

Pripomíname: pre všetkých čitateľov „Habr“ — zľava 10 000 rubľov pri zapísaní na akýkoľvek kurz Skillbox s promo kódom „Habr“.

Čo je SOLID?

Tento termín je skratka, každé písmeno termínu predstavuje začiatok názvu konkrétneho princípu:

  • SSingle Responsibility Principle (princíp jedinečnej zodpovednosti). Modul môže mať len jeden a iba jeden dôvod na zmenu.
  • The OOpen/Closed Principle (princíp otvorenosti/zatvorenosti). Triedy a iné elementy by mali byť otvorené na rozširovanie, ale zatvorené na modifikáciu.
  •  The LLiskov Substitution Principle (princíp substitúcie Liskov. Funkcie, ktoré používajú základný typ, by mali mať možnosť používať podtypy základného typu, aniž by o tom vedeli.
  • The IInterface Segregation Principle (princíp oddelenia rozhrania). Softvérové entity by nemali závisieť od metód, ktoré nepoužívajú.
  • The DDependency Inversion Principle (princíp inverznej závislosti). Moduly vyšších úrovní by nemali závisieť od modulov nižších úrovní.

Princíp jedinečnej zodpovednosti

 
Princíp jednotnej zodpovednosti (SRP) hovorí, že každá trieda alebo modul v programe by mala byť zodpovedná iba za jednu časť funkčnosti tohto programu. Okrem toho by mali byť prvky tejto zodpovednosti priradené svojej triede a nie rozdelené medzi nesúvisiace triedy. Vývojár a hlavný evangelista SRP, Robert C. Martin, popisuje zodpovednosť ako príčinu zmien. Pôvodne tento termín predstavil ako jeden z prvkov svojej práce „Princípy objektovo orientovaného navrhovania“. Do konceptu sa dostalo aj veľa zo zákonitosti súvislosti, ktorú predtým definoval Tom DeMarco.

Do konceptu sa zahrnuli aj niektoré pojmy, ktoré formuloval David Parnas. Dve hlavné — enkapsulácia a skrývanie informácií. Parnas tvrdil, že rozdelenie systému na jednotlivé moduly by nemalo byť založené na analýze blokových schém alebo tokov vykonávania. Každý z modulov by mal obsahovať konkrétne riešenie, ktoré poskytuje minimálne informácie klientom.

Mimochodom, Martin uviedol zaujímavý príklad s vrcholovými manažérmi firmy (COO, CTO, CFO), z ktorých každý používa špecifický softvér pre podnikanie s rôznym cieľom. V dôsledku toho môže ktorýkoľvek z nich uplatniť zmeny v softvéri bez toho, aby to ovplyvnilo záujmy ostatných manažérov.

Božský objekt

Ako obvykle, najlepší spôsob, ako sa naučiť SRP, je vidieť všetko v akcii. Pozrime sa na úsek programu, ktorý NEZODPOVEDÁ princípu 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ť nasledujúce:
Povinnosti tých objektov, ktoré sú vyhlásené v triede SpaceStation.
Tých, ktorí by mohli mať záujem o prácu 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} 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

Naša vesmírna stanica nie je funkčná (myslím, že v blízkej budúcnosti nedostanem telefonát od NASA), ale je tu čo analyzovať.

Takže, trieda SpaceStation má niekoľko rôznych zodpovedností (alebo úloh). Všetky môžu byť rozdelené podľa typov:

  • senzory;
  • zásobovanie (spotrebný materiál);
  • palivo;
  • vyťahovače.

Aj keď nikto z zamestnancov stanice nie je definovaný v triede, bez problémov si môžeme predstaviť, kto za čo zodpovedá. Pravdepodobne vedec kontroluje senzory, logistika zodpovedá za dodávku zdrojov, inžinier spravuje zásoby paliva a pilot ovláda vyťahovače.

Môžeme povedať, že tento program nespĺňa SRP? Áno, určite. Ale trieda SpaceStation je typický "božský objekt", ktorý vie o všetkom a robí všetko. Je to hlavný antišablón v objektovo orientovanom programovaní. Pre nováčikov sú takéto objekty mimoriadne zložité 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šej vesmírnej stanici potrebný zdravotný úsek alebo rokovacia miestnosť. A čím viac bude funkcií, tým viac sa SpaceStation rozrastie. A pretože tento objekt bude prepojený s inými, údržba celého komplexu sa stane ešte zložitejšou. Nakoniec môžeme narušiť fungovanie, napríklad urýchľovačov. Ak vedec požiada o zmeny v práci so senzormi, môže to mať významný dopad na komunikačné systémy stanice.

Porušenie SRP-principu môže priniesť krátkodobé takticke víťazstvo, ale nakoniec "prehráme vojnu", údržba takého monštra v budúcnosti sa stane veľmi zložitou. Najlepšie je rozdeliť program na samostatné časti kódu, z ktorých každá zodpovedá za vykonanie určitej operácie. Pochopili sme to, poďme zmeniť triedu SpaceStation.

Rozdelíme zodpovednosť

Vyššie sme určili štyri typy operácií, ktoré kontroluje trieda SpaceStation. Pri refaktoringu ich budeme mať na pamäti. Aktualizovaný kód lepšie spĺňa 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 "----- Akcia senzora -----"
    puts "Spustenie senzorov!"
  end
end
 
class SupplyHold
  attr_accessor :supplies
 
  def initialize
    @supplies = {}
  end
 
  def load_supplies(type, quantity)
    puts "----- Akcia dodania -----"
    puts "Nahrávané #{quantity} jednotiek #{type} do zásobníka."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Akcia použitia -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Používam #{quantity} jednotiek #{type} zo zásobníka."
      @supplies[type] -= quantity
    else
      puts "Chyba zásob: Nedostatok #{type} v zásobníku."
    end
  end
 
  def report_supplies
    puts "----- Zpráva o zásobách -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} dostupné: #{quantity} jednotiek"
      end
    else
      puts "Zásobník je prázdny."
    end
  end
end
 
class FuelTank
  attr_accessor :fuel
 
  def initialize
    @fuel = 0
  end
 
  def get_fuel_levels
    @fuel
  end
 
  def load_fuel(quantity)
    puts "----- Akcia paliva -----"
    puts "Nahrávanie #{quantity} jednotiek paliva do nádrže."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Akcia paliva -----"
    puts "Používam #{quantity} jednotiek paliva z nádrže."
    @fuel -= quantity
  end
 
  def report_fuel
    puts "----- Zpráva o palive -----"
    puts "#{@fuel} jednotiek paliva dostupných."
  end
end
 
class Thrusters
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Akcia motorov -----"
    if @linked_fuel_tank.get_fuel_levels >= 10
      puts "Úspešný úspech."
      @linked_fuel_tank.use_fuel(10)
    else
      puts "Chyba motorov: Nedostatok paliva."
    end
  end
end

Zmien je veľa, program teraz vyzerá určite lepšie. Náš trieda SpaceStation sa teraz stala skôr kontajnerom, v ktorom sa spúšťajú operácie pre závislé časti, vrátane súboru senzorov, systému dodávky zásob, palivovej nádrže a motorov.

Pre každú z premenných teraz existuje zodpovedajúca trieda: Sensors; SupplyHold; FuelTank; Thrusters.

V tejto verzii kódu je niekoľko dôležitých zmien. Ide o to, že samostatné funkcie nie sú len enkapsulované v vlastných triedach, ale sú usporiadané tak, aby sa stali predvídateľnými a konzistentnými. Skupujeme podobné funkčné prvky, aby sme dodržali zásadu koherencie. Teraz, ak budeme musieť zmeniť princíp fungovania systému, prechádzajúc z hash-štruktúry na pole, jednoducho použijeme triedu SupplyHold, iné moduly sa dotknuté nebudú. Týmto spôsobom, ak dôstojník zodpovedný za logistiku niečo zmení vo svojej sekcii, ostatné prvky stanice zostanú nedotknuté. Trieda SpaceStation pritom ani nebude vedieť o týchto zmenách.

Naši dôstojníci pracujúci na vesmírnej stanici pravdepodobne uvítajú zmeny, pretože môžu žiadať o to, čo potrebujú. Všimnite si, že v kóde sú také metódy ako report_supplies a report_fuel, ktoré sa nachádzajú v triedach SupplyHold a FuelTank. Čo sa stane, ak Zem požiada o zmenu spôsobu generovania správ? Bude potrebné zmeniť obe triedy, SupplyHold a FuelTank. A čo ak bude treba zmeniť spôsob dodávky paliva a spotrebných materiálov? Pravdepodobne bude opäť potrebné zmeniť všetky tie isté triedy. A to už porušuje SRP princíp. Poďme to opraviť.

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 "----- Akcia senzora -----"
    puts "Spúšťam senzory!"
  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 "----- Akcia dodávky -----"
    puts "Načítavam #{quantity} jednotiek #{type} do úložiska."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Akcia dodávky -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Používam #{quantity} z #{type} z úložiska."
      @supplies[type] -= quantity
    else
      puts "Chyba dodávky: Nedostatok #{type} v úložisku."
    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 "----- Akcia paliva -----"
    puts "Načítavam #{quantity} jednotiek paliva do nádrže."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Akcia paliva -----"
    puts "Používam #{quantity} jednotiek paliva z nádrže."
    @fuel -= quantity
  end
end
 
class Thrusters
  FUEL_PER_THRUST = 10
 
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Akcia thrusteru -----"
    
    if @linked_fuel_tank.get_fuel_levels >= FUEL_PER_THRUST
      puts "Akcia potlačenia úspešná."
      @linked_fuel_tank.use_fuel(FUEL_PER_THRUST)
    else
      puts "Chyba thrusteru: Nedostatok paliva."
    end
  end
end
 
class Reporter
  def initialize(item, type)
    @linked_item = item
    @type = type
  end
 
  def report
    puts "----- #{@type.capitalize} Správa -----"
  end
end
 
class FuelReporter < Reporter
  def initialize(item)
    super(item, "palivo")
  end
 
  def report
    super
    puts "#{@linked_item.get_fuel_levels} jednotiek paliva dostupných."
  end
end
 
class SupplyReporter  0
      @linked_item.get_supplies.each do |type, quantity|
        puts "#{type} dostupné: #{quantity} jednotiek"
      end
    else
      puts "Úložisko je prázdne."
    end
  end
end
 
iss = SpaceStation.new
 
iss.sensors.run_sensors
  # ----- Akcia senzora -----
  # Spúšťam senzory!
 
iss.supply_hold.use_supplies("diely", 2)
  # ----- Akcia dodávky -----
  # Chyba dodávky: Nedostatok dielov v úložisku.
iss.supply_hold.load_supplies("diely", 10)
  # ----- Akcia dodávky -----
  # Načítavam 10 jednotiek dielov do úložiska.
iss.supply_hold.use_supplies("diely", 2)
  # ----- Akcia dodávky -----
  # Používam 2 z dielov z úložiska.
iss.supply_reporter.report
  # ----- Správa dodávky -----
  # diely dostupné: 8 jednotiek
 
iss.thrusters.activate_thrusters
  # ----- Akcia thrusteru -----
  # Chyba thrusteru: Nedostatok paliva.
iss.fuel_tank.load_fuel(100)
  # ----- Akcia paliva -----
  # Načítavam 100 jednotiek paliva do nádrže.
iss.thrusters.activate_thrusters
  # ----- Akcia thrusteru -----
  # Akcia potlačenia úspešná.
  # ----- Akcia paliva -----
  # Používam 10 jednotiek paliva z nádrže.
iss.fuel_reporter.report
  # ----- Správa paliva -----
# 90 jednotiek paliva dostupných.

V tejto poslednej verzii programu sú povinnosti rozdelené do dvoch nových tried, FuelReporter a SupplyReporter. Obe sú potomkami triedy Reporter. Okrem toho sme pridali inštančné premenné do triedy SpaceStation, aby sme mohli iniciovať potrebný podtried, keď to bude potrebné. Teraz, ak sa Zem rozhodne zmeniť niečo ďalšie, upravíme podtriedy, nie hlavnú triedu.

Samozrejme, niektoré triedy sú naďalej navzájom závislé. Takže objekt SupplyReporter závisí od SupplyHold a FuelReporter závisí od FuelTank. Samozrejme, urýchlovače musia byť prepojené s palivovou nádržou. Ale tu to už vyzerá logicky a uskutočnenie zmien nebude obzvlášť zložité — úprava kódu jedného objektu nebude mať výrazný vplyv na iný.

Týmto spôsobom sme vytvorili modulárny kód, kde sú povinnosti každého z objektov/ tried presne určené. Pracovať s takým kódom nie je problém, jeho údržba bude jednoduchou úlohou. Celý „božský objekt“ sme premenili na SRP.

Skillbox odporúča:

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster