Píšeme flexibilní kód podle SOLID

Píšeme flexibilní kód podle SOLID

Od překladatele: publikovali jsme pro vás článek Severina Pereze o využití principů SOLID v programování. Informace z článku budou užitečné jak pro začátečníky, tak pro zkušené programátory.

Pokud se zabýváte vývojem, pravděpodobně jste už slyšeli o principech SOLID. Tyto principy umožňují programátorům psát čistý, dobře strukturovaný a snadno udržovatelný kód. Je třeba poznamenat, že v programování existuje několik přístupů, jak správně vykonávat určité úkoly. Různí odborníci mají různé názory a chápání „správné cesty“, vše závisí na zkušenostech jednotlivce. Nicméně, myšlenky, které SOLID představuje, jsou akceptovány téměř všemi členy IT komunity. Staly se výchozím bodem pro vznik a rozvoj mnoha dobrých metod správy vývoje.

Pojďme se podívat na to, co jsou to principy SOLID a jak nám pomáhají.

Skillbox doporučuje: Praktický kurz "Mobilní vývojář PRO".

Připomínáme: pro všechny čtenáře "Habra" – sleva 10 000 korun při registraci na jakýkoli kurz Skillbox s promokódem "Habr".

Co je SOLID?

Tento termín je zkratka, každé písmeno představuje počáteční písmeno určitého principu:

Princip jediné odpovědnosti

 
Princip zásady jediné odpovědnosti (SRP) říká, že každá třída nebo modul v programu by měl být odpovědný pouze za jednu část funkčnosti tohoto programu. Dále by prvky této odpovědnosti měly být přiděleny své třídě, a ne rozděleny mezi nesouvisející třídy. Vývojář a hlavní evangelista SRP, Robert C. Martin, popisuje odpovědnost jako důvod změn. Tento termín poprvé navrhl jako jeden z prvků své práce „Principy objektově orientovaného návrhu“. V koncepci bylo zahrnuto mnoho pravidelnosti související s kohezí, která byla dříve definována Tomem Demarcom.

Do koncepce byly také zahrnuty některé pojmy formulované Davidem Parnasem. Dvě hlavní — enkapsulace a skrytí informací. Parnas tvrdil, že rozdělení systému na jednotlivé moduly by nemělo vycházet z analýzy blokových schémat nebo toků provedení. Každý modul by měl obsahovat konkrétní řešení, které poskytuje minimální informace klientům.

Mimochodem, Martin uváděl zajímavý příklad s vrcholovými manažery společnosti (COO, CTO, CFO), z nichž každý používá specifický software pro obchod s různým cílem. Konečně každý z nich může provádět změny v softwaru, aniž by se to dotklo zájmů ostatních manažerů.

Božský objekt

Jak obvykle, nejlepší způsob, jak se naučit SRP, je vidět to všechno v akci. Podívejme se na úsek programu, který NEodpovídá principu jediné odpovědnosti. To je Ruby kód, který popisuje chování a atributy vesmírné stanice.

Prozkoumejte příklad a zkuste určit následující:
Povinnosti těch objektů, které jsou prohlášeny ve třídě SpaceStation.
Ty, kdo mohou mít zájem o práci vesmírné 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še kosmická stanice vlastně nefunguje (myslím, že od NASA v příští době žádný telefonát nedostanu), ale je tu co analyzovat.

Třída SpaceStation tedy má několik různých odpovědností (nebo úkolů). Všechny je lze rozdělit podle typů:

  • senzory;
  • supply (spotřební materiál);
  • palivo;
  • thrusters.

I když nikdo ze zaměstnanců stanice není definován v třídě, můžeme si snadno představit, kdo za co odpovídá. Pravděpodobně vědecký pracovník kontroluje senzory, logistika zajišťuje dodávky zdrojů, inženýr řídí zásoby paliva a pilot řídí thrusters.

Můžeme říci, že tento program neodpovídá SRP? Ano, samozřejmě. Třída SpaceStation je typický "božský objekt", který ví všechno a dělá vše. Je to zásadní antišablona v objektově orientovaném programování. Pro nováčka jsou takové objekty nesmírně obtížné na údržbu. Současná verze programu je velmi jednoduchá, ano, ale představte si, co se stane, pokud přidáme nové funkce. Možná naše kosmická stanice bude potřebovat zdravotní ústav nebo konferenční místnost. A čím více funkcí přidáme, tím více poroste složitost SpaceStation. A protože tento objekt bude propojen s jinými, údržba celého komplexu se stane ještě složitější. Nakonec může přestat fungovat například urychlovač. Pokud vědecký pracovník požádá o změny v práci se senzory, může to ovlivnit komunikační systémy stanice.

