Pagsusulat ng flexible code gamit ang SOLID

Pagsusulat ng flexible code gamit ang SOLID

Mula sa tagasalin: na-publish para sa iyo artikulo ni Severin Perez tungkol sa paggamit ng mga SOLID na prinsipyo sa programming. Ang impormasyon mula sa artikulo ay magiging kapaki-pakinabang sa parehong mga nagsisimula at may karanasan na mga programmer.

Kung ikaw ay nasa pag-unlad, malamang na narinig mo na ang mga SOLID na prinsipyo. Binibigyang-daan nila ang programmer na magsulat ng malinis, maayos na pagkakaayos at madaling mapanatili na code. Kapansin-pansin na sa pagprograma mayroong maraming mga diskarte sa kung paano maayos na maisagawa ang isang partikular na trabaho. Ang iba't ibang mga espesyalista ay may iba't ibang mga ideya at pag-unawa sa "tamang landas"; ang lahat ay nakasalalay sa karanasan ng bawat tao. Gayunpaman, ang mga ideyang ipinahayag sa SOLID ay tinatanggap ng halos lahat ng mga kinatawan ng komunidad ng IT. Sila ang naging panimulang punto para sa paglitaw at pag-unlad ng maraming mahusay na mga kasanayan sa pamamahala ng pag-unlad.

Unawain natin kung ano ang SOLID na mga prinsipyo at kung paano tayo tinutulungan ng mga ito.

Inirerekomenda ng Skillbox ang: Praktikal na kurso "Mobile Developer PRO".

Pinapaalala namin sa iyo: para sa lahat ng mga mambabasa ng "Habr" - isang diskwento na 10 rubles kapag nag-enroll sa anumang kurso sa Skillbox gamit ang code na pang-promosyon ng "Habr".

Ano ang SOLID?

Ang terminong ito ay isang pagdadaglat, ang bawat titik ng termino ay ang simula ng pangalan ng isang tiyak na prinsipyo:

  • SPrinsipyo ng Pananagutan. Ang isang module ay maaaring magkaroon ng isa at isa lamang na dahilan para sa pagbabago.
  • Ang Opanulat/Saradong Prinsipyo (bukas/sarado na prinsipyo). Ang mga klase at iba pang elemento ay dapat na bukas para sa extension, ngunit sarado para sa pagbabago.
  • β€ŠAng LIskov Prinsipyo ng Pagpapalit (prinsipyo ng pagpapalit ng Liskov). Ang mga function na gumagamit ng base type ay dapat na gumamit ng mga subtype ng base type nang hindi alam.
  • Ang IPrinsipyo ng Interface Segregationβ€Š (prinsipyo ng paghihiwalay ng interface). Ang mga entity ng software ay hindi dapat umasa sa mga pamamaraan na hindi nila ginagamit.
  • Ang Dependency Inversion Principle (prinsipyo ng dependency inversion). Ang mga module sa mas mataas na antas ay hindi dapat nakadepende sa mga module sa mas mababang antas.

Prinsipyo ng Iisang Pananagutan

β€Š
Ang Single Responsibility Principle (SRP) ay nagsasaad na ang bawat klase o module sa isang programa ay dapat na responsable para sa isang bahagi lamang ng functionality ng program na iyon. Bilang karagdagan, ang mga elemento ng responsibilidad na ito ay dapat na italaga sa kanilang sariling klase, sa halip na nakakalat sa mga hindi nauugnay na klase. Inilarawan ng developer at punong ebanghelista ng SRP, si Robert S. Martin, ang pananagutan bilang dahilan ng pagbabago. Siya ang orihinal na iminungkahi ang terminong ito bilang isa sa mga elemento ng kanyang gawa na "Principles of Object-Oriented Design". Isinasama ng konsepto ang karamihan sa pattern ng pagkakakonekta na dating tinukoy ni Tom DeMarco.

