Nous écrivons du code flexible en utilisant SOLID

Nous écrivons du code flexible en utilisant SOLID

Du traducteur : nous avons publié pour vous un article de Séverine Pérez sur l'utilisation des principes SOLID en programmation. Les informations de l'article seront utiles tant aux débutants qu'aux programmeurs expérimentés.

Si vous êtes développeur, vous avez probablement entendu parler des principes SOLID. Ils permettent aux programmeurs d'écrire un code propre, bien structuré et facilement maintenable. Il convient de noter qu'en programmation, il existe plusieurs approches sur la manière d'effectuer correctement un travail donné. Différents spécialistes ont des idées et une compréhension différentes du 'bon chemin', tout dépend de l'expérience de chacun. Néanmoins, les idées proclamées dans SOLID sont acceptées par presque tous les représentants de la communauté informatique. Elles sont devenues le point de départ pour l'émergence et le développement de nombreuses bonnes méthodes de gestion du développement.

Voyons donc ce que sont les principes SOLID et comment ils nous aident.

Skillbox recommande : Cours pratique « Développeur mobile PRO ».

Rappelons-le : pour tous les lecteurs de « Habr » — une réduction de 10 000 roubles lors de l'inscription à tout cours Skillbox avec le code promo « Habr ».

Qu'est-ce que SOLID?

Ce terme est un acronyme, chaque lettre représentant le début du nom d'un principe spécifique :

  • SSingle Responsibility Principle (principe de responsabilité unique). Un module ne doit avoir qu'une seule raison de changer.
  • Le OOpen/Closed Principle (principe d'ouverture/fermeture). Les classes et autres éléments doivent être ouverts à l'extension, mais fermés à la modification.
  •  Le LLiskov Substitution Principle (principe de substitution de Liskov). Les fonctions qui utilisent un type de base doivent avoir la possibilité d'utiliser des sous-types de ce type de base sans en avoir connaissance.
  • Le IInterface Segregation Principle (principe de séparation des interfaces). Les entités logicielles ne doivent pas dépendre des méthodes qu'elles n'utilisent pas.
  • Le DDependency Inversion Principle (principe d'inversion des dépendances). Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau.

Principe de responsabilité unique

 
Le principe de responsabilité unique (SRP) stipule que chaque classe ou module dans un programme doit être responsable d'une seule partie de la fonctionnalité de ce programme. De plus, les éléments de cette responsabilité doivent être rattachés à leur classe et non répartis entre des classes non connectées. Le développeur et principal évangéliste du SRP, Robert C. Martin, décrit la responsabilité comme la raison des changements. Ce terme a été initialement proposé comme l'un des éléments de son ouvrage « Principes de la conception orientée objet ». Le concept intègre également de nombreux aspects de la cohésion, qui avait été définie précédemment par Tom DeMarco.

Le concept inclut également plusieurs notions formulées par David Parnas. Les deux principales sont l'encapsulation et le masquage d'informations. Parnas soutenait que la division d'un système en modules distincts ne devait pas reposer sur l'analyse de diagrammes de flux 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 donnait un exemple intéressant avec des cadres supérieurs d'une entreprise (COO, CTO, CFO), chacun d'eux utilisant des logiciels spécifiques pour des objectifs différents. En fin de compte, chacun d'eux peut apporter des modifications au logiciel sans affecter les intérêts des autres cadres.

Objet divin

Comme d'habitude, la meilleure façon d'apprendre le SRP est de le voir en action. Examinons une partie d'un programme qui NE respecte PAS le principe de responsabilité unique. Il s'agit d'un code Ruby décrivant le comportement et les attributs d'une station spatiale.

Examinez l'exemple et essayez de déterminer ce qui suit :
Les 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 "----- Action des Capteurs -----"
    puts "Activation des capteurs!"
  end
 
  def load_supplies(type, quantity)
    puts "----- Action de Fourniture -----"
    puts "Chargement de #{quantity} unités de #{type} dans la soute."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Action de Fourniture -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Utilisation de #{quantity} de #{type} de la soute."
      @supplies[type] -= quantity
    else
      puts "Erreur de Fourniture: Quantité insuffisante de #{type} dans la soute."
    end
  end
 
  def report_supplies
    puts "----- Rapport de Fourniture -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} disponible: #{quantity} unités"
      end
    else
      puts "La soute est vide."
    end
  end
 
  def load_fuel(quantity)
    puts "----- Action de Carburant -----"
    puts "Chargement de #{quantity} unités de carburant dans le réservoir."
    @fuel += quantity
  end
 
  def report_fuel
    puts "----- Rapport de Carburant -----"
    puts "#{@fuel} unités de carburant disponibles."
  end
 
  def activate_thrusters
    puts "----- Action des Propulseurs -----"
    if @fuel >= 10
      puts "Action de poussée réussie."
      @fuel -= 10
    else
      puts "Erreur de Propulseur: Carburant insuffisant."
    end
  end
end

En réalité, notre station spatiale est non fonctionnelle (je ne pense pas recevoir d'appel de la NASA dans un avenir proche), mais il y a beaucoup à analyser ici.

Ainsi, la classe SpaceStation a plusieurs responsabilités (ou tâches) différentes. Toutes peuvent être classées par types:

  • capteurs;
  • fournitures (consommables);
  • carburant;
  • propulseurs.

Bien que personne parmi le personnel de la station ne soit défini dans la classe, nous pouvons facilement imaginer qui est responsable de quoi. Il est probable qu'un scientifique contrôle les capteurs, qu'un logisticien s'occupe des fournitures, qu'un ingénieur gère les réserves de carburant et qu'un pilote contrôle les propulseurs.

Pouvons-nous dire que ce programme ne respecte pas le SRP ? Oui, bien sûr. Mais la classe SpaceStation est un « objet divin » typique, qui sait tout et fait tout. C'est le principal anti-modèle en programmation orientée objet. Pour un débutant, de tels objets sont extrêmement difficiles à maintenir. Pour l'instant, le programme est très simple, oui, mais imaginez ce qui se passera si nous ajoutons de nouvelles fonctionnalités. Peut-être que notre station spatiale aura besoin d'un service médical ou d'une salle de réunion. Plus il y aura de fonctionnalités, plus la SpaceStation deviendra complexe. Et comme cet objet sera relié à d'autres, l'entretien de l'ensemble du complexe deviendra encore plus compliqué. En fin de compte, nous risquons de perturber par exemple les accélérateurs. Si un chercheur demande des modifications au travail avec les capteurs, cela pourrait affecter les systèmes de communication de la station.

Violer le principe SRP peut offrir une victoire tactique à court terme, mais en fin de compte, nous « perdrons la guerre », entretenir un tel monstre à l'avenir deviendra très compliqué. Il est préférable de diviser le programme en sections de code distinctes, chacune responsable d'une opération spécifique. En gardant cela à l'esprit, modifions la classe SpaceStation.

Distribuons les responsabilités

Plus haut, nous avons défini quatre types d'opérations contrôlées par la classe SpaceStation. Lors du refactoring, nous en tiendrons compte. Le code mis à jour respecte mieux le 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 "----- Action des capteurs -----"
    puts "Fonctionnement des capteurs !"
  end
end
 
class SupplyHold
  attr_accessor :supplies
 
  def initialize
    @supplies = {}
  end
 
  def load_supplies(type, quantity)
    puts "----- Action d'approvisionnement -----"
    puts "Chargement de #{quantity} unités de #{type} dans le conteneur d'approvisionnement."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Action d'approvisionnement -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Utilisation de #{quantity} de #{type} dans le conteneur d'approvisionnement."
      @supplies[type] -= quantity
    else
      puts "Erreur d'approvisionnement : Quantité insuffisante de #{type} dans le conteneur d'approvisionnement."
    end
  end
 
  def report_supplies
    puts "----- Rapport d'approvisionnement -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} disponible : #{quantity} unités"
      end
    else
      puts "Le conteneur d'approvisionnement est vide."
    end
  end
end
 
class FuelTank
  attr_accessor :fuel
 
  def initialize
    @fuel = 0
  end
 
  def get_fuel_levels
    @fuel
  end
 
  def load_fuel(quantity)
    puts "----- Action de carburant -----"
    puts "Chargement de #{quantity} unités de carburant dans le réservoir."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Action de carburant -----"
    puts "Utilisation de #{quantity} unités de carburant du réservoir."
    @fuel -= quantity
  end
 
  def report_fuel
    puts "----- Rapport de carburant -----"
    puts "#{@fuel} unités de carburant disponibles."
  end
end
 
class Thrusters
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Action des propulseurs -----"
    if @linked_fuel_tank.get_fuel_levels >= 10
      puts "Action de poussée réussie."
      @linked_fuel_tank.use_fuel(10)
    else
      puts "Erreur des propulseurs : Carburant insuffisant disponible."
    end
  end
end

Il y a eu beaucoup de changements, le programme a maintenant un aspect nettement amélioré. À présent, notre classe SpaceStation est plutôt un conteneur où sont initiées des opérations pour les parties dépendantes, y compris un ensemble de capteurs, un système d'approvisionnement en matériaux, un réservoir de carburant, et des propulseurs.

Pour chacune des variables, il existe maintenant une classe correspondante : Sensors ; SupplyHold ; FuelTank ; Thrusters.

Dans cette version du code, il y a plusieurs changements importants. Les fonctions 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 des éléments similaires en termes de fonctionnalité pour suivre le principe de cohésion. Maintenant, si nous devons modifier le fonctionnement du système, en passant d'une structure de hachage à un tableau, il suffit d'utiliser la classe SupplyHold, sans toucher aux autres modules. Ainsi, si l'officier responsable de la logistique modifie quelque chose dans sa section, les autres éléments de la station resteront intacts. De plus, la classe SpaceStation ne sera même pas au courant des changements.

Nos officiers travaillant à la station spatiale sont probablement contents des changements, car ils peuvent demander ceux qui leur sont nécessaires. Notez qu'il existe des méthodes dans le code, telles que report_supplies et report_fuel, contenues dans les classes SupplyHold et FuelTank. Que se passera-t-il si la Terre demande de modifier la manière de générer des rapports ? Il faudra modifier les deux classes, SupplyHold et FuelTank. Et que faire s'il faut changer la façon de livrer le carburant et les consommables ? Il faudra probablement encore modifier toutes ces classes. Cela représenterait une violation du principe SRP. Corrigeons cela.

class StationSpatiale
  attr_reader :capteurs, :supply_hold, :reporteur_de_provisions,
              :reservoir_de_carburant, :reporteur_de_carburant, :propulseurs
 
  def initialize
    @capteurs = Capteurs.new
    @supply_hold = SupplyHold.new
    @reporteur_de_provisions = SupplyReporter.new(@supply_hold)
    @reservoir_de_carburant = ReservoirDeCarburant.new
    @reporteur_de_carburant = FuelReporter.new(@reservoir_de_carburant)
    @propulseurs = Propulseurs.new(@reservoir_de_carburant)
  end
end
 
class Capteurs
  def run_sensors
    puts "----- Action des capteurs -----"
    puts "Exécution des capteurs !"
  end
end
 
class SupplyHold
  attr_accessor :provisions
  attr_reader :reporteur
 
  def initialize
    @provisions = {}
  end
 
  def get_supplies
    @provisions
  end
 
  def load_supplies(type, quantity)
    puts "----- Action de fourniture -----"
    puts "Chargement de #{quantity} unités de #{type} dans le compartiment de stockage."
    
    if @provisions[type]
      @provisions[type] += quantity
    else
      @provisions[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Action de fourniture -----"
    if @provisions[type] != nil && @provisions[type] > quantity
      puts "Utilisation de #{quantity} de #{type} du compartiment de stockage."
      @provisions[type] -= quantity
    else
      puts "Erreur de fourniture: Quantité insuffisante de #{type} dans le compartiment de stockage."
    end
  end
end
 
class ReservoirDeCarburant
  attr_accessor :carburant
  attr_reader :reporteur
 
  def initialize
    @carburant = 0
  end
 
  def get_fuel_levels
    @carburant
  end
 
  def load_fuel(quantity)
    puts "----- Action de carburant -----"
    puts "Chargement de #{quantity} unités de carburant dans le réservoir."
    @carburant += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Action de carburant -----"
    puts "Utilisation de #{quantity} unités de carburant du réservoir."
    @carburant -= quantity
  end
end
 
class Propulseurs
  FUEL_PER_THRUST = 10
 
  def initialize(reservoir_de_carburant)
    @réservoir_de_carburant_lié = reservoir_de_carburant
  end
 
  def activate_thrusters
    puts "----- Action des propulseurs -----"
    
    if @réservoir_de_carburant_lié.get_fuel_levels >= FUEL_PER_THRUST
      puts "Action de propulsion réussie."
      @réservoir_de_carburant_lié.use_fuel(FUEL_PER_THRUST)
    else
      puts "Erreur de propulseur: Carburant insuffisant disponible."
    end
  end
end
 
class Reporter
  def initialize(item, type)
    @élément_lié = item
    @type = type
  end
 
  def report
    puts "----- Rapport de #{@type.capitalize} -----"
  end
end
 
class FuelReporter < Reporter
  def initialize(item)
    super(item, "carburant")
  end
 
  def report
    super
    puts "#{@élément_lié.get_fuel_levels} unités de carburant disponibles."
  end
end
 
class SupplyReporter  0
      @élément_lié.get_supplies.each do |type, quantity|
        puts "#{type} disponible: #{quantity} unités"
      end
    else
      puts "Le compartiment de stockage est vide."
    end
  end
end
 
iss = StationSpatiale.new
 
iss.capteurs.run_sensors
  # ----- Action des capteurs -----
  # Exécution des capteurs !
 
iss.supply_hold.use_supplies("pièces", 2)
  # ----- Action de fourniture -----
  # Erreur de fourniture: Quantité insuffisante de pièces dans le compartiment de stockage.
iss.supply_hold.load_supplies("pièces", 10)
  # ----- Action de fourniture -----
  # Chargement de 10 unités de pièces dans le compartiment de stockage.
iss.supply_hold.use_supplies("pièces", 2)
  # ----- Action de fourniture -----
  # Utilisation de 2 de pièces du compartiment de stockage.
iss.supply_reporter.report
  # ----- Rapport de fourniture -----
  # pièces disponibles: 8 unités
 
iss.propulseurs.activate_thrusters
  # ----- Action des propulseurs -----
  # Erreur de propulseur: Carburant insuffisant disponible.
iss.reservoir_de_carburant.load_fuel(100)
  # ----- Action de carburant -----
  # Chargement de 100 unités de carburant dans le réservoir.
iss.propulseurs.activate_thrusters
  # ----- Action des propulseurs -----
  # Action de propulsion réussie.
  # ----- Action de carburant -----
  # Utilisation de 10 unités de carburant du réservoir.
iss.fuel_reporter.report
  # ----- Rapport de carburant -----
# 90 unités de carburant disponibles.

Dans cette dernière version du programme, les responsabilités ont été réparties en deux nouvelles classes, FuelReporter et SupplyReporter. Elles sont toutes deux des sous-classes de la classe Reporter. De plus, nous avons ajouté des variables d'instance à la classe SpaceStation afin d'initialiser le sous-classe nécessaire si besoin. Maintenant, si la Terre décide de changer quelque chose d'autre, nous apporterons des modifications aux sous-classes, et non à la classe principale.

Bien sûr, certaines classes dépendent encore les unes des autres. Par exemple, l'objet SupplyReporter dépend de SupplyHold, tandis que FuelReporter dépend de FuelTank. Évidemment, les propulseurs doivent être liés au réservoir de carburant. Mais tout cela semble logique, et apporter des modifications ne sera pas particulièrement difficile — modifier le code d'un objet n'affectera pas beaucoup l'autre.

Ainsi, nous avons créé un code modulaire, où les responsabilités de chaque objet/classe sont clairement définies. Travailler avec ce type de code n'est pas un problème, sa maintenance sera une tâche simple. Nous avons transformé tout l'« objet divin » en SRP.

Skillbox recommande :

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster