By Samuel Oloruntoba
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.
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:
$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.
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.
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 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 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.
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.
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!