
Dalla traduzione: pubblicato per voi sull'uso dei principi SOLID nella programmazione. Le informazioni dell'articolo saranno utili sia ai principianti che ai programmatori esperti.
Se ti occupi di sviluppo, è probabile che tu abbia sentito parlare dei principi SOLID. Essi consentono ai programmatori di scrivere codice pulito, ben strutturato e facile da mantenere. È importante notare che ci sono diversi approcci in programmazione su come eseguire correttamente un determinato lavoro. I diversi specialisti hanno idee e comprensioni diverse del "percorso corretto", tutto dipende dall'esperienza di ciascuno. Tuttavia, le idee proclamate in SOLID sono accettate praticamente da tutti i rappresentanti della comunità IT. Sono diventate il punto di partenza per l'emergere e lo sviluppo di molti buoni metodi di gestione dello sviluppo.
Analizziamo cosa sono i principi SOLID e come ci aiutano.
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 è l'inizio del nome di un principio specifico:
- SPrincipio di Responsabilità Singola (Single Responsibility Principle). Un modulo può avere una e una sola ragione per essere modificato.
- Il
- Principio di Inversione delle Dipendenze (Dependency Inversion Principle). I moduli di alto livello non devono dipendere dai moduli di basso livello.
- Principio di Responsabilità Singola
- Principio di Responsabilità Singola
Principio di Responsabilità Singola
Il principio della responsabilità singola (SRP) afferma che ogni classe o modulo in un programma deve essere responsabile solo di una parte della funzionalità di quel programma. Inoltre, gli elementi di questa responsabilità devono essere attribuiti alla propria classe e non distribuiti tra classi non correlate. Lo sviluppatore e principale evangelista 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". La concettualizzazione include molte delle regole di coesione, che erano state definite in precedenza da Tom DeMarco.
Nel concetto sono stati inclusi anche alcuni termini formulati da David Parnas. I due principali sono incapsulamento e nascondimento delle informazioni. Parnas sosteneva che la suddivisione di un sistema in moduli separati non dovesse basarsi sull'analisi di diagrammi di flusso o flussi di esecuzione. Ognuno dei moduli dovrebbe contenere una soluzione specifica che fornisce il minimo di informazioni ai clienti.
A proposito, Martin portava un esempio interessante con i dirigenti di alto livello dell'azienda (COO, CTO, CFO), ognuno dei quali utilizza software specifico per il business con scopi diversi. Di conseguenza, ognuno di loro può implementare modifiche al software senza influenzare gli interessi degli altri manager.
Oggetto divino
Come al solito, il modo migliore per studiare il SRP è vedere tutto in azione. Vediamo un segmento di programma che NON rispetta il principio della responsabilità singola. Questo è codice Ruby che descrive il comportamento e gli attributi di una stazione spaziale.
Esamina l'esempio e prova a determinare quanto segue:
Le responsabilità di quegli oggetti che sono dichiarati nella classe SpaceStation.
Coloro che possono essere interessati al funzionamento della stazione spaziale.
class SpaceStation
def initialize
@supplies = {}
@fuel = 0
end
def run_sensors
puts "----- Azione Sensori -----"
puts "Esecuzione dei sensori!"
end
def load_supplies(type, quantity)
puts "----- Azione Rifornimento -----"
puts "Caricamento di #{quantity} unità di #{type} nel deposito di rifornimenti."
if @supplies[type]
@supplies[type] += quantity
else
@supplies[type] = quantity
end
end
def use_supplies(type, quantity)
puts "----- Azione Rifornimento -----"
if @supplies[type] != nil && @supplies[type] > quantity
puts "Utilizzo di #{quantity} di #{type} dal deposito di rifornimenti."
@supplies[type] -= quantity
else
puts "Errore di Rifornimento: Quantità insufficiente di #{type} nel deposito."
end
end
def report_supplies
puts "----- Rapporto Rifornimenti -----"
if @supplies.keys.length > 0
@supplies.each do |type, quantity|
puts "#{type} disponibile: #{quantity} unità"
end
else
puts "Il deposito è vuoto."
end
end
def load_fuel(quantity)
puts "----- Azione Carburante -----"
puts "Caricamento di #{quantity} unità di carburante nel serbatoio."
@fuel += quantity
end
def report_fuel
puts "----- Rapporto Carburante -----"
puts "#{@fuel} unità di carburante disponibili."
end
def activate_thrusters
puts "----- Azione Propulsori -----"
if @fuel >= 10
puts "Azione di spinta riuscita."
@fuel -= 10
else
puts "Errore Propulsore: Carburante insufficiente disponibile."
end
end
endIn effetti, la nostra stazione spaziale non è operativa (penso che non riceverò una chiamata da NASA nel prossimo futuro), ma c'è molto da analizzare qui.
Quindi, la classe SpaceStation ha diverse responsabilità (o compiti). Tutti possono essere suddivisi in categorie:
- sensori;
- rifornimenti (materiale di consumo);
- carburante;
- propulsori.
Sebbene nessuno degli impiegati della stazione sia definito nella classe, possiamo facilmente immaginare chi si occupi di cosa. Probabilmente, il ricercatore controlla i sensori, il logista si occupa dei rifornimenti, l'ingegnere gestisce le scorte di carburante, e il pilota controlla i propulsori.
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 è un modello antitetico nell'ambito della programmazione orientata agli oggetti. Per un principiante, tali oggetti sono estremamente complicati da gestire. Attualmente il programma è molto semplice, sì, ma immagina cosa succederebbe se aggiungessimo nuove funzionalità. Potrebbe darsi che alla nostra stazione spaziale serva un pronto soccorso o una sala riunioni. E più funzionalità ci saranno, più crescerà SpaceStation. E poiché questo oggetto sarà connesso ad altri, la gestione dell'intero complesso diventerà ancora più complessa. Alla fine, potremmo compromettere, ad esempio, il funzionamento degli acceleratori. Se un ricercatore richiede modifiche nel lavoro con i sensori, ciò potrebbe influire sui sistemi di comunicazione della stazione.
Violando il principio SRP, si può ottenere una vittoria tattica a breve termine, ma alla fine "perderemo la guerra", poiché mantenere un mostro del genere in futuro diventa piuttosto complicato. È meglio dividere il programma in sezioni di codice distinte, ciascuna delle quali è responsabile dell'esecuzione di una determinata operazione. Tenendo presente ciò, modifichiamo la classe SpaceStation.
Distribuiamo la responsabilità
In precedenza abbiamo definito quattro tipi di operazioni controllate dalla classe SpaceStation. Durante il refactoring, terremo presente questi elementi. Il codice aggiornato rispetta meglio il SRP.
classe StazioneSpaziale
attr_reader :sensori, :magazzino_di_approvvigionamento, :serbatoio_di_carburante, :razzi
def initialize
@magazzino_di_approvvigionamento = MagazzinoDiApprovvigionamento.new
@sensori = Sensori.new
@serbatoio_di_carburante = SerbatoioDiCarburante.new
@razzi = Razzi.new(@serbatoio_di_carburante)
end
end
classe Sensori
def run_sensors
puts "----- Azione Sensore -----"
puts "Esecuzione dei sensori!"
end
end
classe MagazzinoDiApprovvigionamento
attr_accessor :approvvigionamenti
def initialize
@approvvigionamenti = {}
end
def load_supplies(type, quantity)
puts "----- Azione Approvvigionamento -----"
puts "Caricamento di #{quantity} unità di #{type} nel magazzino di approvvigionamento."
if @approvvigionamenti[type]
@approvvigionamenti[type] += quantity
else
@approvvigionamenti[type] = quantity
end
end
def use_supplies(type, quantity)
puts "----- Azione Approvvigionamento -----"
if @approvvigionamenti[type] != nil && @approvvigionamenti[type] > quantity
puts "Utilizzo di #{quantity} di #{type} dal magazzino di approvvigionamento."
@approvvigionamenti[type] -= quantity
else
puts "Errore di Approvvigionamento: Quantità insufficiente di #{type} nel magazzino di approvvigionamento."
end
end
def report_supplies
puts "----- Rapporto Approvvigionamento -----"
if @approvvigionamenti.keys.length > 0
@approvvigionamenti.each do |type, quantity|
puts "#{type} disponibile: #{quantity} unità"
end
else
puts "Il magazzino di approvvigionamento è vuoto."
end
end
end
classe SerbatoioDiCarburante
attr_accessor :carburante
def initialize
@carburante = 0
end
def get_fuel_levels
@carburante
end
def load_fuel(quantity)
puts "----- Azione Carburante -----"
puts "Caricamento di #{quantity} unità di carburante nel serbatoio."
@carburante += quantity
end
def use_fuel(quantity)
puts "----- Azione Carburante -----"
puts "Utilizzo di #{quantity} unità di carburante dal serbatoio."
@carburante -= quantity
end
def report_fuel
puts "----- Rapporto Carburante -----"
puts "#{@carburante} unità di carburante disponibili."
end
end
classe Razzi
def initialize(serbatoio_di_carburante)
@serbatoio_di_carburante_collegato = serbatoio_di_carburante
end
def activate_thrusters
puts "----- Azione Razzi -----"
if @serbatoio_di_carburante_collegato.get_fuel_levels >= 10
puts "Azione di spinta riuscita."
@serbatoio_di_carburante_collegato.use_fuel(10)
else
puts "Errore Razzi: Carburante insufficiente disponibile."
end
end
endCi sono molti cambiamenti, il programma ora appare sicuramente migliore. Ora la nostra classe StazioneSpaziale è diventata, più che altro, un contenitore in cui si avviano operazioni per parti dipendenti, inclusi un insieme di sensori, un sistema di fornitura di materiali di consumo, un serbatoio di carburante e razzi.
Ora per ogni variabile c'è una classe corrispondente: Sensori; MagazzinoDiApprovvigionamento; SerbatoioDiCarburante; Razzi.
In questa versione del codice ci sono alcune modifiche importanti. Il fatto è che le singole funzioni non solo sono incapsulate in classi proprie, ma sono organizzate in modo tale da diventare prevedibili e coerenti. Raggruppiamo elementi simili per funzionalità, seguendo il principio di coesione. Ora, se sarà necessario modificare il funzionamento del sistema, passando da una struttura hash a un array, basterà utilizzare la classe SupplyHold, senza dover toccare altri moduli. In questo modo, se l'ufficiale responsabile della logistica apporta modifiche alla propria sezione, gli altri elementi della stazione rimarranno intatti. La classe SpaceStation, a tal proposito, non si renderà nemmeno conto dei cambiamenti.
I nostri ufficiali che lavorano sulla stazione spaziale sono probabilmente contenti delle modifiche, poiché possono richiedere quelle di cui hanno bisogno. Si noti che nel codice ci sono metodi come report_supplies e report_fuel, contenuti nelle classi SupplyHold e FuelTank. Cosa succederebbe se la Terra chiedesse di modificare il modo in cui vengono generati i report? Sarebbe necessario modificare entrambe le classi, SupplyHold e FuelTank. E se fosse necessario cambiare il modo in cui il carburante e i materiali di consumo vengono forniti? Probabilmente si dovrebbe ri-modificare tutte le stesse classi. E questo violerebbe già il principio SRP. Correggiamo questo.
class StazioneSpaziale
attr_reader :sensori, :magazzino_forneimenti, :reporter_forneimenti,
:serbatoio_carburante, :reporter_carburante, :razzi
def initialize
@sensori = Sensori.new
@magazzino_forneimenti = MagazzinoForneimenti.new
@reporter_forneimenti = ReporterForneimenti.new(@magazzino_forneimenti)
@serbatoio_carburante = SerbatoioCarburante.new
@reporter_carburante = ReporterCarburante.new(@serbatoio_carburante)
@razzi = Razzi.new(@serbatoio_carburante)
end
end
class Sensori
def run_sensors
puts "----- Azione Sensore -----"
puts "Esecuzione dei sensori!"
end
end
class MagazzinoForneimenti
attr_accessor :fornimenti
attr_reader :reporter
def initialize
@fornimenti = {}
end
def get_supplies
@fornimenti
end
def load_supplies(tipo, quantità)
puts "----- Azione Fornitura -----"
puts "Caricamento di #{quantità} unità di #{tipo} nel magazzino fornimenti."
if @fornimenti[tipo]
@fornimenti[tipo] += quantità
else
@fornimenti[tipo] = quantità
end
end
def use_supplies(tipo, quantità)
puts "----- Azione Fornitura -----"
if @fornimenti[tipo] != nil && @fornimenti[tipo] > quantità
puts "Utilizzo di #{quantità} di #{tipo} dal magazzino fornimenti."
@fornimenti[tipo] -= quantità
else
puts "Errore Fornitura: Fornimenti insufficienti di #{tipo} nel magazzino."
end
end
end
class SerbatoioCarburante
attr_accessor :carburante
attr_reader :reporter
def initialize
@carburante = 0
end
def get_fuel_levels
@carburante
end
def load_fuel(quantità)
puts "----- Azione Carburante -----"
puts "Caricamento di #{quantità} unità di carburante nel serbatoio."
@carburante += quantità
end
def use_fuel(quantità)
puts "----- Azione Carburante -----"
puts "Utilizzo di #{quantità} unità di carburante dal serbatoio."
@carburante -= quantità
end
end
class Razzi
FUEL_PER_THRUST = 10
def initialize(serbatoio_carburante)
@linked_fuel_tank = serbatoio_carburante
end
def activate_thrusters
puts "----- Azione Razzetti -----"
if @linked_fuel_tank.get_fuel_levels >= FUEL_PER_THRUST
puts "Azione di spinta riuscita."
@linked_fuel_tank.use_fuel(FUEL_PER_THRUST)
else
puts "Errore Razzetti: Carburante insufficiente disponibile."
end
end
end
class Reporter
def initialize(item, tipo)
@linked_item = item
@type = tipo
end
def report
puts "----- #{ @type.capitalize} Rapporto -----"
end
end
class ReporterCarburante < Reporter
def initialize(item)
super(item, "carburante")
end
def report
super
puts "#{@linked_item.get_fuel_levels} unità di carburante disponibili."
end
end
class ReporterForneimenti < Reporter
def initialize(item)
super(item, "fornitura")
end
def report
super
if @linked_item.get_supplies.keys.length > 0
@linked_item.get_supplies.each do |tipo, quantità|
puts "#{tipo} disponibile: #{quantità} unità"
end
else
puts "Il magazzino fornimenti è vuoto."
end
end
end
iss = StazioneSpaziale.new
iss.sensori.run_sensors
# ----- Azione Sensore -----
# Esecuzione dei sensori!
iss.magazzino_forneimenti.use_supplies("parti", 2)
# ----- Azione Fornitura -----
# Errore Fornitura: Fornimenti insufficienti di parti nel magazzino.
iss.magazzino_forneimenti.load_supplies("parti", 10)
# ----- Azione Fornitura -----
# Caricamento di 10 unità di parti nel magazzino fornimenti.
iss.magazzino_forneimenti.use_supplies("parti", 2)
# ----- Azione Fornitura -----
# Utilizzo di 2 di parti dal magazzino fornimenti.
iss.reporter_forneimenti.report
# ----- Rapporto Forniture -----
# parti disponibili: 8 unità
iss.razzi.activate_thrusters
# ----- Azione Razzetti -----
# Errore Razzetti: Carburante insufficiente disponibile.
iss.serbatoio_carburante.load_fuel(100)
# ----- Azione Carburante -----
# Caricamento di 100 unità di carburante nel serbatoio.
iss.razzi.activate_thrusters
# ----- Azione Razzetti -----
# Azione di spinta riuscita.
# ----- Azione Carburante -----
# Utilizzo di 10 unità di carburante dal serbatoio.
iss.reporter_carburante.report
# ----- Rapporto Carburante -----
# 90 unità di carburante disponibili.In questa, l'ultima versione del programma, i compiti sono stati suddivisi in due nuove classi, FuelReporter e SupplyReporter. Entrambi sono sottoclassi della classe Reporter. Inoltre, abbiamo aggiunto variabili di istanza alla classe SpaceStation per poter inizializzare il sottotipo necessario, se necessario. Ora, se la Terra decide di cambiare qualcosa, apporteremo modifiche alle sottoclassi e non alla classe principale.
Certo, 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 qui tutto appare logico, e apportare modifiche non sarà particolarmente difficile: modificare il codice di un oggetto non influenzerà troppo un altro.
In questo modo, abbiamo creato un codice modulare, in cui i compiti di ciascun oggetto/classe sono chiaramente definiti. Lavorare con questo codice non è un problema e la sua manutenzione sarà un compito semplice. Abbiamo trasformato l'intero "oggetto divino" in SRP.
Skillbox consiglia:
- Corso pratico di due anni .
- Corso online .
- Corso pratico annuale .
Fonte: habr.com
