jeudi 6 décembre 2012

Principe ouvert/fermé (Open/close principle OCP).

Un petit post pour parler du principe ouvert/fermé (Open/close principle OCP).

D'abord la définition wikipédia :
http://fr.wikipedia.org/wiki/Principe_ouvert/ferm%C3%A9

"En programmation orientée objet, le principe ouvert/fermé affirme qu'une classe doit être à la fois ouverte et fermée. Ouverte signifie qu'elle a la capacité à être étendue. Fermée signifie qu'elle peut être modifiée 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 qualifications, vous ne voulez pas changer la classe mais seulement l'étendre. En pratique, le principe ouvert/fermé fait bon usage de l'abstraction et du polymorphisme."

Suivre l'OCP revient à écrire du code que l'on peut étendre sans avoir à le modifier!
Ça peut sembler incohérent, mais non, il n'y a pas d'erreur dans cette définition.

Contrairement au SRP, l'OCP n'est pas forcement un principe à appliquer à toutes les classes.
Il faut savoir identifier les fonctionnalités amené à souvent évoluer.
A partir de la le principe revient à créer un code qui sera amené à ne plus etre modifié, en faisant une abstraction de se qui évoluera.

Maintenant qu'on à défini l'OCP passons a un exemple, on va proposer une fonctionnalité qui consiste à calculer l'air d'un rectangle.

La classe Rectangle (sans les accesseur pour plus de lisibilité) :
  1. public class Rectangle {
  2.     private double width;
  3.    
  4.     private double height; 
  5.    
  6. }

La classe AreaCalculator avec sa methode de calcul d'air de plusieurs rectangles :
  1. public class AreaCalculator {
  2.    
  3.     public double area(List<Rectangle> shapes){
  4.         double area = 0;
  5.         for (Rectangle rectangle : shapes) {
  6.             area += rectangle.getWidth() * rectangle.getHeight();
  7.         }
  8.         return area;
  9.     }
  10.    
  11. }

Nous voila avec une solution qui devrait bien fonctionner. Mais, à peine quelques jours plus tard on vous demande la possibilité de calculer l'air de cercles en plus des rectangles.
Le client est roi et puis ça n'a rien de compliqué, alors on s'y colle en modifiant la classe AreaCalculator et en ajoutant une classe Circle :
  1. public class Circle {
  2.     private double radius;
  3.    
  4.    
  5. }

  1. public class AreaCalculator {
  2.    
  3.     public double area(List<Object> shapes){
  4.         double area = 0;
  5.         for (Object shape : shapes) {
  6.            
  7.             if(shape instanceof Rectangle) {
  8.                 Rectangle rectangle = (Rectangle) shape;
  9.                 area += rectangle.getWidth() * rectangle.getHeight();
  10.             }else{
  11.                 Circle circle = (Circle) shape;
  12.                 area += circle.getRadius() * circle.getRadius() * Math.PI;
  13.             }
  14.            
  15.         }
  16.         return area;
  17.     }
  18.    
  19. }

Voila, vous avez répondu à l'attente du client, il est content... Mais  il semble qu'il y a anguille sous roche.
En effet, on peut très justement se demander pourquoi se limiter aux rectangles et aux cercles, c'est d'ailleurs ce que se dit le client, il revient alors vers vous pour vous demander si ajouter le calcul de l'air d'un triangle serait compliqué.
Pour cela vous allez devoir modifier une nième fois la classe AreaCalculator au risque de casser ce qui fonctionne, la classe AreaCalculator n'est donc pas fermé à la modification.
Et pourtant il semble judicieux, étant donné le nombre de modification déjà apporté et les éventuelles modifications à venir, d'appliquer l'OCP sur AreaCalculator.
Pour se faire, nous allons créer un Interface Shape avec une methode area() se chargeant de calculer l'air d'un Shape.

  1. public interface Shape {
  2.    
  3.     public abstract double area();
  4. }

Maintenant on va faire en sorte que les classes Rectangle et Circle implémente l'interface Area :
  1. public class Circle implements Shape {
  2.     private double radius;
  3.     @Override
  4.     public double area() {
  5.         return  radius * radius * Math.PI;
  6.     }
  7.    
  8.    
  9. }
  1. public class Rectangle implements Shape {
  2.     private double width;
  3.    
  4.     private double height;
  5.     @Override
  6.     public double area() {
  7.        
  8.         return width * height;
  9.     }  
  10. }
A partir de ce modele, on peut réaliser un algorithme de calcul d'air capable de faire completement abstraction du type de "Shape" :

  1. public class AreaCalculator {
  2.    
  3.     public double area(List<Shape> shapes){
  4.         double area = 0;
  5.         for (Shape shape : shapes) {
  6.             area += shape.area();  
  7.         }
  8.         return area;
  9.     }
  10.    
  11. }
Dorénavant l'ajout d'une nouvelle forme géométrique ne passe plus par la modification de l'algorithme du calcul d'air, mais uniquement par l'ajout d'une classe implémentant l'interface Shape ce qui rendra cette nouvelle classe utilisable par l'algorithme de calcul d'air.
AreaCalculator et sa methode de calcul d'air est alors fermé a la modification et ouverte à l’évolution.

Entre autres les desgin pattern Strategy, Template method ou encore Visitor découlent directement de l'OCP.

1 commentaire: