Scriviamo codice flessibile utilizzando SOLID

Scriviamo codice flessibile utilizzando SOLID

Dall'autore: pubblicato per voi l'articolo di Severin Pérez 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 «Sviluppatore mobile PRO».

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:

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
end

In 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
end

Ci 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:

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster