Le livre «Object-Oriented Approach. 5e int. éd. "

imageLa programmation orientée objet (POO) est au cœur des langages C ++, Java, C #, Visual Basic .NET, Ruby, Objective-C et même Swift. Ils ne peuvent pas se passer d'objets technologiques Web, car ils utilisent JavaScript, Python et PHP.

C'est pourquoi Matt Weissfeld conseille de développer une façon de penser orientée objet et de ne procéder ensuite qu'à un développement orienté objet dans un langage de programmation spécifique.

Ce livre est écrit par le développeur pour les développeurs et vous permet de choisir les meilleures approches pour résoudre des problèmes spécifiques. Vous apprendrez comment appliquer correctement l'héritage et la composition, comprendre la différence entre l'agrégation et l'association et arrêter de confondre l'interface et l'implémentation.

Les technologies de programmation sont en constante évolution et en développement, mais les concepts orientés objet sont indépendants de la plate-forme et restent toujours efficaces. Cette publication se concentre sur les fondements fondamentaux de la POO: les modèles de conception, les dépendances et les principes SOLIDES qui rendront votre code compréhensible, flexible et bien entretenu.

Principes de conception orientée objet SOLID


1. SRP: principe de responsabilité exclusive


Le principe de la responsabilité exclusive stipule qu'une seule raison est requise pour apporter des modifications à une classe. Chaque module de classe et de programme doit avoir une tâche en priorité. Par conséquent, vous ne devez pas introduire de méthodes pouvant entraîner des modifications de la classe pour plusieurs raisons. Si la description de classe contient le mot «et», alors le principe SRP peut être violé. En d'autres termes, chaque module ou classe doit être responsable d'une partie des fonctionnalités du logiciel, et cette responsabilité doit être entièrement encapsulée dans la classe.

La création d'une hiérarchie de figures est l'un des exemples classiques illustrant l'héritage. Cet exemple se retrouve souvent dans l'enseignement, et je l'utilise tout au long de ce chapitre (ainsi que tout au long du livre). Dans cet exemple, la classe Circle hérite des attributs de la classe Shape. La classe Shape fournit la méthode abstraite calcArea () comme contrat pour une sous-classe. Chaque classe qui hérite de Shape doit avoir sa propre implémentation de la méthode calcArea ():

abstract class Shape{
     protected String name;
     protected double area;
     public abstract double calcArea();
}

Dans cet exemple, la classe Circle, qui hérite de la classe Shape, fournit son implémentation de la méthode calcArea (), si nécessaire:

class Circle extends Shape{
     private double radius;

     public Circle(double r) {
           radius = r;
     }
     public double calcArea() {
           area = 3.14*(radius*radius) ;
           return (area);
     };
}

La troisième classe, CalculateAreas, calcule l'aire des différentes formes contenues dans le tableau Shape. Le tableau Shape est de taille illimitée et peut contenir différentes formes, telles que des carrés et des triangles.

class CalculateAreas {
     Shape[] shapes;
     double sumTotal=0;
     public CalculateAreas(Shape[] sh) {
           this.shapes = sh;
     }
     public double sumAreas() {
           sumTotal=0;
           for (inti=0; i<shapes.length; i++) {
           sumTotal = sumTotal + shapes[i].calcArea() ;
           }
           return sumTotal ;
     }
     public void output() {
           System.out.printIn("Total of all areas = " + sumTotal);
     }
}

Notez que la classe CalculateAreas gère également la sortie de l'application, ce qui peut provoquer des problèmes. Le comportement de comptage de zone et le comportement de sortie sont liés car ils sont contenus dans la même classe.

Nous pouvons vérifier la fonctionnalité de ce code en utilisant l'application de test appropriée TestShape:

public class TestShape {
      public static void main(String args[]) {

            System.out.printin("Hello World!");

            Circle circle = new Circle(1);

            Shape[] shapeArray = new Shape[1];
            shapeArray[0] = circle;

            CalculateAreas ca = new CalculateAreas(shapeArray) ;

            ca.sumAreas() ;
            ca.output();
      }
}

Maintenant que nous avons une application de test à notre disposition, nous pouvons nous concentrer sur le problème du principe de la responsabilité exclusive. Encore une fois, le problème est lié à la classe CalculateAreas et au fait que cette classe contient des comportements pour additionner les zones de différentes formes, ainsi que pour la sortie de données.

