Das Buch „Objektorientierter Ansatz. 5th int. ed. "

BildDie objektorientierte Programmierung (OOP) ist das Herzstück der Sprachen C ++, Java, C #, Visual Basic .NET, Ruby, Objective-C und sogar Swift. Sie können nicht auf Web-Technologieobjekte verzichten, da sie JavaScript, Python und PHP verwenden.

Deshalb rät Matt Weissfeld, eine objektorientierte Denkweise zu entwickeln und erst dann mit der objektorientierten Entwicklung in einer bestimmten Programmiersprache fortzufahren.

Dieses Buch wurde vom Entwickler für Entwickler geschrieben und ermöglicht es Ihnen, die besten Ansätze zur Lösung spezifischer Probleme auszuwählen. Sie lernen, wie Sie Vererbung und Zusammensetzung richtig anwenden, den Unterschied zwischen Aggregation und Zuordnung verstehen und die Schnittstelle und Implementierung nicht mehr verwirren.

Programmiertechnologien ändern und entwickeln sich ständig, aber objektorientierte Konzepte sind plattformunabhängig und bleiben durchweg effektiv. Diese Veröffentlichung konzentriert sich auf die grundlegenden Grundlagen von OOP: Entwurfsmuster, Abhängigkeiten und SOLID-Prinzipien, die Ihren Code verständlich, flexibel und gut gepflegt machen.

SOLID Objektorientierte Designprinzipien


1. SRP: Prinzip der alleinigen Verantwortung


Das Prinzip der alleinigen Verantwortung besagt, dass nur ein Grund erforderlich ist, um Änderungen an einer Klasse vorzunehmen. Jede Klasse und jedes Programmmodul muss eine Aufgabe mit Priorität haben. Daher sollten Sie keine Methoden einführen, die aus mehr als einem Grund Änderungen an der Klasse verursachen können. Wenn die Klassenbeschreibung das Wort „und“ enthält, kann das SRP-Prinzip verletzt werden. Mit anderen Worten, jedes Modul oder jede Klasse muss für einen Teil der Softwarefunktionalität verantwortlich sein, und diese Verantwortung muss vollständig in der Klasse enthalten sein.

Das Erstellen einer Zahlenhierarchie ist eines der klassischen Beispiele für die Vererbung. Dieses Beispiel findet sich häufig in der Lehre und wird in diesem Kapitel (sowie im gesamten Buch) verwendet. In diesem Beispiel erbt die Circle-Klasse Attribute von der Shape-Klasse. Die Shape-Klasse stellt die abstrakte calcArea () -Methode als Vertrag für eine Unterklasse bereit. Jede Klasse, die von Shape erbt, muss über eine eigene Implementierung der calcArea () -Methode verfügen:

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

In diesem Beispiel stellt die Circle-Klasse, die von der Shape-Klasse erbt, bei Bedarf die Implementierung der calcArea () -Methode bereit:

class Circle extends Shape{
     private double radius;

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

Die dritte Klasse, CalculateAreas, berechnet die Fläche der verschiedenen Formen, die im Shape-Array enthalten sind. Das Shape-Array ist unbegrenzt groß und kann verschiedene Formen enthalten, z. B. Quadrate und Dreiecke.

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);
     }
}

Beachten Sie, dass die CalculateAreas-Klasse auch die Anwendungsausgabe verarbeitet, was zu Problemen führen kann. Bereichszählverhalten und Ausgabeverhalten hängen zusammen, da sie in derselben Klasse enthalten sind.

Wir können die Funktionalität dieses Codes mit der entsprechenden Testanwendung TestShape überprüfen:

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();
      }
}

Nachdem wir nun eine Testanwendung zur Verfügung haben, können wir uns auf das Problem des Prinzips der alleinigen Verantwortung konzentrieren. Wiederum liegt das Problem bei der CalculateAreas-Klasse und der Tatsache, dass diese Klasse Verhaltensweisen zum Addieren der Bereiche verschiedener Formen sowie zum Ausgeben von Daten enthält.

Die grundlegende Frage (und tatsächlich das Problem) ist, dass Sie, wenn Sie die Funktionalität der output () -Methode ändern müssen, Änderungen an der CalculateAreas-Klasse vornehmen müssen, unabhängig davon, ob sich die Methode zur Berechnung des Bereichs von Formen ändert. Wenn wir beispielsweise plötzlich Daten an die HTML-Konsole und nicht an einfachen Text ausgeben möchten, müssen wir den Code neu kompilieren und neu implementieren, wodurch sich der Bereich der Abbildungen addiert. Alles nur, weil die Haftung damit zusammenhängt.

Nach dem Prinzip der alleinigen Verantwortung besteht die Aufgabe darin, eine Methode zu ändern, die die anderen Methoden nicht beeinflusst und nicht neu kompiliert werden muss. "Die Klasse sollte nur einen Grund für Veränderungen haben - die einzige Verantwortung, die geändert werden muss."

Um dieses Problem zu lösen, können Sie zwei Methoden in separate Klassen einteilen, eine für die ursprüngliche Konsolenausgabe und eine für die HTML-Ausgabe:

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>") ;
     }
}

Das Fazit hier ist, dass Sie jetzt je nach Bedarf eine Schlussfolgerung in verschiedene Richtungen senden können. Wenn Sie die Möglichkeit einer anderen Ausgabemethode hinzufügen möchten, z. B. JSON, können Sie diese der OutputAreas-Klasse hinzufügen, ohne Änderungen an der CalculateAreas-Klasse vornehmen zu müssen. Infolgedessen können Sie die CalculateAreas-Klasse neu verteilen, ohne andere Klassen in irgendeiner Weise zu beeinflussen.

2. OCP: offenes / geschlossenes Prinzip


Das Prinzip der Offenheit / Nähe besagt, dass Sie das Verhalten einer Klasse erweitern können, ohne Änderungen vorzunehmen.

Lassen Sie uns noch einmal auf das Beispiel mit den Zahlen achten. Im folgenden Code gibt es eine ShapeCalculator-Klasse, die ein Rectangle-Objekt verwendet, die Fläche dieses Objekts berechnet und Werte zurückgibt. Dies ist eine einfache Anwendung, die jedoch nur mit Rechtecken funktioniert.

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));
      }
}

Die Tatsache, dass diese Anwendung nur bei Rechtecken funktioniert, führt zu einer Einschränkung, die das Prinzip der Offenheit / Schließung klar erklärt: Wenn wir die Circle-Klasse zur CalculateArea-Klasse hinzufügen möchten (ändern Sie, was sie tut), müssen Sie Änderungen am Modul selbst vornehmen. Dies steht offensichtlich im Widerspruch zum Prinzip der Offenheit / Nähe, das besagt, dass wir keine Änderungen am Modul vornehmen sollten, um dessen Funktionsweise zu ändern.

Um dem Prinzip der Offenheit / Nähe zu entsprechen, können wir zu dem bereits getesteten Beispiel mit Zahlen zurückkehren, in dem eine abstrakte Formklasse erstellt wird und die Figuren direkt von der Formklasse erben, die eine abstrakte getArea () -Methode hat.

Im Moment können Sie beliebig viele verschiedene Klassen hinzufügen, ohne Änderungen direkt an der Shape-Klasse (z. B. der Circle-Klasse) vornehmen zu müssen. Jetzt können wir sagen, dass die Shape-Klasse geschlossen ist.

Der folgende Code bietet eine Implementierung der Lösung für Rechtecke und Kreise und ermöglicht Ihnen das Erstellen einer unbegrenzten Anzahl von Formen:

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));
}
}

Es ist zu beachten, dass bei dieser Implementierung die CalculateAreas () -Methode beim Erstellen einer neuen Instanz der Shape-Klasse nicht geändert werden sollte.

Sie können den Code skalieren, ohne sich Gedanken über die Existenz des vorherigen Codes machen zu müssen. Das Prinzip der Offenheit / Nähe besteht darin, dass Sie den Code mithilfe von Unterklassen erweitern, damit für die ursprüngliche Klasse keine Änderungen erforderlich sind. Das Konzept der „Erweiterung“ selbst ist jedoch in einigen Diskussionen über die Prinzipien von SOLID umstritten. Um es auf den Punkt zu bringen: Wenn wir Komposition statt Vererbung bevorzugen, wie wirkt sich dies auf das Prinzip der Offenheit / Nähe aus?

Bei Einhaltung eines der SOLID-Prinzipien kann der Code die Kriterien anderer SOLID-Prinzipien erfüllen. Wenn der Code beispielsweise nach dem Prinzip der Offenheit / Nähe gestaltet wird, kann er den Anforderungen des Prinzips der alleinigen Verantwortung entsprechen.

3. LSP: Lisk-Substitutionsprinzip


Nach dem Liskov-Substitutionsprinzip sollte das Design die Möglichkeit bieten, jede Instanz der Elternklasse durch eine Instanz einer der Kindklassen zu ersetzen. Wenn die übergeordnete Klasse eine Aufgabe ausführen kann, muss auch die untergeordnete Klasse dazu in der Lage sein.