Kasama rin sa konsepto ang ilang mga konsepto na binuo ni David Parnas. Ang dalawang pangunahing ay ang encapsulation at pagtatago ng impormasyon. Nagtalo si Parnas na ang paghahati ng isang sistema sa magkakahiwalay na mga module ay hindi dapat batay sa pagsusuri ng mga block diagram o mga daloy ng pagpapatupad. Ang alinman sa mga module ay dapat maglaman ng isang partikular na solusyon na nagbibigay ng pinakamababang impormasyon sa mga kliyente.

Siyanga pala, nagbigay si Martin ng isang kawili-wiling halimbawa sa mga senior manager ng isang kumpanya (COO, CTO, CFO), na bawat isa ay gumagamit ng partikular na software ng negosyo para sa iba't ibang layunin. Bilang resulta, ang sinuman sa kanila ay maaaring magpatupad ng mga pagbabago sa software nang hindi naaapektuhan ang mga interes ng ibang mga tagapamahala.

Banal na bagay

Gaya ng nakasanayan, ang pinakamahusay na paraan upang matutunan ang SRP ay makita ito sa pagkilos. Tingnan natin ang isang seksyon ng programa na HINDI sumusunod sa Single Responsibility Principle. Ito ang Ruby code na naglalarawan sa gawi at katangian ng space station.

Suriin ang halimbawa at subukang tukuyin ang sumusunod:
Mga responsibilidad ng mga bagay na idineklara sa klase ng SpaceStation.
Ang mga maaaring interesado sa pagpapatakbo ng istasyon ng kalawakan.

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

Actually, dysfunctional ang space station namin (I don't think I'll be getting a call from NASA anytime soon), pero may dapat pag-aralan dito.

Kaya, ang klase ng SpaceStation ay may iba't ibang mga responsibilidad (o mga gawain). Ang lahat ng mga ito ay maaaring nahahati sa mga uri:

  • mga sensor;
  • mga supply (consumables);
  • panggatong;
  • mga accelerators.

Kahit na wala sa mga empleyado ng istasyon ang naatasan ng klase, madali nating maisip kung sino ang may pananagutan sa kung ano. Malamang, kinokontrol ng siyentipiko ang mga sensor, ang logistician ang may pananagutan sa pagbibigay ng mga mapagkukunan, ang inhinyero ay responsable para sa mga supply ng gasolina, at ang piloto ang kumokontrol sa mga booster.

Masasabi ba natin na ang programang ito ay hindi sumusunod sa SRP? Oo ba. Ngunit ang klase ng SpaceStation ay isang tipikal na "god object" na nakakaalam ng lahat at ginagawa ang lahat. Ito ay isang pangunahing anti-pattern sa object-oriented programming. Para sa isang baguhan, ang mga naturang bagay ay napakahirap na mapanatili. Sa ngayon ang programa ay napaka-simple, oo, ngunit isipin kung ano ang mangyayari kung magdagdag kami ng mga bagong tampok. Marahil ang aming istasyon ng kalawakan ay mangangailangan ng isang medikal na istasyon o isang silid ng pagpupulong. At kung mas maraming function ang mayroon, mas lalago ang SpaceStation. Well, dahil ang pasilidad na ito ay konektado sa iba, ang pagseserbisyo sa buong complex ay magiging mas kumplikado. Bilang resulta, maaari nating maantala ang pagpapatakbo ng, halimbawa, mga accelerator. Kung ang isang mananaliksik ay humiling ng mga pagbabago sa mga sensor, ito ay lubos na makakaapekto sa mga sistema ng komunikasyon ng istasyon.

Ang paglabag sa prinsipyo ng SRP ay maaaring magbigay ng isang panandaliang taktikal na tagumpay, ngunit sa huli ay "matatalo tayo sa digmaan", at magiging napakahirap na mapanatili ang gayong halimaw sa hinaharap. Pinakamainam na hatiin ang programa sa magkakahiwalay na mga seksyon ng code, na ang bawat isa ay may pananagutan sa pagsasagawa ng isang partikular na operasyon. Pag-unawa dito, baguhin natin ang klase ng SpaceStation.

Ipamahagi natin ang responsibilidad

