
Dall'autore: pubblicato per voi sull'uso dei principi SOLID nella programmazione. Le informazioni contenute nell'articolo saranno utili sia ai principianti che ai programmatori esperti.
Se ti occupi di sviluppo, probabilmente hai già sentito parlare dei principi SOLID. Questi permettono ai programmatori di scrivere codice pulito, ben strutturato e facilmente manutenibile. Va notato che nella programmazione ci sono diversi approcci su come eseguire correttamente un compito. Ogni specialista ha idee e comprensioni diverse del "modo giusto" di procedere, a seconda dell'esperienza individuale. Tuttavia, le idee espresse in SOLID sono ampiamente accettate da quasi tutti i membri della comunità IT. Sono diventate il punto di partenza per la nascita e lo sviluppo di molti buoni metodi di gestione dello sviluppo.
Analizziamo quali sono i principi SOLID e come possono aiutarci.
Skillbox consiglia: Corso pratico .
Ricordiamo: per tutti i lettori di «Habr» — sconto di 10.000 rubli per l'iscrizione a qualsiasi corso Skillbox con il codice promozionale «Habr».
Cosa sono i SOLID?
Questo termine è un acronimo, ogni lettera del termine rappresenta l'inizio del nome di un principio specifico:
- SPrincipio di Responsabilità Unica. Un modulo deve avere una e una sola ragione per cui cambiare.
- Le classi e altri elementi dovrebbero essere aperti all'estensione, ma chiusi alla modifica.
- Le funzioni che utilizzano un tipo base devono essere in grado di utilizzare i sottotipi di quel tipo senza conoscerli.
- Le entità software non dovrebbero dipendere da metodi che non utilizzano.
- I moduli di alto livello non dovrebbero dipendere dai moduli di basso livello.
Principio di Responsabilità Unica
Il principio di responsabilità unica (SRP) afferma che ogni classe o modulo in un programma dovrebbe essere responsabile solo per una parte della funzionalità di quel programma. Inoltre, gli elementi di questa responsabilità devono essere attribuiti alla propria classe e non distribuiti su classi non correlate. Lo sviluppatore e principale sostenitore del SRP, Robert C. Martin, descrive la responsabilità come la causa del cambiamento. Ha originariamente proposto questo termine come uno degli elementi del suo lavoro "Principi di progettazione orientata agli oggetti". Il concetto include gran parte della regola di coesione, definita in precedenza da Tom DeMarco.
Sono stati inclusi anche diversi concetti formulati da David Parnas. I due principali sono l'incapsulamento e la segregazione delle informazioni. Parnas sosteneva che la suddivisione di un sistema in moduli separati non dovrebbe basarsi sull'analisi dei diagrammi di flusso o dei flussi di esecuzione. Ognuno dei moduli dovrebbe contenere una soluzione specifica che fornisca il minimo di informazioni ai clienti.
A proposito, Martin ha fatto un interessante esempio con i dirigenti superiori dell'azienda (COO, CTO, CFO), ognuno dei quali utilizza software specifico per scopi diversi. Alla fine, ognuno di loro può implementare modifiche al software senza influenzare gli interessi degli altri dirigenti.
Oggetto divino
Come al solito, il modo migliore per comprendere il SRP è vedere tutto in azione. Diamo un'occhiata a un segmento di codice che NON rispetta il principio di responsabilità unica. Questo è un codice Ruby che descrive il comportamento e gli attributi di una stazione spaziale.
Esamina l'esempio e prova a determinare quanto segue:
Le responsabilità degli oggetti dichiarati nella classe SpaceStation.
Coloro che possono essere interessati al funzionamento della stazione spaziale.
classe SpaceStation
def initialize
@supplies = {}
@fuel = 0
end
def run_sensors
puts "----- Azione del sensore -----"
puts "Esecuzione dei sensori!"
end
def load_supplies(type, quantity)
puts "----- Azione di fornitura -----"
puts "Caricamento di #{quantity} unità di #{type} nel deposito delle forniture."
if @supplies[type]
@supplies[type] += quantity
else
@supplies[type] = quantity
end
end
def use_supplies(type, quantity)
puts "----- Azione di fornitura -----"
if @supplies[type] != nil && @supplies[type] > quantity
puts "Utilizzo di #{quantity} di #{type} dal deposito delle forniture."
@supplies[type] -= quantity
else
puts "Errore di fornitura: Fornitura insufficiente di #{type} nel deposito delle forniture."
end
end
def report_supplies
puts "----- Rapporto sulle forniture -----"
if @supplies.keys.length > 0
@supplies.each do |type, quantity|
puts "#{type} disponibile: #{quantity} unità"
end
else
puts "Il deposito delle forniture è vuoto."
end
end
def load_fuel(quantity)
puts "----- Azione di carburante -----"
puts "Caricamento di #{quantity} unità di carburante nel serbatoio."
@fuel += quantity
end
def report_fuel
puts "----- Rapporto sul carburante -----"
puts "#{@fuel} unità di carburante disponibili."
end
def activate_thrusters
puts "----- Azione dei razzi -----"
if @fuel >= 10
puts "Azione di spinta riuscita."
@fuel -= 10
else
puts "Errore dei razzi: Carburante insufficiente disponibile."
end
end
endIn effetti, la nostra stazione spaziale non è funzionale (non credo di ricevere una chiamata dalla NASA nel prossimo futuro), ma c'è molto da analizzare qui.
Quindi, la classe SpaceStation ha diverse responsabilità (o compiti). Tutti possono essere distinti per tipo:
- sensori;
- fornitura (consumabili);
- carburante;
- acceleratori.
Anche se nessuno dei membri dello staff della stazione è designato a una classe specifica, possiamo facilmente immaginare chi sia responsabile di cosa. Probabilmente, il ricercatore controlla i sensori, il logistico è responsabile della fornitura di risorse, l'ingegnere si occupa delle scorte di carburante e il pilota controlla gli acceleratori.
Possiamo dire che questo programma non rispetta il SRP? Sì, certo. Ma la classe SpaceStation è un tipico "oggetto divino", che sa tutto e fa tutto. Questo è il principale anti-pattern nella programmazione orientata agli oggetti. Per un principiante, tali oggetti sono estremamente difficili da manutenere. Finora il programma è molto semplice, sì, ma immaginate cosa succederebbe se aggiungessimo nuove funzionalità. Potrebbe essere necessario un ambulatorio o una sala riunioni per la nostra stazione spaziale. E più funzioni ci sono, più grande diventa SpaceStation. Inoltre, poiché questo oggetto sarà collegato ad altri, la manutenzione dell'intero complesso diventa ancora più complessa. Alla fine, potremmo compromettere il funzionamento, ad esempio, degli acceleratori. Se un ricercatore richiede modifiche nel lavoro con i sensori, questo potrebbe influenzare i sistemi di comunicazione della stazione.
La violazione del principio SRP può portare a una vittoria tattica a breve termine, ma alla fine perderemo "la guerra"; gestire un tale mostro in futuro diventerà piuttosto difficile. È meglio suddividere il programma in singole parti di codice, ognuna delle quali è responsabile dell'esecuzione di un'operazione specifica. Tenendo questo a mente, modifichiamo la classe SpaceStation.
Assegniamo le responsabilità
Abbiamo definito quattro tipi di operazioni controllate dalla classe SpaceStation. Durante il refactoring, le avremo in considerazione. Il codice aggiornato si conforma meglio al 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 "----- Azione del Sensore -----"
puts "Esecuzione dei sensori!"
end
end
class SupplyHold
attr_accessor :supplies
def initialize
@supplies = {}
end
def load_supplies(type, quantity)
puts "----- Azione di Carico -----"
puts "Caricamento di #{quantity} unità di #{type} nella riserva di forniture."
if @supplies[type]
@supplies[type] += quantity
else
@supplies[type] = quantity
end
end
def use_supplies(type, quantity)
puts "----- Azione di Consumo -----"
if @supplies[type] != nil && @supplies[type] > quantity
puts "Utilizzo di #{quantity} di #{type} dalla riserva di forniture."
@supplies[type] -= quantity
else
puts "Errore di Fornitura: Quantità insufficiente di #{type} nella riserva."
end
end
def report_supplies
puts "----- Rapporto di Fornitura -----"
if @supplies.keys.length > 0
@supplies.each do |type, quantity|
puts "#{type} disponibile: #{quantity} unità"
end
else
puts "La riserva di forniture è vuota."
end
end
end
class FuelTank
attr_accessor :fuel
def initialize
@fuel = 0
end
def get_fuel_levels
@fuel
end
def load_fuel(quantity)
puts "----- Azione di Carico del Carburante -----"
puts "Caricamento di #{quantity} unità di carburante nel serbatoio."
@fuel += quantity
end
def use_fuel(quantity)
puts "----- Azione di Consumo del Carburante -----"
puts "Utilizzo di #{quantity} unità di carburante dal serbatoio."
@fuel -= quantity
end
def report_fuel
puts "----- Rapporto di Carburante -----"
puts "#{@fuel} unità di carburante disponibile."
end
end
class Thrusters
def initialize(fuel_tank)
@linked_fuel_tank = fuel_tank
end
def activate_thrusters
puts "----- Azione dei Razzi -----"
if @linked_fuel_tank.get_fuel_levels >= 10
puts "Azione di spinta effettuata con successo."
@linked_fuel_tank.use_fuel(10)
else
puts "Errore dei Razzi: Carburante insufficiente disponibile."
end
end
endCi sono molte modifiche, il programma ora appare decisamente migliore. Adesso la nostra classe SpaceStation funge più da contenitore, all'interno del quale vengono iniziate le operazioni per le parti dipendenti, compresi un insieme di sensori, il sistema di fornitura dei materiali di consumo, il serbatoio del carburante e i razzi.
Per ciascuna delle variabili ora esiste una classe corrispondente: Sensors; SupplyHold; FuelTank; Thrusters.
In questa versione del codice ci sono alcuni cambiamenti importanti. Infatti, le singole funzioni non solo sono incapsulate in classi proprie, ma sono organizzate in modo da essere prevedibili e coerenti. Raggruppiamo elementi simili per funzionalità per seguire il principio di coesione. Ora, se abbiamo bisogno di modificare il funzionamento del sistema, passando da una struttura hash a un array, è sufficiente utilizzare la classe SupplyHold, senza dover toccare altri moduli. In questo modo, se l'ufficiale responsabile della logistica apporta delle modifiche alla sua sezione, gli altri elementi della stazione rimarranno intatti. Inoltre, la classe SpaceStation nemmeno sarà a conoscenza delle modifiche.
I nostri ufficiali, che lavorano sulla stazione spaziale, sono probabilmente felici dei cambiamenti, poiché possono richiedere ciò di cui hanno effettivamente bisogno. Si noti che nel codice ci sono metodi come report_supplies e report_fuel, presenti nelle classi SupplyHold e FuelTank. Cosa succede se la Terra richiede di modificare il modo in cui vengono generati i report? Sarà necessario modificare entrambe le classi, SupplyHold e FuelTank. E se fosse necessario cambiare il modo di erogare carburante e forniture? Probabilmente sarà necessario modificare di nuovo tutte le stesse classi. E questo violerebbe già il principio SRP. Correggiamo questa situazione.
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 0
@linked_item.get_supplies.each do |type, quantity|
puts "#{type} available: #{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 available: 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 del programma, le responsabilità sono state suddivise in due nuove classi, FuelReporter e SupplyReporter. Entrambi sono sottoclassi della classe Reporter. Inoltre, abbiamo aggiunto variabili di istanza alla classe SpaceStation in modo da poter inizializzare il sottotipo necessario se necessario. Ora, se la Terra dovesse decidere di cambiare qualcosa, apporteremo modifiche alle sottoclassi, piuttosto che alla classe principale.
Naturalmente, alcune classi dipendono ancora l'una dall'altra. Ad esempio, l'oggetto SupplyReporter dipende da SupplyHold, mentre FuelReporter dipende da FuelTank. Ovviamente, i razzi devono essere collegati al serbatoio di carburante. Ma ora tutto sembra logico e apportare modifiche non sarà particolarmente difficile: modificare il codice di un oggetto non influenzerà significativamente l'altro.
In questo modo, abbiamo creato codice modulare, dove le responsabilità di ciascun oggetto/classe sono chiaramente definite. Lavorare con questo codice non è un problema e la sua manutenzione sarà un compito semplice. Abbiamo trasformato tutto il "diossido" in SRP.
Skillbox consiglia:
- Corso pratico di due anni .
- Corso online .
- Corso pratico annuale .
Fonte: habr.com
