Pišemo fleksibilan kod koristeći SOLID

Pišemo fleksibilan kod koristeći SOLID

Od prevoditelja: objavili smo vam članak Severina Pereza o korištenju SOLID principa u programiranju. Informacije iz članka bit će korisne kako za novake, tako i za iskusne programere.

Ako se bavite razvojem, vjerojatno ste čuli za SOLID principe. Oni omogućavaju programerima pisanje čistog, dobro strukturiranog i lako održivog koda. Važno je napomenuti da u programiranju postoji nekoliko pristupa kako pravilno izvršiti neki zadatak. Različiti stručnjaci imaju različite ideje i tumačenja „ispravnog puta“, a sve zavisi od iskustva svakog pojedinca. Ipak, ideje koje su proklamirane u SOLID-u prihvaćaju gotovo svi članovi IT zajednice. Postali su polazišna točka za pojavu i razvoj mnogih dobrih metoda upravljanja razvojem.

Pogledajmo što su SOLID principi i kako nam pomažu.

Skillbox preporučuje: Praktični tečaj «Mobilni programer PRO».

Podsjećamo: za sve čitatelje "Habra" — popust od 10 000 rubalja prilikom upisa na bilo koji tečaj Skillbox uz promo kod "Habr".

Što su SOLID?

Ovaj pojam je akronim, svako slovo označava početno slovo određenog principa:

Princip jedinstvene odgovornosti

 
Načelo jedinstvene odgovornosti (SRP) tvrdi da bi svaki razred ili modul u programu trebao biti odgovoran samo za jedan dio funkcionalnosti tog programa. Također, elementi te odgovornosti trebaju biti vezani za svoj razred, a ne raspoređeni po nepovezanim razredima. Razvijač i glavni zagovornik SRP-a, Robert C. Martin, opisuje odgovornost kao uzrok promjena. Prvotno je ovaj termin predložio kao jedan od elemenata svog rada 'Načela objektno orijentiranog dizajna'. U koncept su uključene mnoge značajke kohezije, koje je ranije definirao Tom DeMarco.

U koncept su također uključeni pojmovi koje je formulirao David Parnas. Dva osnovna su inkapsulacija i skrivanje informacija. Parnas je tvrdio da razdvajanje sustava na odvojene module ne bi trebalo biti temeljeno na analizi blok-dijagrama ili tokova izvršenja. Svaki od modula trebao bi sadržavati određeno rješenje koje pruža minimalne informacije klijentima.

Usput, Martin je naveo zanimljiv primjer s višim menadžerima tvrtke (COO, CTO, CFO), od kojih svaki koristi specifičan poslovni softver s različitim ciljevima. Na kraju, svatko od njih može implementirati promjene u softver bez utjecaja na interese drugih menadžera.

Božanski objekt

Kao i obično, najbolji način da se prouči SRP je vidjeti ga u akciji. Pogledajmo dio programa koji NE odgovara načelu jedinstvene odgovornosti. Ovo je Ruby kod koji opisuje ponašanje i atribute svemirske stanice.

Pogledajte primjer i pokušajte odrediti sljedeće:
Obveze onih objekata koji su proglašeni u razredu SpaceStation.
Onih koji bi mogli biti zainteresirani za rad svemirske 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 svemirska stanica nije funkcionalna (mislim da neću dobiti poziv od NASA-e u skoroj budućnosti), ali ovdje ima što za analizirati.

Dakle, klasa SpaceStation ima nekoliko različitih odgovornosti (ili zadatka). Sve se njih može razvrstati po tipovima:

  • senzori;
  • opskrba (potrošni materijal);
  • gorivo;
  • potisnici.

Iako nitko od osoblja stanice nije definiran u klasi, možemo lako zamisliti tko za što odgovara. Vjerojatno znanstvenik kontrolira senzore, logističar je odgovoran za opskrbu resursima, inženjer upravlja zalihama goriva, a pilot kontrolira potisnike.

Možemo li reći da ova programa ne ispunjava SRP? Da, naravno. No, klasa SpaceStation je tipični „božanski objekt“ koji zna sve i sve obavlja. To je osnovni antišablon u objektno-orijentiranom programiranju. Za novake, takvi objekti su izuzetno složeni za održavanje. Program je trenutno vrlo jednostavan, da, ali zamislite što će se dogoditi kada dodamo nove funkcije. Možda će našoj svemirskoj stanici trebati medicinska stanica ili soba za sastanke. A što više funkcija dodamo, to će SpaceStation postati složenija. A budući da će ovaj objekt biti povezan s drugima, održavanje cijelog kompleksa postat će još složenije. Na kraju, mogli bismo poremetiti rad, primjerice, ubrzivača. Ako znanstvenik zatraži promjene u radu sa senzorima, to bi moglo utjecati na komunikacijske sustave stanice.

Kršenje SRP principa može donijeti kratkoročnu taktičku pobjedu, ali na kraju ćemo „izgubiti rat”, održavanje takvog čudovišta u budućnosti postat će vrlo otežano. Najbolje je program podijeliti na odvojene dijelove koda, od kojih svaki odgovara za izvršenje određene operacije. Shvaćajući to, promijenimo klasu SpaceStation.

