Implementierung der SOLID-Prinzipien für C

Lesezeit: 8 Minuten

Benutzer-Avatar
Oskar Castiblanco

Ich weiß, dass SOLID-Prinzipien für objektorientierte Sprachen geschrieben wurden.

Ich habe in dem Buch: “Test Driven Development for Embedded C” von Robert Martin im letzten Kapitel des Buches folgenden Satz gefunden:

„Die Anwendung des Open-Closed-Prinzips und des Liskov-Substitutionsprinzips ermöglicht flexiblere Designs.“

Da dies ein Buch über C ist (kein C++ oder C#), sollte es eine Möglichkeit geben, diese Prinzipien zu implementieren.

Gibt es einen Standardweg zur Implementierung dieser Prinzipien in C?

  • Warten auf die Antwort. Hatte beim Entwerfen in C wirklich viel zu kämpfen.

    – Pranit Kothari

    15. Juli 2013 um 6:53 Uhr


  • Vielleicht möchten Sie diesen Artikel überprüfen … Der Link in diesem Artikel enthält ziemlich gute Informationen codeproject.com/Articles/60845/…

    – NullPoièteя

    15. Juli 2013 um 7:22 Uhr


  • Warum ist dies als C++ gekennzeichnet?

    – JVApen

    9. Juni 2018 um 14:07 Uhr

Benutzer-Avatar
Joni

Auf-Zu-Prinzip besagt, dass ein System so konzipiert sein sollte, dass es für Erweiterungen offen ist, während es vor Änderungen geschützt ist, oder dass es verwendet und erweitert werden kann, ohne es zu ändern. Ein I/O-Subsystem, wie von Dennis erwähnt, ist ein ziemlich häufiges Beispiel: In einem wiederverwendbaren System sollte der Benutzer in der Lage sein, festzulegen, wie Daten gelesen und geschrieben werden, anstatt anzunehmen, dass Daten beispielsweise nur in Dateien geschrieben werden können.

Wie Sie dies implementieren, hängt von Ihren Bedürfnissen ab: Sie können dem Benutzer erlauben, einen offenen Dateideskriptor oder ein Handle zu übergeben, was bereits die Verwendung von Sockets oder Pipes zusätzlich zu Dateien ermöglicht. Oder Sie können dem Benutzer erlauben, Zeiger auf die Funktionen zu übergeben, die zum Lesen und Schreiben verwendet werden sollen: Auf diese Weise könnte Ihr System zusätzlich zu dem, was das Betriebssystem zulässt, mit verschlüsselten oder komprimierten Datenströmen verwendet werden.

Das Substitutionsprinzip von Liskov besagt, dass es immer möglich sein sollte, einen Typ durch einen Untertyp zu ersetzen. In C gibt es nicht oft Untertypen, aber Sie können das Prinzip auf Modulebene anwenden: Code sollte so gestaltet sein, dass die Verwendung einer erweiterten Version eines Moduls, wie einer neueren Version, es nicht beschädigen sollte. Eine erweiterte Version eines Moduls kann a verwenden struct das hat mehr Felder als das Original, mehr Felder in einem enumund ähnliche Dinge, sodass Ihr Code nicht davon ausgehen sollte, dass eine übergebene Struktur eine bestimmte Größe hat oder dass Aufzählungswerte ein bestimmtes Maximum haben.

Ein Beispiel dafür ist die Implementierung von Socket-Adressen in der BSD-Socket-API: Dort gibt es einen “abstrakten” Socket-Typ struct sockaddr die für jeden Socket-Adresstyp stehen kann, und einen konkreten Socket-Typ für jede Implementierung, wie z struct sockaddr_un für Unix-Domain-Sockets und struct sockaddr_in für IP-Steckdosen. Funktionen, die mit Socket-Adressen arbeiten, muss ein Zeiger auf die Daten übergeben werden und die Größe des konkreten Adresstyps.

  • Könntest du nicht Liskow / Modulbeispiel als “Offen für Erweiterung, geschlossen für Änderung” betrachtet werden?

    – Elliot

    26. September 2021 um 13:09 Uhr


Benutzer-Avatar
John Deter

Zunächst hilft es, darüber nachzudenken warum Wir haben diese Designprinzipien. Warum macht das Befolgen der SOLID-Prinzipien Software besser? Arbeiten Sie daran, die Ziele jedes Prinzips zu verstehen und nicht nur die spezifischen Implementierungsdetails, die erforderlich sind, um sie mit einer bestimmten Sprache zu verwenden.

  • Das Single-Responsibility-Prinzip verbessert die Modularität, indem es die Kohäsion erhöht; bessere Modularität führt zu verbesserter Testbarkeit, Benutzerfreundlichkeit und Wiederverwendbarkeit.
  • Das Open/Closed-Prinzip ermöglicht eine asynchrone Bereitstellung, indem Implementierungen voneinander entkoppelt werden.
  • Das Liskov-Substitutionsprinzip fördert die Modularität und Wiederverwendung von Modulen, indem es die Kompatibilität ihrer Schnittstellen sicherstellt.
  • Das Interface-Segregations-Prinzip reduziert die Kopplung zwischen unabhängigen Benutzern der Schnittstelle, während es die Lesbarkeit und Verständlichkeit erhöht.
  • Das Abhängigkeitsinversionsprinzip reduziert die Kopplung und ermöglicht eine starke Testbarkeit.

Beachten Sie, wie jedes Prinzip eine Verbesserung eines bestimmten Attributs des Systems vorantreibt, sei es eine höhere Kohäsion, eine lockerere Kopplung oder Modularität.

Denken Sie daran, Ihr Ziel ist es, qualitativ hochwertige Software zu produzieren. Qualität setzt sich aus vielen verschiedenen Attributen zusammen, darunter Korrektheit, Effizienz, Wartbarkeit, Verständlichkeit usw. Wenn Sie sie befolgen, helfen Ihnen die SOLID-Prinzipien dabei, dorthin zu gelangen. Sobald Sie also das „Warum“ der Prinzipien verstanden haben, wird das „Wie“ der Umsetzung viel einfacher.

BEARBEITEN:

Ich werde versuchen, Ihre Frage direkter zu beantworten.

Für das Open/Close-Prinzip gilt, dass sowohl die Signatur als auch das Verhalten der alten Schnittstelle vor und nach Änderungen gleich bleiben müssen. Unterbrechen Sie keinen Code, der ihn aufruft. Das bedeutet, dass es unbedingt eine neue Schnittstelle braucht, um das neue Zeug zu implementieren, weil das alte Zeug bereits ein Verhalten hat. Die neue Schnittstelle muss eine andere Signatur haben, weil sie die neue und andere Funktionalität bietet. Sie erfüllen diese Anforderungen also in C genauso wie in C++.

Nehmen wir an, Sie haben eine Funktion int foo(int a, int b, int c) und Sie möchten eine Version hinzufügen, die fast genau gleich ist, aber einen vierten Parameter wie diesen benötigt: int foo(int a, int b, int c, int d). Es ist eine Voraussetzung, dass die neue Version mit der alten Version abwärtskompatibel ist und dass ein Standardwert (z. B. Null) für den neuen Parameter dies ermöglicht. Sie würden den Implementierungscode vom alten foo in das neue foo verschieben, und in Ihrem alten foo würden Sie Folgendes tun: int foo(int a, int b, int c) { return foo(a, b, c, 0);} Obwohl wir den Inhalt von radikal verändert haben int foo(int a, int b, int c), haben wir seine Funktionalität beibehalten. Es blieb für Veränderungen verschlossen.

Das Liskov-Substitutionsprinzip besagt, dass verschiedene Subtypen kompatibel funktionieren müssen. Mit anderen Worten, die Dinge mit gemeinsamen Signaturen, die gegeneinander ausgetauscht werden können, müssen sich rational gleich verhalten.

In C kann dies mit Funktionszeigern auf Funktionen erreicht werden, die identische Parametersätze verwenden. Angenommen, Sie haben diesen Code:

#include <stdio.h>
void fred(int x)
{
    printf( "fred %d\n", x );
}
void barney(int x)
{
    printf( "barney %d\n", x );
}

#define Wilma 0
#define Betty 1

int main()
{

    void (*flintstone)(int);

    int wife = Betty;
    switch(wife)
    {
    case Wilma:
        flintstone = &fred;
    case Betty:
        flintstone = &barney;
    }

    (*flintstone)(42);

    return 0;
}

fred() und barney() müssen natürlich kompatible Parameterlisten haben, damit dies funktioniert, aber das ist nicht anders, als wenn Unterklassen ihre vtable von ihren Oberklassen erben. Teil des Verhaltensvertrags wäre, dass sowohl fred() als auch barney() keine versteckten Abhängigkeiten haben sollten oder, falls doch, sie auch kompatibel sein müssten. In diesem vereinfachten Beispiel verlassen sich beide Funktionen nur auf stdout, also ist es keine große Sache. Die Idee ist, dass Sie das korrekte Verhalten in beiden Situationen beibehalten, in denen beide Funktionen austauschbar verwendet werden könnten.

  • Danke für deinen Vorschlag, aber er beantwortet die Frage nicht. Ich kenne das Warum gut, aber ich frage nach dem Wie in einem speziellen Kontext: nicht objektorientierte Sprache.

    – Oscar Castiblanco

    15. Juli 2013 um 20:42 Uhr

  • Ihr Vorschlag ist, das Fabrikmuster zu verwenden, um das Liskov-Substitutionsprinzip zu erreichen?. Ich finde es ist eine sehr gute Idee danke.

    – Oscar Castiblanco

    17. Juli 2013 um 8:14 Uhr

Das Nächstliegende, was mir auf Anhieb einfällt (und es ist nicht perfekt, also wenn jemand eine viel bessere Idee hat, kann er mich gerne eins-zu-eins machen) ist meistens, wenn ich Funktionen für eine Art Bibliothek schreibe .

Wenn Sie für die Liskov-Substitution eine Header-Datei haben, die eine Reihe von Funktionen definiert, möchten Sie nicht, dass die Funktionalität dieser Bibliothek davon abhängt die Implementierung der Funktionen, die Sie haben; Sie sollten in der Lage sein, jede vernünftige Implementierung zu verwenden und erwarten, dass Ihr Programm seine Aufgabe erfüllt.

Was das Open/Closed-Prinzip angeht, wenn Sie eine E/A-Bibliothek implementieren möchten, möchten Sie Funktionen haben, die das Nötigste tun (wie z read und write). Gleichzeitig möchten Sie diese möglicherweise verwenden, um ausgefeiltere I/O-Funktionen (wie z scanf und printf), aber Sie werden den Code, der das Nötigste getan hat, nicht ändern.

Ich sehe, es ist eine Weile her, seit die Frage geöffnet wurde, aber ich denke, es lohnt sich, einen neueren Blick darauf zu werfen.

Die fünf SOLID-Prinzipien beziehen sich auf die fünf Aspekte einer Software-Entität, wie sie in der dargestellt sind Solides Diagramm. Obwohl dies ein Klassendiagramm ist, kann es grundsätzlich andere Arten von SW-Identitäten bedienen. Schnittstellen, die für Aufrufer verfügbar gemacht werden (der linke Pfeil steht für Interface Segregation) und Schnittstellen, die als Aufgerufene angefordert werden (der rechte Pfeil steht für Dependency Inversion), können genauso gut klassische C-Funktionen und Argumentschnittstellen sein.

Der obere Pfeil (Erweiterungspfeil, steht für das Liskov-Substitutionsprinzip) funktioniert für jede andere Implementierung einer ähnlichen Entität. Wenn Sie beispielsweise eine API für eine verknüpfte Liste haben, können Sie die Implementierung ihrer Funktionen und sogar die Struktur des Vektor-„Objekts“ ändern (vorausgesetzt, dass beispielsweise die Struktur des Originals beibehalten wird, wie in der BSD Sockets, oder es handelt sich um einen undurchsichtigen Typ). Das ist zwar nicht so elegant wie ein Objekt in einer OOP-Sprache, aber es folgt dem gleichen Prinzip und kann beispielsweise mit dynamischer Verknüpfung verwendet werden.

In ähnlicher Weise definiert der untere Pfeil (Verallgemeinerungspfeil, steht für das Öffnen/Schließen-Prinzip), was von Ihrer Entität definiert wird und was offen ist. Beispielsweise können einige Funktionen in einer Datei definiert sein und sollten nicht ersetzt werden, während andere Funktionen möglicherweise einen anderen Satz von APIs aufrufen, was die Verwendung unterschiedlicher Implementierungen ermöglicht.

Auf diese Weise können Sie SOLID SW auch mit C schreiben, obwohl dies wahrscheinlich mit Entitäten höherer Ebenen durchgeführt wird und möglicherweise etwas mehr Engineering erfordert.

1106910cookie-checkImplementierung der SOLID-Prinzipien für C

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy