Écrire du code flexible avec SOLID

Écrire du code flexible avec SOLID

Du traducteur: publié pour vous article de Séverin Pérez sur l'utilisation des principes SOLID dans la programmation. Les informations contenues dans l'article seront utiles aux programmeurs débutants et expérimentés.

Si vous êtes passionné de développement, vous avez probablement entendu parler des principes SOLID. Ils permettent au programmeur d'écrire du code propre, bien structuré et facilement maintenable. Il convient de noter qu'en programmation, il existe plusieurs approches pour effectuer correctement un travail particulier. Différents spécialistes ont des idées et une compréhension différentes de la « bonne voie » ; tout dépend de l’expérience de chacun. Cependant, les idées proclamées dans SOLID sont acceptées par presque tous les représentants de la communauté informatique. Ils sont devenus le point de départ de l’émergence et du développement de nombreuses bonnes pratiques de gestion du développement.

Comprenons quels sont les principes SOLID et comment ils nous aident.

Skillbox vous recommande : Cours pratique "Développeur mobile PRO".

Nous rappelons: pour tous les lecteurs de "Habr" - une remise de 10 000 roubles lors de l'inscription à n'importe quel cours Skillbox en utilisant le code promotionnel "Habr".

Qu’est-ce que SOLIDE ?

Ce terme est une abréviation, chaque lettre du terme est le début du nom d'un principe précis :

  • SPrincipe de responsabilité unique. Un module peut avoir une et une seule raison de changement.
  • Les OStylo/Principe Fermé (principe ouvert/fermé). Les classes et autres éléments doivent être ouverts à l’extension, mais fermés à la modification.
  •  Les LPrincipe de substitution d'Iskov (Principe de substitution de Liskov). Les fonctions qui utilisent un type de base doivent pouvoir utiliser des sous-types du type de base sans le savoir.
  • Les IPrincipe de séparation des interfaces  (principe de séparation des interfaces). Les entités logicielles ne doivent pas dépendre de méthodes qu’elles n’utilisent pas.
  • Les DPrincipe d'inversion de dépendance (principe d'inversion de dépendance). Les modules des niveaux supérieurs ne devraient pas dépendre des modules des niveaux inférieurs.

Principe de responsabilité unique


Le principe de responsabilité unique (SRP) stipule que chaque classe ou module d'un programme ne doit être responsable que d'une partie des fonctionnalités de ce programme. De plus, les éléments de cette responsabilité devraient être attribués à leur propre classe, plutôt que dispersés dans des classes non liées. Le développeur et évangéliste en chef de SRP, Robert S. Martin, décrit la responsabilité comme la raison du changement. Il a initialement proposé ce terme comme l'un des éléments de son ouvrage « Principes de conception orientée objet ». Le concept intègre une grande partie du modèle de connectivité précédemment défini par Tom DeMarco.

Le concept comprenait également plusieurs concepts formulés par David Parnas. Les deux principaux sont l’encapsulation et la dissimulation d’informations. Parnas a fait valoir que la division d'un système en modules distincts ne devrait pas être basée sur l'analyse de schémas fonctionnels ou de flux d'exécution. Chacun des modules doit contenir une solution spécifique qui fournit un minimum d'informations aux clients.

D'ailleurs, Martin a donné un exemple intéressant avec des cadres supérieurs d'une entreprise (COO, CTO, CFO), dont chacun utilise un logiciel métier spécifique à des fins différentes. En conséquence, chacun d'entre eux peut mettre en œuvre des modifications dans le logiciel sans affecter les intérêts des autres gestionnaires.

Objet divin

Comme toujours, la meilleure façon d’apprendre le SRP est de le voir en action. Examinons une section du programme qui ne suit PAS le principe de responsabilité unique. Il s'agit du code Ruby qui décrit le comportement et les attributs de la station spatiale.

Examinez l'exemple et essayez de déterminer les éléments suivants :
Responsabilités des objets déclarés dans la classe SpaceStation.
Ceux qui pourraient être intéressés par le fonctionnement de la station spatiale.

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

En fait, notre station spatiale est dysfonctionnelle (je ne pense pas que je recevrai un appel de la NASA de sitôt), mais il y a quelque chose à analyser ici.

Ainsi, la classe SpaceStation a plusieurs responsabilités (ou tâches) différentes. Tous peuvent être divisés en types :

  • capteurs;
  • fournitures (consommables);
  • le carburant;
  • accélérateurs.

Même si aucun employé de la station n'est assigné à une classe, on imagine facilement qui est responsable de quoi. Très probablement, le scientifique contrôle les capteurs, le logisticien est responsable de la fourniture des ressources, l'ingénieur est responsable de l'approvisionnement en carburant et le pilote contrôle les boosters.