Raspostavljanje odgovornosti

Gore smo definirali četiri vrste operacija koje kontrolira klasa SpaceStation. Prilikom refaktoringa imat ćemo ih na umu. Ažurirani kod bolje odgovara SRP-u.

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 "----- Akcija senzora -----"
    puts "Pokretanje senzora!"
  end
end
 
class SupplyHold
  attr_accessor :supplies
 
  def initialize
    @supplies = {}
  end
 
  def load_supplies(type, quantity)
    puts "----- Akcija opskrbe -----"
    puts "Učitavanje #{quantity} jedinica #{type} u skladište."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Akcija opskrbe -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Koristenje #{quantity} jedinica #{type} iz skladišta."
      @supplies[type] -= quantity
    else
      puts "Greška u opskrbi: Nedovoljno #{type} u skladištu."
    end
  end
 
  def report_supplies
    puts "----- Izvještaj o opskrbi -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} dostupno: #{quantity} jedinica"
      end
    else
      puts "Skladište je prazno."
    end
  end
end
 
class FuelTank
  attr_accessor :fuel
 
  def initialize
    @fuel = 0
  end
 
  def get_fuel_levels
    @fuel
  end
 
  def load_fuel(quantity)
    puts "----- Akcija goriva -----"
    puts "Učitavanje #{quantity} jedinica goriva u rezervoar."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Akcija goriva -----"
    puts "Koristenje #{quantity} jedinica goriva iz rezervoara."
    @fuel -= quantity
  end
 
  def report_fuel
    puts "----- Izvještaj o gorivu -----"
    puts "#{@fuel} jedinica goriva dostupno."
  end
end
 
class Thrusters
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Akcija potisnika -----"
    if @linked_fuel_tank.get_fuel_levels >= 10
      puts "Akcija potisnika uspješna."
      @linked_fuel_tank.use_fuel(10)
    else
      puts "Greška potisnika: Nedovoljno goriva."
    end
  end
end

Bilo je mnogo promjena, program sada izgleda definitivno bolje. Naš razred SpaceStation sada je u suštini kontejner u kojem se iniciraju operacije za ovisne dijelove, uključujući skup senzora, sustav opskrbe, rezervoar za gorivo, i potisnike.

Svaka od varijabli sada ima odgovarajući razred: Sensors; SupplyHold; FuelTank; Thrusters.

U ovoj verziji koda postoji nekoliko važnih promjena. Naime, posebne funkcije nisu samo enkapsulirane unutar vlastitih klasa, već su organizirane na način da postanu predvidljive i dosljedne. Grupiramo slične elemente po funkcionalnosti da bismo slijedili princip kohezije. Sada, ako budemo trebali promijeniti način rada sustava, prelazeći s heksadodekadske strukture na niz, jednostavno ćemo koristiti klasu SupplyHold, a da pritom ne diramo druge module. Tako će, ako član osoblja zadužen za logistiku napravi neku promjenu u svom odjelu, ostali elementi stanice ostati netaknuti. Pri tome klasa SpaceStation neće ni biti svjesna promjena.

Naši službenici koji rade na svemirskoj stanici vjerojatno su zadovoljni promjenama jer mogu tražiti ono što im je zaista potrebno. Imajte na umu da u kodu postoje metode poput report_supplies i report_fuel, smještene u klasama SupplyHold i FuelTank. Što će se dogoditi ako Zemlja zatraži promjenu načina oblikovanja izvještaja? Trebat će promijeniti obje klase, SupplyHold i FuelTank. A što ako će biti potrebno promijeniti način isporuke goriva i potrepština? Vjerojatno će ponovno biti potrebno promijeniti sve iste klase. A to je već kršenje SRP principa. Popravimo to.

klasa SvemirskaStanica
  attr_reader :senzori, @supply_hold, @supply_reporter,
              :rezervoar_goriva, :fuel_reporter, :potisnici
 
  def initialize
    @senzori = Senzori.new
    @supply_hold = Zalihe.new
    @supply_reporter = ReporterZaliha.new(@supply_hold)
    @rezervoar_goriva = RezervoarGoriva.new
    @fuel_reporter = ReporterGoriva.new(@rezervoar_goriva)
    @potisnici = Potisnici.new(@rezervoar_goriva)
  end
end
 
klasa Senzori
  def run_senzori
    puts "----- Rad Senzora -----"
    puts "Pokretanje senzora!"
  end
end
 
klasa Zalihe
  attr_accessor :zalihe
  attr_reader :reporter
 
  def initialize
    @zalihe = {}
  end
 
  def get_zalihe
    @zalihe
  end
 
  def load_zalihe(vrsta, kolicina)
    puts "----- Akcija Zaliha -----"
    puts "Učitavanje #{kolicina} jedinica #{vrsta} u skladište."
    
    if @zalihe[vrsta]
      @zalihe[vrsta] += kolicina
    else
      @zalihe[vrsta] = kolicina
    end
  end
 
  def use_zalihe(vrsta, kolicina)
    puts "----- Akcija Zaliha -----"
    if @zalihe[vrsta] != nil && @zalihe[vrsta] > kolicina
      puts "Korištenje #{kolicina} od #{vrsta} iz skladišta."
      @zalihe[vrsta] -= kolicina
    else
      puts "Greška u Zaliha: Nedovoljno #{vrsta} u skladištu."
    end
  end