Porušení principu SRP může přinést krátkodobé taktické vítězství, ale nakonec "prohrajeme válku", údržba takového monstra bude v budoucnu velmi obtížná. Nejlepší je rozdělit program na jednotlivé části kódu, z nichž každá odpovídá za provedení určité operace. Chápající to, pojďme změnit třídu SpaceStation.

Rozdělme odpovědnost

Výše jsme definovali čtyři typy operací, které jsou řízeny třídou SpaceStation. Při refaktoringu je budeme mít na paměti. Aktualizovaný kód lépe odpovídá SRP.

třída SpaceStation
  attr_reader :senzory, @supply_hold, @fuel_tank, @thrusters
 
  def initialize
    @supply_hold = SupplyHold.new
    @senzory = Sensors.new
    @fuel_tank = FuelTank.new
    @thrusters = Thrusters.new(@fuel_tank)
  end
end
 
class Sensors
  def run_sensors
    puts "----- Akce senzoru -----"
    puts "Spouštění senzorů!"
  end
end
 
class SupplyHold
  attr_accessor :supplies
 
  def initialize
    @supplies = {}
  end
 
  def load_supplies(type, quantity)
    puts "----- Akce zásob -----"
    puts "Nahrávání #{quantity} jednotek #{type} do zásobovacího prostoru."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Akce zásob -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Používání #{quantity} z #{type} ze zásobovacího prostoru."
      @supplies[type] -= quantity
    else
      puts "Chyba zásob: Nedostatek #{type} v zásobovacím prostoru."
    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} jednotek"
      end
    else
      puts "Zásobovací prostor je prázdný."
    end
  end
end
 
class FuelTank
  attr_accessor :fuel
 
  def initialize
    @fuel = 0
  end
 
  def get_fuel_levels
    @fuel
  end
 
  def load_fuel(quantity)
    puts "----- Akce paliva -----"
    puts "Nahrávání #{quantity} jednotek paliva do nádrže."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Akce paliva -----"
    puts "Používání #{quantity} jednotek paliva z nádrže."
    @fuel -= quantity
  end
 
  def report_fuel
    puts "----- Zpráva o palivu -----"
    puts "#{@fuel} jednotek paliva dostupné."
  end
end
 
class Thrusters
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Akce trysky -----"
    if @linked_fuel_tank.get_fuel_levels >= 10
      puts "Akce posunu úspěšná."
      @linked_fuel_tank.use_fuel(10)
    else
      puts "Chyba trysky: Nedostatek dostupného paliva."
    end
  end
end

Změn je hodně, program teď vypadá rozhodně lépe. Nyní se naše třída SpaceStation stala spíše kontejnerem, ve kterém se iniciují operace pro závislé části, včetně sady senzorů, systému dodávky materiálu, palivové nádrže a tryskového systému.

Pro každou z proměnných nyní existuje odpovídající třída: Sensors; SupplyHold; FuelTank; Thrusters.

V této verzi kódu je několik důležitých změn. Vydělené funkce nejsou pouze inkapsulovány do vlastních tříd, ale jsou uspořádány tak, aby byly předvídatelné a konzistentní. Skupinujeme prvky se podobnou funkcionalitou podle principu soudržnosti. Nyní, pokud budeme potřebovat změnit způsob fungování systému, přechodem z hash struktury na pole, jednoduše použijeme třídu SupplyHold, aniž bychom se dotkli dalších modulů. Tím pádem, pokud změní něco v sekci odpovědný důstojník za logistiku, zbylé prvky stanice zůstanou nedotčené. Přitom třída SpaceStation vůbec nebude mít povědomí o těchto změnách.