Pouvons-nous dire que ce programme n’est pas conforme au SRP ? Oui bien sûr. Mais la classe SpaceStation est un « objet divin » typique qui sait tout et fait tout. Il s’agit d’un anti-modèle majeur dans la programmation orientée objet. Pour un débutant, de tels objets sont extrêmement difficiles à entretenir. Jusqu'à présent, le programme est très simple, certes, mais imaginez ce qui se passera si nous ajoutons de nouvelles fonctionnalités. Peut-être que notre station spatiale aura besoin d’une station médicale ou d’une salle de réunion. Et plus il y aura de fonctions, plus SpaceStation grandira. Eh bien, puisque cette installation sera connectée à d’autres, la desserte de l’ensemble du complexe deviendra encore plus complexe. En conséquence, nous pouvons perturber le fonctionnement, par exemple, des accélérateurs. Si un chercheur demande des modifications aux capteurs, cela pourrait très bien affecter les systèmes de communication de la station.

Violer le principe du SRP peut donner une victoire tactique à court terme, mais à la fin, nous « perdrons la guerre » et il deviendra très difficile de maintenir un tel monstre à l'avenir. Il est préférable de diviser le programme en sections de code distinctes, chacune étant chargée d'effectuer une opération spécifique. Comprenant cela, changeons la classe SpaceStation.

Répartissons les responsabilités

Ci-dessus, nous avons défini quatre types d'opérations contrôlées par la classe SpaceStation. Nous les garderons à l’esprit lors de la refactorisation. Le code mis à jour correspond mieux au 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

Il y a beaucoup de changements, le programme est définitivement meilleur maintenant. Aujourd'hui, notre classe SpaceStation est devenue davantage un conteneur dans lequel les opérations sont lancées pour les pièces dépendantes, notamment un ensemble de capteurs, un système d'alimentation en consommables, un réservoir de carburant et des boosters.

Pour chacune des variables, il existe désormais une classe correspondante : Capteurs ; Maintien de l'approvisionnement ; Réservoir d'essence; Propulseurs.

Il y a plusieurs changements importants dans cette version du code. Le fait est que les fonctions individuelles ne sont pas seulement encapsulées dans leurs propres classes, elles sont organisées de manière à devenir prévisibles et cohérentes. Nous regroupons les éléments aux fonctionnalités similaires pour suivre le principe de cohérence. Maintenant, si nous devons changer la façon dont le système fonctionne, en passant d'une structure de hachage à un tableau, utilisez simplement la classe SupplyHold ; nous n'avons pas besoin de toucher à d'autres modules. Ainsi, si le logisticien modifie quelque chose dans sa section, le reste de la station restera intact. Dans ce cas, la classe SpaceStation ne sera même pas au courant des changements.

Nos officiers travaillant sur la station spatiale sont probablement satisfaits des changements car ils peuvent demander ceux dont ils ont besoin. Notez que le code contient des méthodes telles que report_supplies et report_fuel contenues dans les classes SupplyHold et FuelTank. Que se passerait-il si la Terre demandait de changer sa façon de communiquer ? Les deux classes, SupplyHold et FuelTank, devront être modifiées. Que se passe-t-il si vous devez modifier la manière dont le carburant et les consommables sont livrés ? Vous devrez probablement à nouveau changer de classe. Et c’est déjà une violation du principe SRP. Réparons ça.

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.

Dans cette dernière version du programme, les responsabilités ont été divisées en deux nouvelles classes, FuelReporter et SupplyReporter. Ils sont tous deux enfants de la classe Reporter. De plus, nous avons ajouté des variables d'instance à la classe SpaceStation afin que la sous-classe souhaitée puisse être initialisée si nécessaire. Maintenant, si la Terre décide de changer autre chose, alors nous apporterons des modifications aux sous-classes, et non à la classe principale.

Bien entendu, certaines de nos classes dépendent encore les unes des autres. Ainsi, l'objet SupplyReporter dépend de SupplyHold et FuelReporter dépend de FuelTank. Bien entendu, les boosters doivent être connectés au réservoir de carburant. Mais ici, tout semble déjà logique et apporter des modifications ne sera pas particulièrement difficile - modifier le code d'un objet n'affectera pas beaucoup un autre.

Ainsi, nous avons créé un code modulaire où les responsabilités de chacun des objets/classes sont précisément définies. Travailler avec un tel code n'est pas un problème, le maintenir sera une tâche simple. Nous avons converti l’intégralité de « l’objet divin » en SRP.

Skillbox vous recommande :

Source: habr.com

Ajouter un commentaire