end
 
klasa RezervoarGoriva
  attr_accessor :gorivo
  attr_reader :reporter
 
  def initialize
    @gorivo = 0
  end
 
  def get_nivo_goriva
    @gorivo
  end
 
  def load_gorivo(kolicina)
    puts "----- Akcija Goriva -----"
    puts "Učitavanje #{kolicina} jedinica goriva u rezervoar."
    @gorivo += kolicina
  end
 
  def use_gorivo(kolicina)
    puts "----- Akcija Goriva -----"
    puts "Korištenje #{kolicina} jedinica goriva iz rezervoara."
    @gorivo -= kolicina
  end
end
 
klasa Potisnici
  FUEL_PER_THRUST = 10
 
  def initialize(rezervoar_goriva)
    @povezani_rezervoar_goriva = rezervoar_goriva
  end
 
  def activate_potisnike
    puts "----- Akcija Potisnika -----"
    
    if @povezani_rezervoar_goriva.get_nivo_goriva >= FUEL_PER_THRUST
      puts "Akcija potiska uspješna."
      @povezani_rezervoar_goriva.use_gorivo(FUEL_PER_THRUST)
    else
      puts "Greška Potisnika: Nedovoljno goriva dostupno."
    end
  end
end
 
klasa Reporter
  def initialize(item, vrsta)
    @povezani_item = item
    @vrsta = vrsta
  end
 
  def report
    puts "----- #{@vrsta.capitalize} Izvještaj -----"
  end
end
 
klasa ReporterGoriva < Reporter
  def initialize(item)
    super(item, "gorivo")
  end
 
  def report
    super
    puts "#{@povezani_item.get_nivo_goriva} jedinica goriva dostupno."
  end
end
 
klasa ReporterZaliha  0
      @povezani_item.get_zalihe.each do |vrsta, kolicina|
        puts "#{vrsta} dostupno: #{kolicina} jedinica"
      end
    else
      puts "Skladište je prazno."
    end
  end
end
 
iss = SvemirskaStanica.new
 
iss.senzori.run_senzori
  # ----- Rad Senzora -----
  # Pokretanje senzora!
 
iss.supply_hold.use_zalihe("dijelovi", 2)
  # ----- Akcija Zaliha -----
  # Greška u Zaliha: Nedovoljno dijelova u skladištu.
iss.supply_hold.load_zalihe("dijelovi", 10)
  # ----- Akcija Zaliha -----
  # Učitavanje 10 jedinica dijelova u skladište.
iss.supply_hold.use_zalihe("dijelovi", 2)
  # ----- Akcija Zaliha -----
  # Korištenje 2 od dijelova iz skladišta.
iss.supply_reporter.report
  # ----- Izvještaj o Zaliha -----
  # dijelovi dostupni: 8 jedinica
 
iss.potisnici.activate_potisnike
  # ----- Akcija Potisnika -----
  # Greška Potisnika: Nedovoljno goriva dostupno.
iss.rezervoar_goriva.load_gorivo(100)
  # ----- Akcija Goriva -----
  # Učitavanje 100 jedinica goriva u rezervoar.
iss.potisnici.activate_potisnike
  # ----- Akcija Potisnika -----
  # Akcija potiska uspješna.
  # ----- Akcija Goriva -----
  # Korištenje 10 jedinica goriva iz rezervoara.
iss.fuel_reporter.report
  # ----- Izvještaj o Gorivu -----
# 90 jedinica goriva dostupno.

U ovoj, posljednjoj verziji programa, obaveze su podijeljene u dvije nove klase, FuelReporter i SupplyReporter. Obje su podklase klase Reporter. Osim toga, dodali smo primjerne varijable klasi SpaceStation kako bismo, ako je potrebno, inicijalizirali odgovarajuću podklasu. Sada, ako Zemlja odluči promijeniti nešto drugo, napravit ćemo izmjene u podklasama, a ne u osnovnoj klasi.

Naravno, neki razredi još uvijek ovise jedni o drugima. Tako, objekt SupplyReporter ovisi o SupplyHold, dok FuelReporter ovisi o FuelTanku. Naravno, pojačivači moraju biti povezani s rezervoarom goriva. No, ovdje sve izgleda logično, a unošenje izmjena neće biti osobito teško — uređivanje koda jednog objekta neće previše utjecati na drugi.

Na taj način, stvorili smo modularni kod gdje su obaveze svakog od objekata/klasa točno definirane. Raditi s takvim kodom nije problem, njegovo održavanje bit će jednostavan zadatak. Cijeli "božanski objekt" preoblikovali smo u SRP.

Skillbox preporučuje:

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster