Conceptual Article

SOLID: Die ersten 5 Prinzipien des objektorientierten Designs

Published on February 20, 2021
    authorauthor

    Samuel Oloruntoba and Bradley Kouchi

    Deutsch
    SOLID: Die ersten 5 Prinzipien des objektorientierten Designs

    Einführung

    SOLID ist ein Akronym für die ersten fünf Prinzipien des objektorientierten Designs (OOD) von Robert C. Martin (auch bekannt als Onkel Bob).

    Anmerkung: Obwohl diese Prinzipien auf verschiedene Programmiersprachen angewendet werden können, wird der in diesem Artikel enthaltene Beispielcode PHP verwendet.

    Diese Prinzipien legen Praktiken fest, die sich für die Entwicklung von Software mit Überlegungen zur Aufrechterhaltung und Erweiterung eignen, wenn das Projekt wächst. Die Übernahme dieser Praktiken kann auch zur Vermeidung von Code Smells, Refactoring von Code und agiler oder adaptiver Softwareentwicklung beitragen.

    SOLID steht für:

    In diesem Artikel werden Sie jedes Prinzip einzeln kennenlernen, um zu verstehen, wie SOLID Ihnen dabei helfen kann, ein besserer Entwickler zu werden.

    Single-Responsibility-Prinzip

    Das Single-Responsibility-Prinzip (SRP) besagt:

    Eine Klasse sollte einen und nur einen Grund haben, sich zu ändern, d. h. eine Klasse sollte nur eine Aufgabe haben.

    Betrachten Sie beispielsweise eine Anwendung, die eine Sammlung von Formen – Kreise und Quadrate – nimmt und die Summe der Fläche aller Formen in der Sammlung berechnet.

    Erstellen Sie zunächst die Formklassen und lassen Sie die Konstruktoren die erforderlichen Parameter einrichten.

    Für Quadrate müssen Sie die length einer Seite kennen:

    class Square
    {
        public $length;
    
        public function construct($length)
        {
            $this->length = $length;
        }
    }
    

    Für Kreise müssen Sie den radius kennen:

    class Circle
    {
        public $radius;
    
        public function construct($radius)
        {
            $this->radius = $radius;
        }
    }
    

    Erstellen Sie anschließend die Klasse AreaCalculator und schreiben Sie dann die Logik, um die Fläche aller bereitgestellten Formen zu summieren. Der Flächeninhalt eines Quadrats wird durch die Länge zum Quadrat berechnet. Der Flächeninhalt eines Kreises wird durch Pi mal Radius zum Quadrat berechnet.

    class AreaCalculator
    {
        protected $shapes;
    
        public function __construct($shapes = [])
        {
            $this->shapes = $shapes;
        }
    
        public function sum()
        {
            foreach ($this->shapes as $shape) {
                if (is_a($shape, 'Square')) {
                    $area[] = pow($shape->length, 2);
                } elseif (is_a($shape, 'Circle')) {
                    $area[] = pi() * pow($shape->radius, 2);
                }
            }
    
            return array_sum($area);
        }
    
        public function output()
        {
            return implode('', [
              '',
                  'Sum of the areas of provided shapes: ',
                  $this->sum(),
              '',
          ]);
        }
    }
    

    Um die Klasse AreaCalculator zu verwenden, müssen Sie die Klasse instanziieren und ein Array von Formen übergeben und die Ausgabe am Ende der Seite anzeigen.

    Hier ist ein Beispiel mit einer Sammlung von drei Formen:

    • Ein Kreis mit einem Radius von 2
    • Ein Quadrat mit einer Länge von 5
    • Ein zweites Quadrat mit einer Länge von 6
    $shapes = [
      new Circle(2),
      new Square(5),
      new Square(6),
    ];
    
    $areas = new AreaCalculator($shapes);
    
    echo $areas->output();
    

    Das Problem mit der Ausgabemethode ist, dass der AreaCalculator die Logik zur Ausgabe der Daten bearbeitet.

    Bedenken Sie ein Szenario, in dem die Ausgabe in ein anderes Format wie JSON konvertiert werden soll.

    Die gesamte Logik würde von der Klasse AreaCalculator bearbeitet werden. Dies würde gegen das Single-Responsibility-Prinzip verstoßen. Die Klasse AreaCalculator sollte nur mit der Summe der Flächen der bereitgestellten Formen befasst sein. Sie sollte sich nicht damit befassen, ob der Benutzer JSON oder HTML wünscht.

    Um dies zu beheben, können Sie eine separate Klasse SumCalculatorOutputter erstellen und diese neue Klasse verwenden, um die Logik zu bearbeiten, die Sie für die Ausgabe der Daten an den Benutzer benötigen:

    class SumCalculatorOutputter
    {
        protected $calculator;
    
        public function __constructor(AreaCalculator $calculator)
        {
            $this->calculator = $calculator;
        }
    
        public function JSON()
        {
            $data = [
              'sum' => $this->calculator->sum(),
          ];
    
            return json_encode($data);
        }
    
        public function HTML()
        {
            return implode('', [
              '',
                  'Sum of the areas of provided shapes: ',
                  $this->calculator->sum(),
              '',
          ]);
        }
    }
    

    Die Klasse SumCalculatorOutputter würde wie folgt funktionieren:

    $shapes = [
      new Circle(2),
      new Square(5),
      new Square(6),
    ];
    
    $areas = new AreaCalculator($shapes);
    $output = new SumCalculatorOutputter($areas);
    
    echo $output->JSON();
    echo $output->HTML();
    

    Jetzt wird die Logik, die Sie zur Ausgabe der Daten an den Benutzer benötigen, von der Klasse SumCalculatorOutputter bearbeitet.

    Das erfüllt das Single-Responsibility-Prinzip.

    Open-Closed-Prinzip

    Das Open-Closed-Prinzip (S.R.P.) besagt:

    Objekte oder Entitäten sollten offen für Erweiterungen, aber geschlossen für Änderungen sein.

    Das bedeutet, dass eine Klasse erweiterbar sein sollte, ohne die Klasse selbst zu modifizieren.

    Gehen wir noch einmal auf die Klasse AreaCalculator ein und konzentrieren uns auf die Methode sum:

    class AreaCalculator
    {
        protected $shapes;
    
        public function __construct($shapes = [])
        {
            $this->shapes = $shapes;
        }
    
        public function sum()
        {
            foreach ($this->shapes as $shape) {
                if (is_a($shape, 'Square')) {
                    $area[] = pow($shape->length, 2);
                } elseif (is_a($shape, 'Circle')) {
                    $area[] = pi() * pow($shape->radius, 2);
                }
            }
    
            return array_sum($area);
        }
    }
    

    Bedenken Sie ein Szenario, in dem der Benutzer die Summe sum zusätzlicher Formen wie Dreiecke, Fünfecke, Sechsecke usw. wünscht. Sie müssten diese Datei ständig bearbeiten und weitere if/else-Blöcke hinzufügen. Das würde das Open-Closed-Prinzip verletzen.

    Eine Möglichkeit, diese Methode sum zu verbessern, besteht darin, die Logik zur Berechnung der Fläche jeder Form aus der Klassenmethode AreaCalculator zu entfernen und sie an die Klasse jeder Form anzuhängen.

    Hier ist die in Square definierte Methode area:

    class Square
    {
        public $length;
    
        public function __construct($length)
        {
            $this->length = $length;
        }
    
        public function area()
        {
            return pow($this->length, 2);
        }
    }
    

    Und hier ist die in Circle definierte Methode area:

    class Circle
    {
        public $radius;
    
        public function construct($radius)
        {
            $this->radius = $radius;
        }
    
        public function area()
        {
            return pi() * pow($shape->radius, 2);
        }
    }
    

    Die Methode sum für AreaCalculator kann dann umgeschrieben werden als:

    class AreaCalculator
    {
        // ...
    
        public function sum()
        {
            foreach ($this->shapes as $shape) {
                $area[] = $shape->area();
            }
    
            return array_sum($area);
        }
    }
    

    Jetzt können Sie eine andere Formklasse erstellen und diese bei der Berechnung der Summe übergeben, ohne den Code zu verändern.

    Es ergibt sich jedoch ein weiteres Problem. Woher wissen Sie, dass das an den AreaCalculator übergebene Objekt tatsächlich eine Form ist oder ob die Form eine Methode namens area aufweist?

    Die Codierung auf eine Schnittstelle ist ein integraler Bestandteil von SOLID.

    Erstellen Sie ein ShapeInterface, das area unterstützt:

    interface ShapeInterface
    {
        public function area();
    }
    

    Ändern Sie Ihre Formklassen, um das ShapeInterface mit implement zu implementieren.

    Hier ist die Aktualisierung für Square:

    class Square implements ShapeInterface
    {
        // ...
    }
    

    Und hier ist die Aktualisierung für Circle:

    class Circle implements ShapeInterface
    {
        // ...
    }
    

    In der Methode sum für AreaCalculator können Sie überprüfen, ob die bereitgestellten Formen tatsächlich Instanzen des ShapeInterface sind; andernfalls verwenden Sie „throw“ für eine Ausnahme:

     class AreaCalculator
    {
        // ...
    
        public function sum()
        {
            foreach ($this->shapes as $shape) {
                if (is_a($shape, 'ShapeInterface')) {
                    $area[] = $shape->area();
                    continue;
                }
    
                throw new AreaCalculatorInvalidShapeException();
            }
    
            return array_sum($area);
        }
    }
    

    Damit ist das Open-Closed-Prinzip erfüllt.

    Liskovsches Substitutionsprinzip

    Das Liskovsche Substitutionsprinzip besagt:

    Lassen Sie q(x) eine Eigenschaft sein, die für Objekte x von Typ T beweisbar ist. Dann soll q(y) für Objekte y von Typ S beweisbar sein, wobei S ein Untertyp von T ist.

    Das bedeutet, dass jede Unterklasse oder abgeleitete Klasse für ihre Basis- oder übergeordnete Klasse ersetzbar sein sollte.

    Bedenken Sie, aufbauend auf dem Beispiel der Klasse AreaCalculator, eine neue Klasse VolumeCalculator, die die Klasse AreaCalculator erweitert:

    class VolumeCalculator extends AreaCalculator
    {
        public function construct($shapes = [])
        {
            parent::construct($shapes);
        }
    
        public function sum()
        {
            // logic to calculate the volumes and then return an array of output
            return [$summedData];
        }
    }
    

    Erinnern Sie sich daran, dass die Klasse SumCalculatorOutputter dem ähnelt:

    class SumCalculatorOutputter {
        protected $calculator;
    
        public function __constructor(AreaCalculator $calculator) {
            $this->calculator = $calculator;
        }
    
        public function JSON() {
            $data = array(
                'sum' => $this->calculator->sum();
            );
    
            return json_encode($data);
        }
    
        public function HTML() {
            return implode('', array(
                '',
                    'Sum of the areas of provided shapes: ',
                    $this->calculator->sum(),
                ''
            ));
        }
    }
    

    Wenn Sie versuchen würden, ein Beispiel wie dieses auszuführen:

    $areas = new AreaCalculator($shapes);
    $volumes = new VolumeCalculator($solidShapes);
    
    $output = new SumCalculatorOutputter($areas);
    $output2 = new SumCalculatorOutputter($volumes);
    

    Wenn Sie die Methode HTML auf dem Objekt $output2 aufrufen, erhalten Sie einen Fehler E_NOTICE, der Sie über eine Array-zu-String-Konvertierung informiert.

    Um dies zu beheben, geben Sie anstelle der Rückgabe eines Arrays aus der Summenmethode der Klasse VolumeCalculator $summedData zurück:

    class VolumeCalculator extends AreaCalculator
    {
        public function construct($shapes = [])
        {
            parent::construct($shapes);
        }
    
        public function sum()
        {
            // logic to calculate the volumes and then return a value of output
            return $summedData;
        }
    }
    

    Das $summedData können ein Float, Double oder Integer sein.

    Damit ist das Liskovsche Substitutionsprinzip erfüllt.

    Das Interface-Segregation-Prinzip

    Das Interface-Segregation-Prinzip besagt:

    Ein Client sollte nie gezwungen werden, eine Schnittstelle zu implementieren, die er nicht verwendet, oder Clients sollten nicht gezwungen werden, von Methoden abzuhängen, die sie nicht verwenden.

    Weiterhin aufbauend auf dem vorherigen Beispiel ShapeInterface, müssen Sie die neuen dreidimensionalen Formen Cuboid und Spheroid unterstützen, und diese Formen müssen auch das Volumen berechnen.

    Bedenken wir, was passieren würde, wenn Sie das ShapeInterface modifizieren würden, um einen weiteren Vertrag hinzuzufügen:

    interface ShapeInterface
    {
        public function area();
    
        public function volume();
    }
    

    Nun muss jede Form, die Sie erstellen, die Methode volume implementieren, aber Sie wissen, dass Quadrate flache Formen sind und kein Volumen haben, also würde diese Schnittstelle die Klasse Square zwingen, eine Methode zu implementieren, die sie nicht braucht.

    Dies würde das Interface-Segregation-Prinzip verletzen. Stattdessen könnten Sie eine andere Schnittstelle namens ThreeDimensionalShapeInterface erstellen, die den Vertrag volume hat und dreidimensionale Formen können diese Schnittstelle implementieren:

    interface ShapeInterface
    {
        public function area();
    }
    
    interface ThreeDimensionalShapeInterface
    {
        public function volume();
    }
    
    class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface
    {
        public function area()
        {
            // calculate the surface area of the cuboid
        }
    
        public function volume()
        {
            // calculate the volume of the cuboid
        }
    }
    

    Dies ist ein wesentlich besserer Ansatz, aber ein Fallstrick, auf den Sie achten müssen, wenn Sie diese Schnittstellen mit Typ-Hinweisen versehen. Anstatt ein ShapeInterface oder ein ThreeDimensionalShapeInterface zu verwenden, können Sie eine andere Schnittstelle erstellen, vielleicht ManageShapeInterface, und diese sowohl für die flachen als auch für die dreidimensionalen Formen implementieren.

    Auf diese Weise können Sie eine einzige API für die Verwaltung der Formen haben:

    interface ManageShapeInterface
    {
        public function calculate();
    }
    
    class Square implements ShapeInterface, ManageShapeInterface
    {
        public function area()
        {
            // calculate the area of the square
        }
    
        public function calculate()
        {
            return $this->area();
        }
    }
    
    class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface, ManageShapeInterface
    {
        public function area()
        {
            // calculate the surface area of the cuboid
        }
    
        public function volume()
        {
            // calculate the volume of the cuboid
        }
    
        public function calculate()
        {
            return $this->area();
        }
    }
    

    In der Klasse AreaCalculator können Sie den Aufruf für die Methode area durch calculate ersetzen und außerdem überprüfen, ob das Objekt eine Instanz des ManageShapeInterface und nicht des ShapeInterface ist.

    Das erfüllt das Interface-Segregation-Prinzip.

    Das Dependency-Inversion-Prinzip

    Das Dependency-Inversion-Prinzip besagt:

    Entitäten müssen von Abstraktionen abhängen, nicht von Konkretionen. Es besagt, dass das Modul auf hoher Ebene nicht vom Modul auf niedriger Ebene abhängen darf, sondern diese von Abstraktionen abhängen sollten.

    Dieses Prinzip ermöglicht die Entkopplung.

    Hier ist ein Beispiel für einen PasswordReminder der sich mit einer MySQL-Datenbank verbindet:

    class MySQLConnection
    {
        public function connect()
        {
            // handle the database connection
            return 'Database connection';
        }
    }
    
    class PasswordReminder
    {
        private $dbConnection;
    
        public function __construct(MySQLConnection $dbConnection)
        {
            $this->dbConnection = $dbConnection;
        }
    }
    

    Zuerst ist die MySQLConnection das Modul auf niedriger Ebene, während der PasswordReminder auf hoher Ebene angesiedelt ist, aber gemäß der Definition von D in SOLID, die besagt, von der Abstraktion abzuhängen und nicht von Konkretionen. Dieses obige Snippet verletzt dieses Prinzip, da die Klasse PasswordReminder gezwungen wird, von der Klasse MySQLConnection abzuhängen.

    Wenn Sie später die Datenbank-Engine ändern würden, müssten Sie auch die Klasse PasswordReminder bearbeiten, und das würde das Open-Close-Prinzip verletzen.

    Die Klasse PasswordReminder sollte sich nicht darum kümmern, welche Datenbank Ihre Anwendung verwendet. Um diese Probleme zu beheben, können Sie an eine Schnittstelle kodieren, da Module auf hoher Ebene und niedriger Ebene von der Abstraktion abhängen sollten:

    interface DBConnectionInterface
    {
        public function connect();
    }
    

    Die Schnittstelle hat eine Verbindungsmethode und die Klasse MySQLConnection implementiert diese Schnittstelle. Anstatt die Klasse MySQLConnection im Konstruktor von PasswordReminder, direkt zu typisieren, geben Sie stattdessen das DBConnectionInterface an, und unabhängig davon, welchen Datenbanktyp Ihre Anwendung verwendet, kann die Klasse PasswordReminder ohne Probleme eine Verbindung zur Datenbank herstellen und das Open-Close-Prinzip wird nicht verletzt.

    class MySQLConnection implements DBConnectionInterface
    {
        public function connect()
        {
            // handle the database connection
            return 'Database connection';
        }
    }
    
    class PasswordReminder
    {
        private $dbConnection;
    
        public function __construct(DBConnectionInterface $dbConnection)
        {
            $this->dbConnection = $dbConnection;
        }
    }
    

    Dieser Code verdeutlicht, dass sowohl die Module auf hoher Ebene als auch auf niedriger Ebene von der Abstraktion abhängen.

    Zusammenfassung

    In diesem Artikel wurden Ihnen die fünf Prinzipien von SOLID Code vorgestellt. Projekte, die sich an die SOLID-Prinzipien halten, können mit weniger Komplikationen mit anderen Mitarbeitern geteilt, erweitert, modifiziert, getestet und refraktorisiert werden.

    Lernen Sie weiter, indem Sie über andere Praktiken für die Agile und Adaptive Softwareentwicklung lesen.

    Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

    Learn more about our products

    About the authors
    Default avatar
    Samuel Oloruntoba

    author



    Still looking for an answer?

    Ask a questionSearch for more help

    Was this helpful?
     
    Leave a comment
    

    This textbox defaults to using Markdown to format your answer.

    You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

    Try DigitalOcean for free

    Click below to sign up and get $200 of credit to try our products over 60 days!

    Sign up

    Join the Tech Talk
    Success! Thank you! Please check your email for further details.

    Please complete your information!

    Become a contributor for community

    Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

    DigitalOcean Documentation

    Full documentation for every DigitalOcean product.

    Resources for startups and SMBs

    The Wave has everything you need to know about building a business, from raising funding to marketing your product.

    Get our newsletter

    Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

    New accounts only. By submitting your email you agree to our Privacy Policy

    The developer cloud

    Scale up as you grow — whether you're running one virtual machine or ten thousand.

    Get started for free

    Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

    *This promotional offer applies to new accounts only.