D'apres Sandi Metz' :
- Une classe ne doit pas dépasser 100 lignes de code.
- Une méthode ne doit pas dépasser 5 lignes de code.
- Une méthode ne doit pas avoir plus de 4 paramètres. ¹
- Un controlleur doit instancier 1 seul objet. ²
¹ Hash comprit
² Utiliser le pattern Facade ou Présenteur
Une seule responsabilité pour une classe ou une méthode.
Prenons l'exemple d'un module qui compile et imprime un rapport. Imaginons que ce module peut changer pour deux raisons. D'abord, le contenu du rapport peut changer. Ensuite, le format du rapport peut changer. Ces deux choses changent pour des causes différentes; l'une substantielle, et l'autre cosmétique. Le principe de responsabilité unique dit que ces deux aspects du problème ont deux responsabilités distinctes, et devraient donc être dans des classes ou des modules séparés. Ce serait une mauvaise conception de coupler ces deux choses dans une même classe.
La raison pour laquelle il est important de garder une classe axée sur une seule préoccupation est que cela rend la classe plus robuste. En continuant avec l'exemple précédent, s'il y a un changement dans le processus de compilation du rapport, il y a un plus grand danger que le code d'impression se casse si elle fait partie de la même classe.
La responsabilité est définie comme une tâche assignée à un acteur unique2
Une entité applicative (class, method, module ...) doit être ouverte à l'extension, mais fermée à la modification.
En programmation orientée objet, le principe ouvert/fermé (open/closed principle) affirme qu'une classe doit être à la fois ouverte (à l'extension) et fermée (à la modification). Il correspond au « O » de l'acronyme SOLID. « Ouverte » signifie qu'elle a la capacité d'être étendue. « Fermée » signifie qu'elle ne peut être modifiée que par extension, sans modification de son code source.
L'idée est qu'une fois qu'une classe a été approuvée via des revues de code, des tests unitaires et d'autres procédures de qualification, elle ne doit plus être modifiée mais seulement étendue.
En pratique, le principe ouvert/fermé oblige à faire bon usage de l'abstraction et du polymorphisme.
Une instance de type T doit pouvoir être remplacée par une instance de type G, tel que G sous-type de T, sans que cela ne modifie la cohérence du programme.
Le principe de Liskov impose des restrictions sur les signatures sur la définition des sous-types :
- Contravariance des arguments de méthode dans le sous-type.
- Covariance du type de retour dans le sous-type.
- Aucune nouvelle exception ne doit être générée par la méthode du sous-type, sauf si celles-ci sont elles-mêmes des sous-types des exceptions levées par la méthode du supertype.
On définit également un certain nombre de conditions comportementales (voir la section Conception par contrat).
Le principe de substitution de Liskov est étroitement relié à la méthodologie de programmation par contrat aboutissant à des restrictions qui spécifient la manière dont les contrats peuvent interagir avec les mécanismes d'héritage :
- Les préconditions ne peuvent pas être renforcées dans une sous-classe. Cela signifie que vous ne pouvez pas avoir une sous-classe avec des préconditions plus fortes que celles de sa superclasse ;
- Les postconditions ne peuvent pas être affaiblies dans une sous-classe. Cela signifie que vous ne pouvez pas avoir une sous-classe avec des postconditions plus faibles que celles de sa superclasse.
De plus, le principe de substitution de Liskov implique que des exceptions d'un type nouveau ne peuvent pas être levées par des méthodes de la sous-classe, sauf si ces exceptions sont elles-mêmes des sous-types des exceptions lancées par les méthodes de la superclasse.
Une fonction utilisant la connaissance de la hiérarchie de classe viole le principe car elle utilise une référence à la classe de base, mais doit aussi avoir connaissance des sous-classes. Une telle fonction viole le principe ouvert/fermé car elle doit être modifiée quand on crée une classe dérivée de la classe de base.
Plusieurs interfaces spécifiques pour chaque client plutôt qu'une seule interface générale. Aucun client ne devrait dépendre de méthodes qu'il n'utilise pas. Il faut donc diviser les interfaces volumineuses en plus petites plus spécifiques, de sorte que les clients n'ont accès qu'aux méthodes intéressantes pour eux. Ces interfaces rétrécies sont également appelés interfaces de rôle. Tout ceci est destiné à maintenir un système à couplage faible, donc plus facile à refactoriser.
Il faut dépendre des abstractions, pas des implémentations Les deux assertions de ce principe sont :
- Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d'abstractions.
- Les abstractions ne doivent pas dépendre des détails. Les détails doivent dépendre des abstractions.
D'après le Gang des quatres (Gang of Four) il existe plusieurs types de patterns dans la conception de logiciels orientée objet :
Le premier type de motif est le motif de création. Les motifs de création permettent d'instancier des objets individuels ou des groupes d'objets liés. Il existe cinq modèles de ce type :
Le modèle d'usine abstraite est utilisé pour fournir à un client un ensemble d'objets liés ou dépendants. Les "familles" d'objets créées par la fabrique sont déterminées au moment de l'exécution.
Le modèle builder est utilisé pour créer des objets complexes avec des parties constitutives qui doivent être créées dans le même ordre ou en utilisant un algorithme spécifique. Une classe externe contrôle l'algorithme de construction.
Le modèle d'usine est utilisé pour remplacer les constructeurs de classe, en abstraction du processus de génération d'objets afin que le type de l'objet instancié puisse être déterminé au moment de l'exécution.
Le modèle prototype est utilisé pour instancier un nouvel objet en copiant toutes les propriétés d'un objet existant, créant ainsi un clone indépendant. Cette pratique est particulièrement utile lorsque la construction d'un nouvel objet est inefficace.
Le modèle singleton garantit qu'un seul objet d'une classe particulière est créé. Toutes les autres références aux objets de la classe singleton se rapportent à la même instance sous-jacente.
Le deuxième type de modèle de conception est le modèle structurel. Les modèles structurels permettent de définir les relations entre les classes ou les objets.
Le modèle d'adaptateur est utilisé pour fournir un lien entre deux types autrement incompatibles en enveloppant l'"adaptateur" avec une classe qui prend en charge l'interface requise par le client.
Le modèle de pont est utilisé pour séparer les éléments abstraits d'une classe des détails de l'implémentation, fournissant ainsi le moyen de remplacer les détails de l'implémentation sans modifier l'abstraction.
Le modèle composite est utilisé pour créer des structures arborescentes hiérarchiques et récursives d'objets apparentés où tout élément de la structure peut être accessible et utilisé de manière standard.
Le motif décorateur est utilisé pour étendre ou modifier la fonctionnalité des objets en cours d'exécution en les enveloppant dans un objet d'une classe décorateur. Cela offre une alternative flexible à l'utilisation de l'héritage pour modifier le comportement.
Le modèle de façade est utilisé pour définir une interface simplifiée vers un sous-système plus complexe.
Le modèle de poids mouche est utilisé pour réduire l'utilisation de la mémoire et des ressources pour des modèles complexes contenant plusieurs centaines, milliers ou centaines de milliers d'objets similaires.
Le modèle de proxy est utilisé pour fournir un objet de substitution ou de remplacement, qui fait référence à un objet sous-jacent. Le proxy fournit la même interface publique que la classe de sujets sous-jacente, ajoutant un niveau d'indirection en acceptant les requêtes d'un objet client et en les transmettant à l'objet sujet réel si nécessaire.
Le dernier type de modèle de conception est le modèle comportemental. Les modèles comportementaux définissent les modes de communication entre les classes et les objets.
Le modèle de chaîne de responsabilité est utilisé pour traiter des demandes variées, chacune d'entre elles pouvant être traitée par un gestionnaire différent.
Le modèle de commande est utilisé pour exprimer une demande, y compris l'appel à effectuer et tous ses paramètres requis, dans un objet de commande. La commande peut ensuite être exécutée immédiatement ou conservée pour une utilisation ultérieure.
Le modèle d'interprétation est utilisé pour définir la grammaire des instructions qui font partie d'une langue ou d'une notation, tout en permettant d'étendre facilement la grammaire.
Le modèle d'itérateur est utilisé pour fournir une interface standard permettant de parcourir une collection d'éléments dans un objet global sans avoir besoin de comprendre sa structure sous-jacente.
Le modèle de médiateur est utilisé pour réduire le couplage entre les classes qui communiquent entre elles. Au lieu que les classes communiquent directement, et nécessitent donc une connaissance de leur mise en œuvre, les classes envoient des messages via un objet médiateur.
Le modèle memento est utilisé pour capturer l'état actuel d'un objet et le stocker de manière à ce qu'il puisse être restauré ultérieurement sans enfreindre les règles d'encapsulation.
Le modèle observateur est utilisé pour permettre à un objet de publier les changements de son état. Les autres objets s'abonnent pour être immédiatement informés de toute modification.
Le modèle d'état est utilisé pour modifier le comportement d'un objet lorsque son état interne change. Le modèle permet à la classe d'un objet de changer apparemment au moment de l'exécution.
Le modèle de stratégie est utilisé pour créer une famille d'algorithmes interchangeables à partir de laquelle le processus requis est choisi au moment de l'exécution.
Le modèle de la méthode des modèles est utilisé pour définir les étapes de base d'un algorithme et permettre la modification de la mise en œuvre des différentes étapes.
Le modèle de visiteur est utilisé pour séparer un ensemble relativement complexe de classes de données structurées de la fonctionnalité qui peut être exécutée sur les données que l'on veut traiter.
Exemples en crystal : https://github.com/crystal-community/crystal-patterns
Problèmatique:
class Vehicle
def run
refill
load
end
end
class Car < Vehicle
def load
# load passengers
end
end
class Truck < Vehicle
def load
# load cargo
end
end
class PetrolCar < Car
def refill
# refill with fuel
end
end
class ElectricCar < Car
def refill
# refill with electricity
end
end
class PetrolTruck < Truck
def refill
# refill with fuel (code duplication!)
end
end
class ElectricTruck < Truck
def refill
# refill with electricity (code duplication!)
end
end
L'héritage se complexifie au fur et à mesure que l'on créé des enfants.
module Vehicle
def run
refill
load
end
end
module Truck
def load
# load cargo
end
end
module Car
def load
# load passengers
end
end
module ElectricEngine
def refill
# refill with electricity
end
end
module PetrolEngine
def refill
# refill with petrol
end
end
class PetrolCar
include Vehicle
include Car
include PetrolEngine
end
class ElectricCar
include Vehicle
include Car
include ElectricEngine
end
class PetrolTruck
include Vehicle
include Truck
include PetrolEngine
end
class ElectricTruck
include Vehicle
include Truck
include ElectricEngine
end
Le problème avec le système de mixins c'est que que les methodes de chaque mixins peuvent s'interférer en portant le même nom.
class Vehicle
def initialize(engine:, body:)
@engine = engine
@body = body
end
def run
@engine.refill
@body.load
end
end
class ElectricEngine
def refill
# refill with electricity
end
end
class PetrolEngine
def refill
# refill with petrol
end
end
class TruckBody
def load
# load cargo
end
end
class CarBody
def load
# load passengers
end
end
petrol_car = Vehicle.new(engine: PetrolEngine.new, body: CarBody.new)
electric_car = Vehicle.new(engine: ElectricEngine.new, body: CarBody.new)
petrol_truck = Vehicle.new(engine: PetrolEngine.new, body: TruckBody.new)
electric_truck = Vehicle.new(engine: ElectricEngine.new, body: TruckBody.new)
Ici la composition permet :
- Un faible couplage (un objet -> une logique)
- Une grande cohésion (permet de garder une logique commune)
- Une plus grande maintenabilité
- Une simplification à l'extension
- Le guide Ruby ³
- Le guide Ruby on Rails ⁴
- Le guide de Bonne Pratiques ⁵
- Le guide de Bonne Pratiques Css ⁶
³: D'après porecreat
⁴: D'après bbatsov
⁵: D'après Elevated Abstractions via Elevated Abstractions
⁶: D'après csswizardry