Da u traduttore: publicatu per voi articulu di Severin Perez circa l'usu di i principii SOLID in a prugrammazione. L'infurmazioni da l'articulu seranu utili à i principianti è i programatori sperimentati.
Sè vo site in u sviluppu, probabilmente avete intesu parlà di i principii SOLID. Permettenu à u programatore di scrive un codice pulito, ben strutturatu è faciule da mantene. Hè da nutà chì in a prugrammazione ci sò parechji approcci à cumu fà currettamente un travagliu particulari. Diversi spezialisti anu idee diverse è capiscenu a "strada ghjusta"; tuttu dipende di l'esperienza di ogni persona. Tuttavia, l'idee proclamate in SOLID sò accettate da quasi tutti i rapprisentanti di a cumunità IT. Sò diventati u puntu di partenza per l'emergenza è u sviluppu di parechje pratiche di gestione di u sviluppu bè.
Capemu ciò chì sò i principii SOLID è cumu ci aiutanu.
Ramintemu:per tutti i lettori di "Habr" - un scontu di 10 000 rubles quandu si iscrizzione in ogni cursu Skillbox cù u codice promozionale "Habr".
Cosa hè SOLID?
Stu termu hè una abbreviazione, ogni lettera di u termu hè u principiu di u nome di un principiu specificu:
SPrincipiu di rispunsabilità. Un modulu pò avè una è una sola ragione per cambià.
lu OPenna / Principiu Chiusu (principiu apertu / chjusu). E classi è altri elementi devenu esse aperti per estensione, ma chjusi per mudificazione.
lu LIskov Principiu di Sustituzione (principiu di sustituzione di Liskov). E funzioni chì utilizanu un tipu di basa duveranu esse capaci di utilizà sottotipi di u tipu di basa senza sapè.
lu DPrincipiu d'inversione di dipendenza (principiu di l'inversione di dependenza). I moduli à i livelli più alti ùn deve micca dipende di i moduli à i livelli più bassi.
Principiu di Responsabilità Unicu
U Principiu di Responsabilità Unicu (SRP) dice chì ogni classa o modulu in un prugramma deve esse rispunsevuli di una sola parte di e funziunalità di quellu prugramma. Inoltre, elementi di sta rispunsabilità deve esse attribuiti à a so propria classa, piuttostu cà spargugliati in classi senza relazione. U sviluppatore di SRP è u capu evangelista, Robert S. Martin, descrive a responsabilità cum'è u mutivu di u cambiamentu. Hè prupostu urigginariamenti stu terminu cum'è unu di l'elementi di u so travagliu "Principi di u Design Orientatu à l'Ughjettu". U cuncettu incorpora assai di u mudellu di cunnessione chì era definitu prima da Tom DeMarco.
U cuncettu include ancu parechji cuncetti formulati da David Parnas. I dui principali sò l'incapsulazione è l'ocultazione di l'infurmazioni. Parnas hà sustinutu chì a divisione di un sistema in moduli separati ùn deve esse basatu annantu à l'analisi di i diagrammi di blocchi o di i flussi di esecuzione. Qualchidunu di i moduli deve cuntene una suluzione specifica chì furnisce un minimu di informazioni à i clienti.
Per via, Martin hà datu un esempiu interessante cù i dirigenti di l'impresa (COO, CTO, CFO), ognunu di quale usa un software di cummerciale specificu per diversi scopi. In u risultatu, qualsiasi di elli ponu implementà cambiamenti in u software senza affettà l'interessi di l'altri gestori.
Ughjettu divinu
Cum'è sempre, u megliu modu per amparà SRP hè di vede in azzione. Fighjemu una sezione di u prugramma chì ùn seguita micca u Principiu di Responsabilità Unicu. Questu hè u codice Ruby chì descrive u cumpurtamentu è l'attributi di a stazione spaziale.
Revisate l'esempiu è pruvate à determinà i seguenti:
Responsabilità di quelli oggetti chì sò dichjarati in a classa SpaceStation.
Quelli chì ponu esse interessatu in u funziunamentu di a stazione spaziale.
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
In verità, a nostra stazione spaziale hè disfunzionale (ùn pensu micca chì riceveraghju una chjamata da a NASA prestu), ma ci hè qualcosa da analizà quì.
Cusì, a classa SpaceStation hà parechje rispunsabilità (o compiti). Tutti ponu esse divisu in tipi:
sensori;
forniture (consumables);
carburante;
acceleratori.
Ancu s'è nimu di l'impiegati di a stazione sò assignati una classa, pudemu facilmente imagine quale hè rispunsevule per ciò chì. Probabilmente, u scientist cuntrola i sensori, u logisticu hè rispunsevule per furnisce i risorse, l'ingegnere hè rispunsevuli di u suministru di carburante, è u pilotu cuntrolla i boosters.
Pudemu dì chì stu prugramma ùn hè micca conforme à SRP? Iè, sicuru. Ma a classa SpaceStation hè un "ughjettu di Diu" tipicu chì sapi tuttu è face tuttu. Questu hè un grande anti-pattern in a prugrammazione orientata à l'ughjettu. Per un principiante, tali oggetti sò assai difficiuli di mantene. Finu a ora, u prugramma hè assai simplice, sì, ma imaginate ciò chì succede si aghjunghjenu novi funziunalità. Forse a nostra stazione spaziale avarà bisognu di una stazione medica o di una sala di riunioni. E più funzioni ci sò, più SpaceStation cresce. Ebbè, postu chì sta facilità serà cunnessa cù l'altri, u serviziu di tuttu u cumplessu diventerà ancu più difficiule. In u risultatu, pudemu disturbà u funziunamentu di, per esempiu, acceleratori. Se un ricercatore dumanda cambiamenti à i sensori, questu puderia influenzà assai bè i sistemi di cumunicazione di a stazione.
A violazione di u principiu di SRP pò dà una vittoria tattica à cortu termine, ma à a fine "perderemu a guerra", è diventerà assai difficiuli di mantene un tali mostru in u futuru. Hè megliu di dividisce u prugramma in sezzioni separati di codice, ognuna di quale hè rispunsevuli di fà una operazione specifica. Capiscendu questu, cambiemu a classa SpaceStation.
Distribuemu a rispunsabilità
Sopra avemu definitu quattru tipi di operazioni chì sò cuntrullati da a classa SpaceStation. Avemu da tene à mente quandu refactoring. U codice aghjurnatu currisponde megliu à u 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
Ci sò assai cambiamenti, u prugramma hè definitu megliu avà. Avà a nostra classa SpaceStation hè diventata più di un cuntainer in quale l'operazioni sò iniziate per e parte dipendente, cumpresu un set di sensori, un sistema di fornitura di cunsumu, un tank di carburante è boosters.
Per qualsiasi di e variàbili ci hè avà una classa currispundente: Sensors; SupplyHold; FuelTank; I propulsori.
Ci hè parechje cambiamenti impurtanti in questa versione di u codice. U puntu hè chì e funzioni individuali ùn sò micca solu incapsulate in i so classi, sò urganizati in modu chì diventanu prevedibile è coherente. Raggruppemu elementi cù funziunalità simili per seguità u principiu di coherenza. Avà, s'ellu ci vole à cambià u modu di travagliu di u sistema, passendu da una struttura di hash à un array, basta aduprà a classa SupplyHold; ùn avemu micca toccu altri moduli. Questu modu, se l'uffiziale di logistica cambia qualcosa in a so sezione, u restu di a stazione ferma intactu. In questu casu, a classa SpaceStation ùn serà ancu cunzignata di i cambiamenti.
I nostri ufficiali chì travaglianu in a stazione spaziale sò prubabilmente felici di i cambiamenti perchè ponu dumandà quelli chì anu bisognu. Avete chì u codice hà metudi cum'è report_supplies è report_fuel cuntenuti in e classi SupplyHold è FuelTank. Chì succederebbe se a Terra dumandassi di cambià a manera di raporta ? E duie classi, SupplyHold è FuelTank, anu da esse cambiate. E s'è avete bisognu di cambià a manera di furnisce u carburante è i consumabili? Probabilmente avete da cambià tutte e stesse classi di novu. È questu hè digià una violazione di u principiu SRP. Fixemu questu.
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.
In questa ultima versione di u prugramma, i rispunsabilità sò stati divisi in duie classi novi, FuelReporter è SupplyReporter. Sò tramindui figlioli di a classa Reporter. Inoltre, avemu aghjustatu variabili di istanza à a classa SpaceStation in modu chì a subclasse desiderata pò esse inizializzata se ne necessariu. Avà, se a Terra decide di cambià qualcosa altru, allora faremu cambiamenti à i subclassi, è micca à a classa principale.
Di sicuru, alcune di e nostre classi dipendenu sempre l'una di l'altru. Cusì, l'ughjettu SupplyReporter dipende di SupplyHold, è FuelReporter dipende di FuelTank. Di sicuru, i boosters deve esse cunnessi à u tank di carburante. Ma quì tuttu pare digià logicu, è fà cambiamenti ùn serà micca particularmente difficiule - edità u codice di un ughjettu ùn avarà micca assai un altru.
Cusì, avemu creatu un codice modulare induve e rispunsabilità di ognuna di l'uggetti / classi sò precisamente definite. U travagliu cù tali codice ùn hè micca un prublema, mantenenu serà un compitu simplice. Avemu cunvertitu tuttu u "ughjettu divinu" in SRP.