La question fondamentale (et, en fait, le problème) est que si vous devez modifier la fonctionnalité de la méthode output (), vous devrez apporter des modifications à la classe CalculateAreas, que la méthode de calcul de l'aire des formes change ou non. Par exemple, si nous voulons soudainement exporter des données vers la console HTML et non du texte brut, nous devrons recompiler et ré-incorporer le code, ce qui ajoute la zone des figures. Tout cela parce que la responsabilité est liée.

Conformément au principe de la responsabilité exclusive, la tâche consiste à changer une méthode sans affecter les autres méthodes et à ne pas avoir à recompiler. "La classe devrait avoir une, une seule raison de changement - la seule responsabilité qui doit être changée."

Pour résoudre ce problème, vous pouvez placer deux méthodes dans des classes distinctes, l'une pour la sortie de la console d'origine, l'autre pour la sortie HTML:

class CaiculateAreas {;
     Shape[] shapes;
     double sumTotal=0;

     public CalculateAreas(Shape[] sh) {
           this.shapes = sh;
     }

     public double sumAreas() {
           sumTotal=0;

           for (inti=0; i<shapes.length; i++) {

                sumTotal = sumTotal + shapes[i].calcArea();

           }

                return sumTotal;
           }
}
class OutputAreas {
     double areas=0;
     public OutputAreas (double a) {
           this.areas = a;
     }

           public void console() {
           System.out.printin("Total of all areas = " + areas);
     }
     public void HTML() {
           System.out.printIn("<HTML>") ;
           System.out.printin("Total of all areas = " + areas);
           System.out.printin("</HTML>") ;
     }
}

L'essentiel ici est que vous pouvez maintenant envoyer une conclusion dans différentes directions en fonction du besoin. Si vous souhaitez ajouter la possibilité d'une autre méthode de sortie, par exemple, JSON, vous pouvez l'ajouter à la classe OutputAreas sans avoir à modifier la classe CalculateAreas. Par conséquent, vous pouvez redistribuer la classe CalculateAreas sans affecter les autres classes en aucune façon.

2. OCP: principe ouvert / fermé


Le principe d'ouverture / de proximité stipule que vous pouvez étendre le comportement d'une classe sans apporter de modifications.

Prenons encore une fois l'exemple avec les chiffres. Dans le code ci-dessous, il existe une classe ShapeCalculator qui prend un objet Rectangle, calcule la zone de cet objet et renvoie des valeurs. Il s'agit d'une application simple, mais elle ne fonctionne qu'avec des rectangles.

class Rectangle{
     protected double length;
     protected double width;

     public Rectangle(double 1, double w) {
           length = 1;
           width = w;
     };
}
class CalculateAreas {
     private double area;

     public double calcArea(Rectangle r) {

           area = r.length * r.width;

           return area;
     }
}
public class OpenClosed {
      public static void main(String args[]) {

            System.out.printin("Hello World");

            Rectangle r = new Rectangle(1,2);

            CalculateAreas ca = new CalculateAreas ();

            System.out.printin("Area = "+ ca.calcArea(r));
      }
}

Le fait que cette application ne fonctionne que dans le cas des rectangles conduit à une limitation qui explique clairement le principe d'ouverture / fermeture: si nous voulons ajouter la classe Circle à la classe CalculateArea (changer ce qu'elle fait), nous devons apporter des modifications au module lui-même. Évidemment, cela entre en conflit avec le principe d'ouverture / de proximité, qui stipule que nous ne devons pas modifier le module pour changer ce qu'il fait.

Pour respecter le principe d'ouverture / de proximité, nous pouvons revenir à l'exemple déjà testé avec des figures, où une classe Shape abstraite est créée et directement les figures héritent de la classe Shape, qui a une méthode getArea () abstraite.

Pour le moment, vous pouvez ajouter autant de classes différentes que nécessaire, sans avoir à apporter de modifications directement à la classe Shape (par exemple, la classe Circle). Maintenant, nous pouvons dire que la classe Shape est fermée.

Le code ci-dessous fournit une implémentation de la solution pour les rectangles et les cercles et vous permet de créer un nombre illimité de formes:

abstract class Shape {
      public abstract double getArea() ;
}
class Rectangle extends Shape
{