Naši důstojníci pracující na kosmické stanici jsou pravděpodobně rádi za změny, protože mohou požadovat to, co potřebují přímo oni. Všimněte si, že v kódu existují metody jako report_supplies a report_fuel, obsažené v třídách SupplyHold a FuelTank. Co se stane, pokud Země požádá o změnu způsobu generování zpráv? Bude nutné upravit obě třídy, SupplyHold a FuelTank. A co když bude potřeba změnit způsob dodávky paliva a spotřebního materiálu? Pravděpodobně bude nutné znovu upravit všechny tyto třídy. A to již porušuje princip SRP. Opravinou to.

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 "----- Akce senzoru -----"
    puts "Senzory běží!"
  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 "----- Akce dodávky -----"
    puts "Náloading #{quantity} jednotek #{type} do zásobovacího prostoru."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Akce dodávky -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Používám #{quantity} #{type} ze zásobovacího prostoru."
      @supplies[type] -= quantity
    else
      puts "Chyba dodávky: Nedostatek #{type} v zásobovacím prostoru."
    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 "----- Akce paliva -----"
    puts "Náloading #{quantity} jednotek paliva do nádrže."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Akce paliva -----"
    puts "Používám #{quantity} jednotek 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 "----- Akce trysky -----"
    
    if @linked_fuel_tank.get_fuel_levels >= FUEL_PER_THRUST
      puts "Akce tahu byla úspěšná."
      @linked_fuel_tank.use_fuel(FUEL_PER_THRUST)
    else
      puts "Chyba trysky: Nedostatek paliva."
    end
  end
end
 
class Reporter
  def initialize(item, type)
    @linked_item = item
    @type = type
  end
 
  def report
    puts "----- #{@type.capitalize} Zpráva -----"
  end
end
 
class FuelReporter < Reporter
  def initialize(item)
    super(item, "palivo")
  end
 
  def report
    super
    puts "#{@linked_item.get_fuel_levels} jednotek paliva dostupných."
  end
end
 
class SupplyReporter  0
      @linked_item.get_supplies.each do |type, quantity|
        puts "#{type} dostupné: #{quantity} jednotek"
      end
    else
      puts "Zásobovací prostor je prázdný."
    end
  end
end
 
iss = SpaceStation.new
 
iss.sensors.run_sensors
  # ----- Akce senzoru -----
  # Senzory běží!
 
iss.supply_hold.use_supplies("díly", 2)
  # ----- Akce dodávky -----
  # Chyba dodávky: Nedostatek dílů v zásobovacím prostoru.
iss.supply_hold.load_supplies("díly", 10)
  # ----- Akce dodávky -----
  # Náloading 10 jednotek dílů do zásobovacího prostoru.
iss.supply_hold.use_supplies("díly", 2)
  # ----- Akce dodávky -----
  # Používám 2 díly ze zásobovacího prostoru.
iss.supply_reporter.report
  # ----- Zpráva o dodávce -----
  # díly dostupné: 8 jednotek
 
iss.thrusters.activate_thrusters
  # ----- Akce trysky -----
  # Chyba trysky: Nedostatek paliva.
iss.fuel_tank.load_fuel(100)
  # ----- Akce paliva -----
  # Náloading 100 jednotek paliva do nádrže.
iss.thrusters.activate_thrusters
  # ----- Akce trysky -----
  # Akce tahu byla úspěšná.
  # ----- Akce paliva -----
  # Používám 10 jednotek paliva z nádrže.
iss.fuel_reporter.report
  # ----- Zpráva o palivu -----
# 90 jednotek paliva dostupných.

V této poslední verzi programu byly povinnosti rozděleny do dvou nových tříd, FuelReporter a SupplyReporter. Obě jsou podtřídami třídy Reporter. Rovněž jsme přidali instanční proměnné do třídy SpaceStation, abychom v případě potřeby mohli inicializovat potřebnou podtřídu. Nyní, pokud se Země rozhodne něco změnit, provedeme úpravy v podtřídách, nikoli v hlavní třídě.

Samozřejmě, některé třídy na sobě stále závisí. Například objekt SupplyReporter závisí na SupplyHold a FuelReporter závisí na FuelTank. Pochopitelně musí být urychlovače spojeny s palivovou nádrží. Ale nyní to již vypadá logicky a provádění změn nebude obzvlášť složité — úprava kódu jednoho objektu příliš neovlivní druhý.

Tímto způsobem jsme vytvořili modulární kód, kde jsou povinnosti každého z objektů/tříd přesně definovány. Práce s takovým kódem není problém, jeho údržba bude jednoduchým úkolem. Celý „božský objekt“ jsme transformovali na SRP.

Skillbox doporučuje:

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster