Skribante flekseblan kodon uzante SOLID

Skribante flekseblan kodon uzante SOLID

De la tradukinto: publikigita por vi artikolo de Severin Perez pri uzado de SOLIDAJ principoj en programado. La informoj de la artikolo estos utilaj al komencantoj kaj spertaj programistoj.

Se vi estas en evoluo, vi plej verŝajne aŭdis pri la SOLIDAJ principoj. Ili ebligas al la programisto skribi puran, bone strukturitan kaj facile konserveblan kodon. Indas noti, ke en programado ekzistas pluraj aliroj al kiel ĝuste plenumi apartan laboron. Malsamaj specialistoj havas malsamajn ideojn kaj komprenon pri la "ĝusta vojo"; ĉio dependas de la sperto de ĉiu persono. Tamen, la ideoj proklamitaj en SOLID estas akceptitaj de preskaŭ ĉiuj reprezentantoj de la IT-komunumo. Ili iĝis la deirpunkto por la apero kaj evoluo de multaj bonaj evoluaj administradpraktikoj.

Ni komprenu, kio estas la SOLIDAJ principoj kaj kiel ili helpas nin.

Skillbox rekomendas: Praktika kurso "Poŝtelefona Programisto PRO".

Ni memorigas vin: por ĉiuj legantoj de "Habr" - rabato de 10 000 rubloj kiam oni enskribas en iu ajn Skillbox-kurso per la reklamkodo "Habr".

Kio estas SOLIDA?

Ĉi tiu termino estas mallongigo, ĉiu litero de la termino estas la komenco de la nomo de specifa principo:

  • SAngla Principo de Respondeco. Modulo povas havi unu kaj nur unu kialon por ŝanĝo.
  • la Oplumo/Fermita Principo (malfermita/fermita principo). Klasoj kaj aliaj elementoj estu malfermitaj por etendo, sed fermitaj por modifo.
  •  la LIskov Anstataŭiga Principo (Liskov-anstataŭiga principo). Funkcioj kiuj uzas bazan tipon devus povi uzi subtipojn de la baza tipo sen scii ĝin.
  • la IInterfaca Apartigo-Principo  (principo de apartigo de interfaco). Programaraj entoj ne devus dependi de metodoj, kiujn ili ne uzas.
  • la Ddependa Inversio-Principo (principo de dependeca inversio). Moduloj ĉe pli altaj niveloj ne devus dependi de moduloj ĉe pli malaltaj niveloj.

Unua Principo de Respondeco


La Single Responsibility Principle (SRP) deklaras ke ĉiu klaso aŭ modulo en programo devus respondeci pri nur unu parto de la funkcieco de tiu programo. Aldone, elementoj de ĉi tiu respondeco devus esti asignitaj al sia propra klaso, prefere ol disigitaj trans neparencaj klasoj. La ellaboranto kaj ĉefa evangeliisto de SRP, Robert S. Martin, priskribas respondecon kiel la kialon de ŝanĝo. Li origine proponis tiun esprimon kiel unu el la elementoj de sia laboro "Principoj de Objekt-Orientita Dezajno". La koncepto asimilas multon da la konekteblecpadrono kiu antaŭe estis difinita fare de Tom DeMarco.

La koncepto ankaŭ inkludis plurajn konceptojn formulitajn fare de David Parnas. La du ĉefaj estas enkapsulado kaj informkaŝado. Parnas argumentis ke dividado de sistemo en apartajn modulojn ne devus esti bazita sur analizo de blokdiagramoj aŭ ekzekutfluoj. Ajna el la moduloj devas enhavi specifan solvon, kiu provizas minimumon da informoj al klientoj.

Cetere, Martin donis interesan ekzemplon kun altrangaj administrantoj de kompanio (COO, CTO, CFO), ĉiu el kiuj uzas specifajn komercajn programojn por malsamaj celoj. Kiel rezulto, iu ajn el ili povas efektivigi ŝanĝojn en la programaro sen tuŝi la interesojn de aliaj administrantoj.

Dia objekto

Kiel ĉiam, la plej bona maniero lerni SRP estas vidi ĝin en ago. Ni rigardu sekcion de la programo, kiu NE sekvas la Unuan Respondecon. Ĉi tio estas Ruby-kodo, kiu priskribas la konduton kaj atributojn de la kosmostacio.

Revizu la ekzemplon kaj provu determini la jenon:
Respondecoj de tiuj objektoj kiuj estas deklaritaj en la SpaceStation klaso.
Tiuj, kiuj eble interesiĝas pri la funkciado de la kosmostacio.

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} avalilable: #{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

Efektive, nia kosmostacio estas malfunkcia (mi pensas, ke mi ne baldaŭ ricevos vokon de NASA), sed estas io analizenda ĉi tie.