      protected double length;
      protected double width;

      public Rectangle(double 1, double w) {
            length = 1;
            width = w;
      };
      public double getArea() {
            return length*width;
      }

}
class Circle extends Shape
{
      protected double radius;

      public Circle(double r) {
            radius = r;
      };
      public double getArea() {
            return radius*radius*3.14;
      }
}
class CalculateAreas {
      private double area;

      public double calcArea(Shape s) {
            area = s.getArea();
            return area;
      }
}

public class OpenClosed {
      public static void main(String args[]) {

            System.out.printiIn("Hello World") ;

            CalculateAreas ca = new CalculateAreas() ;

            Rectangle r = new Rectangle(1,2);

            System.out.printIn("Area = " + ca.calcArea(r));

            Circle c = new Circle(3);

            System.out.printIn("Area = " + ca.calcArea(c));
}
}

Il convient de noter qu'avec cette implémentation, la méthode CalculateAreas () ne doit pas être modifiée lors de la création d'une nouvelle instance de la classe Shape.

Vous pouvez mettre le code à l'échelle sans vous soucier de l'existence du code précédent. Le principe d'ouverture / de proximité est que vous devez étendre le code à l'aide de sous-classes afin que la classe d'origine ne nécessite pas de modifications. Cependant, le concept d '«extension» lui-même est controversé dans certaines discussions concernant les principes de SOLID. Pour le dire franchement, si nous préférons la composition plutôt que l'héritage, comment cela affecte-t-il le principe d'ouverture / de proximité?

Lorsqu'il se conforme à l'un des principes SOLID, le code peut satisfaire aux critères d'autres principes SOLID. Par exemple, lors de la conception conformément au principe d'ouverture / de proximité, le code peut répondre aux exigences du principe de la responsabilité exclusive.

3. LSP: principe de substitution Lisk


Selon le principe de substitution de Liskov, la conception devrait prévoir la possibilité de remplacer toute instance de la classe parent par une instance de l'une des classes enfants. Si la classe parent peut effectuer n'importe quelle tâche, la classe enfant doit également pouvoir le faire.

Considérez un code qui est correct à première vue, mais qui viole le principe de substitution Lisk. Le code ci-dessous contient une classe abstraite générique Shape. La classe Rectangle, à son tour, hérite des attributs de la classe Shape et remplace sa méthode abstraite calcArea (). La classe Square, à son tour, hérite de Rectangle.

abstract class Shape{
      protected double area;

      public abstract double calcArea();
}
class Rectangle extends Shape{
      private double length;
      private double width;

      public Rectangle(double 1, double w) {
            length = 1;
            width = w;
      }
      public double calcArea() {
            area = length*width;
            return (area) ;
      };
}
class Square extends Rectangle{
      public Square(double s) {
            super(s, Ss);
      }
}

public class LiskovSubstitution {
      public static void main(String args[]) {

            System.out.printIn("Hello World") ;

            Rectangle r = new Rectangle(1,2);

            System.out.printin("Area = " + r.calcArea());

            Square s = new Square(2) ;

            System.out.printin("Area = " + s.calcArea());
      }
}

Jusqu'à présent, tout va bien: le rectangle est une instance de la figure, donc il n'y a rien à craindre, car le carré est une instance du rectangle - et encore une fois, tout est correct, non?

Maintenant posons une question philosophique: un carré est-il toujours un rectangle? Beaucoup répondront par l'affirmative. Bien que l'on puisse supposer qu'un carré est un cas particulier d'un rectangle, ses propriétés diffèrent. Un rectangle est un parallélogramme (les côtés opposés sont les mêmes), comme un carré. En même temps, le carré est également un losange (tous les côtés sont identiques), tandis que le rectangle ne l'est pas. Il existe donc des différences.

En matière de conception orientée objet, le problème n'est pas la géométrie. Le problème est de savoir exactement comment nous créons des rectangles et des carrés. Voici le constructeur de la classe Rectangle:

public Rectangle(double 1, double w) {
      length = 1;
      width = w;
}

De toute évidence, le constructeur nécessite deux paramètres. Cependant, le constructeur de la classe Square n'en requiert qu'un, même si la classe parente, Rectangle, en requiert deux.

class Square extends Rectangle{
      public Square(double s) {
      super(s, Ss);
}

En fait, la fonction de calcul de l'aire est légèrement différente dans le cas de chacune de ces deux classes. Autrement dit, la classe Square, pour ainsi dire, imite un rectangle, en passant deux fois le même paramètre au constructeur. Il peut sembler qu'une telle solution de contournement soit tout à fait appropriée, mais en fait, elle peut induire en erreur les développeurs accompagnant le code, qui est assez semé d'embûches lorsqu'il est accompagné à l'avenir. Au moins, c'est un problème et, probablement, une décision de conception douteuse. Quand un constructeur en appelle un autre, c'est une bonne idée de faire une pause et de reconsidérer la construction - peut-être que la classe enfant n'est pas construite correctement.

Comment trouver un moyen de sortir de cette situation? Autrement dit, vous ne pouvez pas substituer une classe Square à un Rectangle. Ainsi, Square ne doit pas être un enfant de la classe Rectangle. Ils doivent être des classes séparées.

abstract class Shape {
      protected double area;

      public abstract double calcArea();
}

class Rectangle extends Shape {

      private double length;
      private double width;

      public Rectangle(double 1, double w) {
            length = 1;
            width = w;
      }

      public double calcArea() {
            area = length*width;
            return (area);
      };
}

class Square extends Shape {
      private double side;

      public Square(double s) {
            side = s;
      }
      public double calcArea() {
            area = side*side;
            return (area);
      };
}

public class LiskovSubstitution {
      public static void main(String args[]) {

             System.out.printIn("Hello World") ;

             Rectangle r = new Rectangle(1,2);

             System.out.printIn("Area = " + r.calcArea());

             Square s = new Square(2) ;

             System.out.printIn("Area = " + s.calcArea());
      }
}

4. ISP: principe de partage d'interface


Le principe de séparation des interfaces stipule qu'il vaut mieux créer de nombreuses petites interfaces que plusieurs grandes.

Dans cet exemple, nous créons une interface unique qui inclut plusieurs comportements pour la classe Mammal, à savoir eat () et makeNoise ():

interface IMammal {
     public void eat();
     public void makeNoise() ;
}
class Dog implements IMammal {
     public void eat() {
           System.out.printIn("Dog is eating");
     }
     public void makeNoise() {
           System.out.printIn("Dog is making noise");
     }
}
public class MyClass {
      public static void main(String args[]) {

            System.out.printIn("Hello World");

            Dog fido = new Dog();
            fido.eat();
            fido.makeNoise()
      }
}

Au lieu de créer une interface unique pour la classe Mammal, vous devez créer
des interfaces distinctes pour tous les comportements:

interface IEat {
     public void eat();
}
interface IMakeNoise {
     public void makeNoise() ;
}
class Dog implements IEat, IMakeNoise {
     public void eat() {
           System.out.printIn("Dog is eating");
     }
     public void makeNoise() {
           System.out.printIn("Dog is making noise");
     }
}
public class MyClass {
      public static void main(String args[]) {

            System.out.printIn("Hello World") ;

            Dog fido = new Dog();
            fido.eat();
            fido.makeNoise();
      }
}

Nous séparons les comportements de la classe des mammifères. Il s'avère qu'au lieu de créer une seule classe de mammifères par héritage (plus précisément, les interfaces), nous passons à une conception basée sur la composition, similaire à la stratégie utilisée dans le chapitre précédent.

En quelques mots, avec cette approche, nous pouvons créer des instances de la classe Mammal en utilisant la composition, et ne pas être obligés d'utiliser des comportements qui sont intégrés dans une seule classe Mammal. Par exemple, supposons qu'un mammifère découvert ne mange pas, mais absorbe plutôt les nutriments à travers la peau. Si nous héritons de la classe Mammal contenant le comportement eat (), ce comportement sera redondant pour le nouveau mammifère. De plus, si tous les comportements sont présentés dans des interfaces uniques distinctes, il se révélera construire la classe de chaque mammifère exactement comme prévu.

"Pour plus d'informations sur le livre, consultez le site Web de l'éditeur
" Table of Contents
" Extrait

To Habrozhiteley 25% coupon de réduction -POO

Lors du paiement de la version papier du livre, un livre électronique est envoyé par e-mail.

Source: https://habr.com/ru/post/undefined/


All Articles