Betrachten Sie einen Code, der auf den ersten Blick korrekt ist, aber gegen das Lisk-Substitutionsprinzip verstößt. Der folgende Code enthält eine generische abstrakte Formklasse. Die Rectangle-Klasse erbt wiederum Attribute von der Shape-Klasse und überschreibt ihre abstrakte calcArea () -Methode. Die Square-Klasse wiederum erbt von 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());
      }
}

So weit so gut: Das Rechteck ist eine Instanz der Figur, also gibt es keinen Grund zur Sorge, da das Quadrat eine Instanz des Rechtecks ​​ist - und wieder ist alles richtig, oder?

Stellen wir nun eine philosophische Frage: Ist ein Quadrat noch ein Rechteck? Viele werden bejahen. Obwohl davon ausgegangen werden kann, dass ein Quadrat ein Sonderfall eines Rechtecks ​​ist, unterscheiden sich seine Eigenschaften. Ein Rechteck ist ein Parallelogramm (die gegenüberliegenden Seiten sind gleich) wie ein Quadrat. Gleichzeitig ist das Quadrat auch eine Raute (alle Seiten sind gleich), das Rechteck jedoch nicht. Daher gibt es Unterschiede.

Wenn es um objektorientiertes Design geht, ist das Problem nicht die Geometrie. Das Problem ist, wie genau wir Rechtecke und Quadrate erstellen. Hier ist der Konstruktor für die Rectangle-Klasse:

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

Offensichtlich benötigt der Konstruktor zwei Parameter. Der Konstruktor für die Square-Klasse benötigt jedoch nur eine, obwohl die übergeordnete Klasse Rectangle zwei benötigt.

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

Tatsächlich unterscheidet sich die Funktion zur Berechnung der Fläche bei jeder dieser beiden Klassen geringfügig. Das heißt, die Square-Klasse imitiert sozusagen ein Rechteck und übergibt denselben Parameter zweimal an den Konstruktor. Es mag den Anschein haben, dass eine solche Problemumgehung durchaus geeignet ist, aber tatsächlich kann sie die Entwickler irreführen, die den Code begleiten, was in Zukunft mit Fallstricken behaftet ist. Zumindest ist dies ein Problem und wahrscheinlich eine zweifelhafte Entwurfsentscheidung. Wenn ein Konstruktor einen anderen aufruft, ist es eine gute Idee, eine Pause einzulegen und das Konstrukt zu überdenken - möglicherweise ist die untergeordnete Klasse nicht richtig erstellt.

Wie finde ich einen Ausweg aus dieser Situation? Einfach ausgedrückt, Sie können ein Rechteck nicht durch eine Quadratklasse ersetzen. Daher sollte Square kein Kind der Rectangle-Klasse sein. Sie müssen separate Klassen sein.

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: Prinzip der Schnittstellenfreigabe


Das Prinzip der Trennung von Schnittstellen besagt, dass es besser ist, viele kleine Schnittstellen als mehrere große zu erstellen.

In diesem Beispiel erstellen wir eine einzelne Schnittstelle, die verschiedene Verhaltensweisen für die Mammal-Klasse enthält, nämlich eat () und 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()
      }
}

Anstatt eine einzelne Schnittstelle für die Mammal-Klasse zu erstellen, müssen Sie
separate Schnittstellen für alle Verhaltensweisen erstellen :

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();
      }
}

Wir trennen Verhaltensweisen von der Säugetierklasse. Es stellt sich heraus, dass wir, anstatt eine einzelne Mammal-Klasse durch Vererbung (genauer gesagt Schnittstellen) zu erstellen, mit dem Design fortfahren, das auf der Zusammensetzung basiert, ähnlich der im vorherigen Kapitel verwendeten Strategie.

Mit diesem Ansatz können wir mit wenigen Worten Instanzen der Mammal-Klasse mithilfe der Komposition erstellen, anstatt gezwungen zu sein, Verhaltensweisen zu verwenden, die in eine einzelne Mammal-Klasse eingebettet sind. Angenommen, es wird ein Säugetier entdeckt, das nicht frisst, sondern Nährstoffe über die Haut aufnimmt. Wenn wir von der Mammal-Klasse erben, die das eat () -Verhalten enthält, ist dieses Verhalten für das neue Säugetier überflüssig. Wenn alle Verhaltensweisen in separaten Schnittstellen dargestellt werden, wird sich außerdem herausstellen, dass die Klasse jedes Säugetiers genau wie beabsichtigt aufgebaut ist.

»Weitere Informationen zum Buch finden Sie auf der Website des Herausgebers.
» Inhaltsverzeichnis
» Auszug

für Habrozhitelami 25% Rabatt auf den Gutschein -OOP

Nach Zahlung der Papierversion des Buches wird ein elektronisches Buch per E-Mail verschickt.

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


All Articles