
Od tłumacza: opublikowali dla was o zastosowaniu zasad SOLID w programowaniu. Informacje zawarte w artykule będą przydatne zarówno dla nowicjuszy, jak i dla doświadczonych programistów.
Jeśli zajmujesz się programowaniem, to prawdopodobnie słyszałeś o zasadach SOLID. Umożliwiają one programiście pisanie czystego, dobrze zorganizowanego i łatwego w utrzymaniu kodu. Warto zauważyć, że w programowaniu istnieje kilka podejść do wykonania danej pracy. Różni specjaliści mają różne pomysły i rozumienia «właściwej drogi», wszystko zależy od doświadczenia. Niemniej jednak, idee głoszone w SOLID są akceptowane przez prawie wszystkich przedstawicieli społeczności IT. Stały się one punktem wyjścia dla powstania i rozwoju wielu dobrych metod zarządzania rozwojem.
Rozważmy, czym są zasady SOLID i jak mogą nam pomóc.
Skillbox poleca: Praktyczny kurs .
Przypominamy: dla wszystkich czytelników „Habra” — zniżka 10 000 rubli przy zapisie na dowolny kurs Skillbox z kodem promocyjnym „Habra”.
Czym jest SOLID?
Termin ten jest akronimem, każda litera oznacza początek nazwy określonej zasady:
- SSingle Responsibility Principle (zasada pojedynczej odpowiedzialności). Moduł powinien mieć tylko jeden powód do zmiany.
- (zasada otwartości/zamknięcia). Klasy i inne elementy powinny być otwarte na rozbudowę, ale zamknięte na modyfikacje.
- (zasada podstawienia Liskov). Funkcje, które używają typu bazowego, powinny mieć możliwość używania podtypów typu bazowego, nie wiedząc o tym.
- (zasada segregacji interfejsu). Jednostki programowe nie powinny zależeć od metod, które nie są przez nie wykorzystywane.
- (zasada inwersji zależności). Moduły wyższego poziomu nie powinny zależeć od modułów niższego poziomu.
Zasada pojedynczej odpowiedzialności
Zasada pojedynczej odpowiedzialności (SRP) mówi, że każda klasa lub moduł w programie powinny odpowiadać tylko za jedną część funkcjonalności tego programu. Ponadto elementy tej odpowiedzialności powinny być przypisane do swojej klasy, a nie rozproszone po niezwiązanych klasach. Twórca i główny ewangelista SRP, Robert C. Martin, opisuje odpowiedzialność jako przyczynę zmian. Początkowo zaproponował ten termin jako jeden z elementów swojej pracy „Zasady projektowania obiektowego”. W koncepcję weszło wiele z zasadności spójności, która została wcześniej określona przez Toma Demarco.
Do koncepcji weszło również kilka pojęć sformułowanych przez Davida Parnasa. Dwa główne to enkapsulacja i ukrywanie informacji. Parnas twierdził, że podział systemu na odrębne moduły nie powinien opierać się na analizie diagramów blokowych ani przepływów wykonania. Każdy z modułów powinien zawierać konkretne rozwiązanie, które udostępnia minimalną ilość informacji klientom.
Zresztą, Martin przytoczył ciekawy przykład dotyczący wyższych menedżerów firmy (COO, CTO, CFO), z których każdy stosuje specyficzne oprogramowanie do biznesu w różnym celu. W rezultacie każdy z nich może wprowadzać zmiany w oprogramowaniu, nie naruszając interesów innych menedżerów.
Bosk obiekt
Jak zwykle, najlepszym sposobem na naukę SRP jest zobaczenie tego w akcji. Przyjrzyjmy się fragmentowi programu, który NIE spełnia zasady pojedynczej odpowiedzialności. To jest kod Ruby opisujący zachowanie i atrybuty stacji kosmicznej.
Zapoznaj się z przykładem i spróbuj określić następujące:
Obowiązki tych obiektów, które są zadeklarowane w klasie SpaceStation.
Osoby, które mogą być zainteresowane pracą stacji kosmicznej.
class SpaceStation
def initialize
@supplies = {}
@fuel = 0
end
def run_sensors
puts "----- Akcja Sensora -----"
puts "Uruchamianie sensorów!"
end
def load_supplies(type, quantity)
puts "----- Akcja Zapasów -----"
puts "Załadunek #{quantity} jednostek #{type} do magazynu."
if @supplies[type]
@supplies[type] += quantity
else
@supplies[type] = quantity
end
end
def use_supplies(type, quantity)
puts "----- Akcja Zapasów -----"
if @supplies[type] != nil && @supplies[type] > quantity
puts "Używanie #{quantity} z #{type} z magazynu."
@supplies[type] -= quantity
else
puts "Błąd Zasobów: Niedobór #{type} w magazynie."
end
end
def report_supplies
puts "----- Raport Zapasów -----"
if @supplies.keys.length > 0
@supplies.each do |type, quantity|
puts "#{type} dostępne: #{quantity} jednostek"
end
else
puts "Magazyn jest pusty."
end
end
def load_fuel(quantity)
puts "----- Akcja Paliwa -----"
puts "Załadunek #{quantity} jednostek paliwa do zbiornika."
@fuel += quantity
end
def report_fuel
puts "----- Raport Paliwa -----"
puts "#{@fuel} jednostek paliwa dostępnych."
end
def activate_thrusters
puts "----- Akcja Napędu -----"
if @fuel >= 10
puts "Akcja napędu udana."
@fuel -= 10
else
puts "Błąd Napędu: Niedobór paliwa."
end
end
endNasza stacja kosmiczna właściwie nie działa (nie spodziewam się telefonu od NASA w najbliższej przyszłości), ale jest tutaj wiele rzeczy do przeanalizowania.
Klasa SpaceStation ma kilka różnych odpowiedzialności (lub zadań). Można je podzielić na typy:
- sensory;
- zapasy (materiały eksploatacyjne);
- paliwo;
- napędy.
Mimo że nikt z personelu stacji nie jest przypisany do klasy, możemy łatwo wyobrazić sobie, kto za co odpowiada. Najprawdopodobniej naukowiec kontroluje sensory, logista odpowiada za zaopatrzenie w zasoby, inżynier zajmuje się zapasami paliwa, a pilot zarządza napędami.
Czy możemy powiedzieć, że ten program nie spełnia zasady SRP? Tak, oczywiście. Ale klasa SpaceStation jest typowym „obiektem boskim”, który wie wszystko i robi wszystko. To główny antywzorzec programowania obiektowego. Dla nowicjusza takie obiekty są niezwykle trudne w utrzymaniu. Jak na razie program jest bardzo prosty, tak, ale wyobraźmy sobie, co się stanie, gdy dodamy nowe funkcje. Być może nasza stacja kosmiczna potrzebuje punktu medycznego lub sali konferencyjnej. A im więcej funkcji, tym bardziej rozrośnie się SpaceStation. Ponieważ ten obiekt będzie powiązany z innymi, utrzymanie całego kompleksu stanie się jeszcze bardziej skomplikowane. W efekcie możemy zakłócić działanie, na przykład, akceleratorów. Jeśli pracownik naukowy zażąda zmian w pracy z sensorami, może to mieć wpływ na systemy komunikacyjne stacji.
Naruszenie zasady SRP może przynieść krótkoterminową taktyczną wygraną, ale w końcu „przegramy wojnę”, utrzymanie takiego monstrum w przyszłości stanie się bardzo trudne. Najlepiej jest podzielić program na oddzielne fragmenty kodu, z których każdy odpowiada za wykonanie konkretnej operacji. Rozumiejąc to, zmieńmy klasę SpaceStation.
Podzielmy odpowiedzialność
Powyżej zdefiniowaliśmy cztery rodzaje operacji, które są kontrolowane przez klasę SpaceStation. Podczas refaktoryzacji będziemy je mieć na uwadze. Zaktualizowany kod lepiej odpowiada zasadzie SRP.
klasa StacjaKosmiczna
attr_reader :czujniki, :magazyn, :zbiornik_paliwa, :silniki
def initialize
@magazyn = Magazyn.new
@czujniki = Czujniki.new
@zbiornik_paliwa = ZbiornikPaliwa.new
@silniki = Silniki.new(@zbiornik_paliwa)
end
end
klasa Czujniki
def uruchom_czujniki
puts "----- Akcja Czujnika -----"
puts "Uruchamianie czujników!"
end
end
klasa Magazyn
attr_accessor :materiały
def initialize
@materiały = {}
end
def załaduj_materiały(typ, ilość)
puts "----- Akcja Załadunku -----"
puts "Ładowanie #{ilość} jednostek #{typ} do magazynu."
if @materiały[typ]
@materiały[typ] += ilość
else
@materiały[typ] = ilość
end
end
def użyj_materiałów(typ, ilość)
puts "----- Akcja Użycia -----"
if @materiały[typ] != nil && @materiały[typ] > ilość
puts "Używanie #{ilość} z #{typ} z magazynu."
@materiały[typ] -= ilość
else
puts "Błąd Zaopatrzenia: Niedobór #{typ} w magazynie."
end
end
def raport_zasobów
puts "----- Raport Zasobów -----"
if @materiały.keys.length > 0
@materiały.each do |typ, ilość|
puts "#{typ} dostępne: #{ilość} jednostek"
end
else
puts "Magazyn jest pusty."
end
end
end
klasa ZbiornikPaliwa
attr_accessor :paliwo
def initialize
@paliwo = 0
end
def pobierz_poziomy_paliwa
@paliwo
end
def załaduj_paliwo(ilość)
puts "----- Akcja Paliwa -----"
puts "Ładowanie #{ilość} jednostek paliwa do zbiornika."
@paliwo += ilość
end
def użyj_paliwa(ilość)
puts "----- Akcja Paliwa -----"
puts "Używanie #{ilość} jednostek paliwa ze zbiornika."
@paliwo -= ilość
end
def raport_paliwa
puts "----- Raport Paliwa -----"
puts "#{@paliwo} jednostek paliwa dostępnych."
end
end
klasa Silniki
def initialize(zbiornik_paliwa)
@połączony_zbiornik = zbiornik_paliwa
end
def aktywuj_silniki
puts "----- Akcja Silnika -----"
if @połączony_zbiornik.pobierz_poziomy_paliwa >= 10
puts "Akcja napędu zakończona sukcesem."
@połączony_zbiornik.użyj_paliwa(10)
else
puts "Błąd Silnika: Niedobór paliwa dostępny."
end
end
endWprowadzone zmiany sprawiły, że program wygląda zdecydowanie lepiej. Nasza klasa StacjaKosmiczna stała się bardziej kontenerem, w którym inicjujemy operacje dla zależnych części, w tym zestaw czujników, system dostarczania zasobów, zbiornik paliwa i silniki.
Dla każdej z zmiennych teraz istnieje odpowiednia klasa: Czujniki; Magazyn; ZbiornikPaliwa; Silniki.
W tej wersji kodu wprowadzono kilka istotnych zmian. Otóż, poszczególne funkcje nie tylko są zamknięte w swoich klasach, ale również zorganizowane w sposób, który czyni je przewidywalnymi i spójnymi. Grupujemy podobne pod względem funkcjonalności elementy, aby przestrzegać zasady spójności. Teraz, gdy będziemy musieli zmienić sposób działania systemu, przechodząc z struktury haszowej na tablicę, wystarczy skorzystać z klasy SupplyHold, nie wpływając na inne moduły. Dzięki temu, gdy oficer odpowiedzialny za logistykę wprowadzi zmiany w swojej sekcji, pozostałe elementy stacji pozostaną nienaruszone. Klasa SpaceStation nawet nie będzie świadoma tych zmian.
Nasi oficerowie pracujący na stacji kosmicznej prawdopodobnie cieszą się z tych zmian, ponieważ mogą żądać tych, które są im naprawdę potrzebne. Zauważ, że w kodzie znajdują się takie metody jak report_supplies i report_fuel, zawarte w klasach SupplyHold i FuelTank. Co się stanie, jeśli Ziemia poprosi o zmianę sposobu generowania raportów? Będzie konieczne zmodyfikowanie obu klas, SupplyHold i FuelTank. A co, jeśli zajdzie potrzeba zmiany sposobu dostarczania paliwa i materiałów eksploatacyjnych? Najprawdopodobniej znowu trzeba będzie zmienić te same klasy. A to już narusza zasadę SRP. Naprawmy 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 "----- Akcja Sensora -----"
puts "Uruchamianie sensorów!"
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 "----- Akcja Zaopatrzenia -----"
puts "Ładowanie #{quantity} jednostek #{type} do magazynu."
if @supplies[type]
@supplies[type] += quantity
else
@supplies[type] = quantity
end
end
def use_supplies(type, quantity)
puts "----- Akcja Zaopatrzenia -----"
if @supplies[type] != nil && @supplies[type] > quantity
puts "Używanie #{quantity} jednostek #{type} z magazynu."
@supplies[type] -= quantity
else
puts "Błąd Zaopatrzenia: Niedobór #{type} w magazynie."
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 "----- Akcja Paliwa -----"
puts "Ładowanie #{quantity} jednostek paliwa do zbiornika."
@fuel += quantity
end
def use_fuel(quantity)
puts "----- Akcja Paliwa -----"
puts "Używanie #{quantity} jednostek paliwa ze zbiornika."
@fuel -= quantity
end
end
class Thrusters
FUEL_PER_THRUST = 10
def initialize(fuel_tank)
@linked_fuel_tank = fuel_tank
end
def activate_thrusters
puts "----- Akcja Silników -----"
if @linked_fuel_tank.get_fuel_levels >= FUEL_PER_THRUST
puts "Akcja ciągu powiodła się."
@linked_fuel_tank.use_fuel(FUEL_PER_THRUST)
else
puts "Błąd Silnika: Niedobór paliwa."
end
end
end
class Reporter
def initialize(item, type)
@linked_item = item
@type = type
end
def report
puts "----- Raport #{@type.capitalize} -----"
end
end
class FuelReporter < Reporter
def initialize(item)
super(item, "paliwo")
end
def report
super
puts "#{@linked_item.get_fuel_levels} jednostek paliwa dostępnych."
end
end
class SupplyReporter 0
@linked_item.get_supplies.each do |type, quantity|
puts "#{type} dostępne: #{quantity} jednostek"
end
else
puts "Magazyn jest pusty."
end
end
end
iss = SpaceStation.new
iss.sensors.run_sensors
# ----- Akcja Sensora -----
# Uruchamianie sensorów!
iss.supply_hold.use_supplies("części", 2)
# ----- Akcja Zaopatrzenia -----
# Błąd Zaopatrzenia: Niedobór części w magazynie.
iss.supply_hold.load_supplies("części", 10)
# ----- Akcja Zaopatrzenia -----
# Ładowanie 10 jednostek części do magazynu.
iss.supply_hold.use_supplies("części", 2)
# ----- Akcja Zaopatrzenia -----
# Używanie 2 jednostek części z magazynu.
iss.supply_reporter.report
# ----- Raport Zaopatrzenia -----
# części dostępne: 8 jednostek
iss.thrusters.activate_thrusters
# ----- Akcja Silników -----
# Błąd Silnika: Niedobór paliwa.
iss.fuel_tank.load_fuel(100)
# ----- Akcja Paliwa -----
# Ładowanie 100 jednostek paliwa do zbiornika.
iss.thrusters.activate_thrusters
# ----- Akcja Silników -----
# Akcja ciągu powiodła się.
# ----- Akcja Paliwa -----
# Używanie 10 jednostek paliwa ze zbiornika.
iss.fuel_reporter.report
# ----- Raport Paliwa -----
# 90 jednostek paliwa dostępnych.W tej, ostatniej wersji programu obowiązki zostały podzielone na dwie nowe klasy, FuelReporter i SupplyReporter. Obie są podklasami klasy Reporter. Dodatkowo dodaliśmy zmienne instancji do klasy SpaceStation, aby w razie potrzeby zainicjować odpowiednią podklasę. Teraz, jeśli Ziemia zdecyduje się na wprowadzenie jakichkolwiek zmian, wprowadzimy poprawki w podklasach, a nie w klasie głównej.
Oczywiście, niektóre klasy wciąż są od siebie zależne. Tak więc obiekt SupplyReporter jest zależny od SupplyHold, a FuelReporter od FuelTank. Naturalnie, akceleratory muszą być powiązane z zbiornikiem paliwa. Tutaj wszystko wygląda logicznie, a wprowadzenie zmian nie będzie szczególnie trudne — edytowanie kodu jednego obiektu nie wpłynie zbytnio na inny.
Stworzyliśmy w ten sposób modułowy kod, w którym obowiązki każdego z obiektów/klas są dokładnie określone. Praca z takim kodem to nie problem, jego utrzymanie będzie prostym zadaniem. Cały "boski obiekt" przekształciliśmy w SRP.
Skillbox poleca:
- Dwuletni praktyczny kurs .
- Kurs online .
- Praktyczny roczny kurs .
Źródło: habr.com
