
Od prekladateľa: publikovali sme pre vás 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 .
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.
- (princíp otvorenosti/zatvorenosti). Triedy a iné elementy by mali byť otvorené na rozširovanie, ale zatvorené na modifikáciu.
- (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.
- (princíp oddelenia rozhrania). Softvérové entity by nemali závisieť od metód, ktoré nepoužívajú.
- (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
endNaš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
endZmien 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:
- Dvojročný praktický kurz .
- Online kurz .
- Praktický ročný kurz .
Zdroj: habr.com