Tiel, la SpaceStation-klaso havas plurajn malsamajn respondecojn (aŭ taskojn). Ĉiuj ili povas esti dividitaj en tipojn:

  • sensiloj;
  • provizoj (konsumeblaj);
  • brulaĵo;
  • akceliloj.

Eĉ se neniu el la dungitoj de la stacio ricevas klason, ni povas facile imagi kiu respondecas pri kio. Plej verŝajne, la sciencisto kontrolas la sensilojn, la loĝistiko respondecas pri liverado de rimedoj, la inĝeniero respondecas pri fuelprovizoj, kaj la piloto kontrolas la akceliloj.

Ĉu ni povas diri, ke ĉi tiu programo ne konformas al SRP? Jes certa. Sed la klaso SpaceStation estas tipa "diobjekto", kiu scias ĉion kaj faras ĉion. Ĉi tio estas grava kontraŭ-ŝablono en objekt-orientita programado. Por komencanto, tiaj objektoj estas ege malfacile konserveblaj. Ĝis nun la programo estas tre simpla, jes, sed imagu kio okazos se ni aldonos novajn funkciojn. Eble nia kosmostacio bezonos medicinan stacion aŭ kunvenejon. Kaj ju pli da funkcioj estas, des pli da SpaceStation kreskos. Nu, ĉar ĉi tiu instalaĵo estos konektita al aliaj, servi la tutan komplekson fariĝos eĉ pli kompleksa. Kiel rezulto, ni povas interrompi la funkciadon de, ekzemple, akceliloj. Se esploristo petas ŝanĝojn al la sensiloj, tio povus tre bone influi la komunikajn sistemojn de la stacio.

Malobservo de la SRP-principo povas doni mallongdaŭran taktikan venkon, sed finfine ni "perdos la militon", kaj estos tre malfacile konservi tian monstron estonte. Plej bone estas dividi la programon en apartajn sekciojn de kodo, ĉiu el kiuj respondecas pri plenumi specifan operacion. Komprenante ĉi tion, ni ŝanĝu la SpaceStation-klason.

Ni distribuu respondecon

Supre ni difinis kvar specojn de operacioj, kiuj estas kontrolitaj de la klaso SpaceStation. Ni tenos ilin en menso dum refactoring. La ĝisdatigita kodo pli bone kongruas kun la 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 "----- Sensor Action -----"
    puts "Running sensors!"
  end
end
 
class SupplyHold
  attr_accessor :supplies
 
  def initialize
    @supplies = {}
  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} avalilable: #{quantity} units"
      end
    else
      puts "Supply hold is empty."
    end
  end
end
 
class FuelTank
  attr_accessor :fuel
 
  def initialize
    @fuel = 0
  end
 
  def get_fuel_levels
    @fuel
  end
 
  def load_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Loading #{quantity} units of fuel in the tank."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Using #{quantity} units of fuel from the tank."
    @fuel -= quantity
  end
 
  def report_fuel
    puts "----- Fuel Report -----"
    puts "#{@fuel} units of fuel available."
  end
end
 
class Thrusters
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Thruster Action -----"
    if @linked_fuel_tank.get_fuel_levels >= 10
      puts "Thrusting action successful."
      @linked_fuel_tank.use_fuel(10)
    else
      puts "Thruster Error: Insufficient fuel available."
    end
  end
end

Estas multaj ŝanĝoj, la programo certe aspektas pli bone nun. Nun nia klaso SpaceStation fariĝis pli ujo, en kiu operacioj estas komencitaj por dependaj partoj, inkluzive de aro da sensiloj, konsumebla provizosistemo, benzinujo kaj akceliloj.

Por iu ajn el la variabloj ekzistas nun responda klaso: Sensiloj; SupplyHold; FuelTank; Repuŝiloj.

Estas pluraj gravaj ŝanĝoj en ĉi tiu versio de la kodo. La punkto estas, ke individuaj funkcioj ne nur estas enkapsuligitaj en siaj propraj klasoj, ili estas organizitaj tiel, ke ili fariĝas antaŭvideblaj kaj konsekvencaj. Ni grupigas elementojn kun simila funkcieco por sekvi la principon de kohereco. Nun, se ni bezonas ŝanĝi la manieron kiel la sistemo funkcias, moviĝante de hashstrukturo al tabelo, simple uzu la SupplyHold-klason; ni ne devas tuŝi aliajn modulojn. Tiel, se la loĝistika oficisto ŝanĝas ion en sia sekcio, la resto de la stacio restos sendifekta. En ĉi tiu kazo, la klaso SpaceStation eĉ ne konscios pri la ŝanĝoj.

Niaj oficiroj laborantaj sur la kosmostacio verŝajne ĝojas pri la ŝanĝoj ĉar ili povas peti tiujn, kiujn ili bezonas. Rimarku, ke la kodo havas metodojn kiel report_supplies kaj report_fuel enhavitaj en la SupplyHold kaj FuelTank klasoj. Kio okazus se la Tero petus ŝanĝi la manieron kiel ĝi raportas? Ambaŭ klasoj, SupplyHold kaj FuelTank, devos esti ŝanĝitaj. Kio se vi bezonas ŝanĝi la manieron kiel brulaĵo kaj konsumeblaj estas liveritaj? Vi verŝajne devos ŝanĝi ĉiujn samajn klasojn denove. Kaj ĉi tio jam estas malobservo de la SRP-principo. Ni riparu ĉi tion.

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 "----- Sensor Action -----"
    puts "Running sensors!"
  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 "----- 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
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 "----- Fuel Action -----"
    puts "Loading #{quantity} units of fuel in the tank."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Using #{quantity} units of fuel from the tank."
    @fuel -= quantity
  end
end
 
class Thrusters
  FUEL_PER_THRUST = 10
 
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Thruster Action -----"
    
    if @linked_fuel_tank.get_fuel_levels >= FUEL_PER_THRUST
      puts "Thrusting action successful."
      @linked_fuel_tank.use_fuel(FUEL_PER_THRUST)
    else
      puts "Thruster Error: Insufficient fuel available."
    end
  end
end
 
class Reporter
  def initialize(item, type)
    @linked_item = item
    @type = type
  end
 
  def report
    puts "----- #{@type.capitalize} Report -----"
  end
end
 
class FuelReporter < Reporter
  def initialize(item)
    super(item, "fuel")
  end
 
  def report
    super
    puts "#{@linked_item.get_fuel_levels} units of fuel available."
  end
end
 
class SupplyReporter < Reporter
  def initialize(item)
    super(item, "supply")
  end
 
  def report
    super
    if @linked_item.get_supplies.keys.length > 0
      @linked_item.get_supplies.each do |type, quantity|
        puts "#{type} avalilable: #{quantity} units"
      end
    else
      puts "Supply hold is empty."
    end
  end
end
 
iss = SpaceStation.new
 
iss.sensors.run_sensors
  # ----- Sensor Action -----
  # Running sensors!
 
iss.supply_hold.use_supplies("parts", 2)
  # ----- Supply Action -----
  # Supply Error: Insufficient parts in the supply hold.
iss.supply_hold.load_supplies("parts", 10)
  # ----- Supply Action -----
  # Loading 10 units of parts in the supply hold.
iss.supply_hold.use_supplies("parts", 2)
  # ----- Supply Action -----
  # Using 2 of parts from the supply hold.
iss.supply_reporter.report
  # ----- Supply Report -----
  # parts avalilable: 8 units
 
iss.thrusters.activate_thrusters
  # ----- Thruster Action -----
  # Thruster Error: Insufficient fuel available.
iss.fuel_tank.load_fuel(100)
  # ----- Fuel Action -----
  # Loading 100 units of fuel in the tank.
iss.thrusters.activate_thrusters
  # ----- Thruster Action -----
  # Thrusting action successful.
  # ----- Fuel Action -----
  # Using 10 units of fuel from the tank.
iss.fuel_reporter.report
  # ----- Fuel Report -----
# 90 units of fuel available.

En ĉi tiu lasta versio de la programo, la respondecoj estis dividitaj en du novajn klasojn, FuelReporter kaj SupplyReporter. Ili ambaŭ estas infanoj de la klaso Reporter. Krome, ni aldonis ekzemplervariablojn al la SpaceStation-klaso por ke la dezirata subklaso povas esti pravaligita se necese. Nun, se la Tero decidas ŝanĝi ion alian, tiam ni faros ŝanĝojn al la subklasoj, kaj ne al la ĉefa klaso.

Kompreneble, kelkaj el niaj klasoj ankoraŭ dependas unu de la alia. Tiel, la objekto SupplyReporter dependas de SupplyHold, kaj FuelReporter dependas de FuelTank. Kompreneble, la akceliloj devas esti konektitaj al la benzinujo. Sed ĉi tie ĉio jam aspektas logika, kaj fari ŝanĝojn ne estos aparte malfacila - redakti la kodon de unu objekto ne multe influos alian.

Tiel, ni kreis modulan kodon kie la respondecoj de ĉiu el la objektoj/klasoj estas precize difinitaj. Labori kun tia kodo ne estas problemo, konservi ĝin estos simpla tasko. Ni konvertis la tutan "dian objekton" en SRP.

Skillbox rekomendas:

fonto: www.habr.com

Aldoni komenton