Sa itaas ay tinukoy namin ang apat na uri ng mga operasyon na kinokontrol ng klase ng SpaceStation. Isaisip natin ang mga ito kapag nagre-refactor. Ang na-update na code ay mas mahusay na tumutugma sa 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

Mayroong maraming mga pagbabago, ang programa ay tiyak na mukhang mas mahusay na ngayon. Ngayon ang aming klase sa SpaceStation ay naging higit na isang lalagyan kung saan sinisimulan ang mga operasyon para sa mga umaasang bahagi, kabilang ang isang hanay ng mga sensor, isang consumable supply system, isang fuel tank, at mga booster.

Para sa alinman sa mga variable mayroon na ngayong kaukulang klase: Mga Sensor; SupplyHold; FuelTank; Mga thruster.

Mayroong ilang mahahalagang pagbabago sa bersyong ito ng code. Ang punto ay ang mga indibidwal na function ay hindi lamang naka-encapsulated sa kanilang sariling mga klase, sila ay nakaayos sa paraang maging predictable at pare-pareho. Pinagpangkat namin ang mga elemento na may katulad na paggana upang sundin ang prinsipyo ng pagkakaugnay-ugnay. Ngayon, kung kailangan nating baguhin ang paraan ng paggana ng system, lumipat mula sa hash structure patungo sa array, gamitin lang ang SupplyHold class; hindi natin kailangang hawakan ang ibang mga module. Sa ganitong paraan, kung may babaguhin ang logistics officer sa kanyang seksyon, mananatiling buo ang natitirang bahagi ng istasyon. Sa kasong ito, hindi malalaman ng klase ng SpaceStation ang mga pagbabago.

Ang aming mga opisyal na nagtatrabaho sa istasyon ng kalawakan ay malamang na masaya tungkol sa mga pagbabago dahil maaari silang humiling ng mga kailangan nila. Pansinin na ang code ay may mga pamamaraan tulad ng report_supplies at report_fuel na nilalaman sa mga klase ng SupplyHold at FuelTank. Ano ang mangyayari kung hihilingin ng Earth na baguhin ang paraan ng pag-uulat nito? Ang parehong mga klase, SupplyHold at FuelTank, ay kailangang baguhin. Paano kung kailangan mong baguhin ang paraan ng paghahatid ng gasolina at mga consumable? Malamang na kailangan mong baguhin muli ang lahat ng parehong klase. At isa na itong paglabag sa prinsipyo ng SRP. Ayusin natin ito.

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.

Sa pinakabagong bersyon ng programa, ang mga responsibilidad ay nahahati sa dalawang bagong klase, FuelReporter at SupplyReporter. Pareho silang anak ng klase ng Reporter. Bilang karagdagan, nagdagdag kami ng mga variable ng instance sa klase ng SpaceStation para masimulan ang gustong subclass kung kinakailangan. Ngayon, kung magpasya ang Earth na baguhin ang ibang bagay, gagawa tayo ng mga pagbabago sa mga subclass, at hindi sa pangunahing klase.

Syempre, ang ilan sa mga klase namin ay nakadepende pa rin sa isa't isa. Kaya, ang SupplyReporter object ay nakasalalay sa SupplyHold, at FuelReporter ay nakasalalay sa FuelTank. Siyempre, ang mga boosters ay dapat na konektado sa tangke ng gasolina. Ngunit narito ang lahat ay mukhang lohikal, at ang paggawa ng mga pagbabago ay hindi magiging partikular na mahirap - ang pag-edit ng code ng isang bagay ay hindi makakaapekto sa isa pa.

Kaya, lumikha kami ng isang modular code kung saan ang mga responsibilidad ng bawat isa sa mga bagay/klase ay tiyak na tinukoy. Ang pagtatrabaho sa naturang code ay hindi isang problema, ang pagpapanatili nito ay isang simpleng gawain. Na-convert natin ang buong β€œdivine object” sa SRP.

Inirerekomenda ng Skillbox ang:

Pinagmulan: www.habr.com

Magdagdag ng komento