
Du traducteur : nous avons publié pour vous 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 .
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.
- (principe d'ouverture/fermeture). Les classes et autres éléments doivent être ouverts à l'extension, mais fermés à la modification.
- (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.
- (principe de séparation des interfaces). Les entités logicielles ne doivent pas dépendre des méthodes qu'elles n'utilisent pas.
- (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
endEn 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
endIl 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 :
- Cours pratique de deux ans .
- Cours en ligne .
- Cours pratique d'un an .
Source : habr.com
