Piszemy elastyczny kod, korzystając z SOLID

Piszemy elastyczny kod, korzystając z SOLID

Od tłumacza: opublikowali dla was artykuł Severina Peresa 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 „Mobilny programista PRO”.

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.
  • Lista OOpen/Closed Principle (zasada otwartości/zamknięcia). Klasy i inne elementy powinny być otwarte na rozbudowę, ale zamknięte na modyfikacje.
  •  The LLiskov Substitution Principle (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.
  • Lista IInterface Segregation Principle (zasada segregacji interfejsu). Jednostki programowe nie powinny zależeć od metod, które nie są przez nie wykorzystywane.
  • Lista DDependency Inversion Principle (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
end

Nasza 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
end

Wprowadzone 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:

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster