Wie wende ich das Prinzip der Schnittstellentrennung in C an?

15

Ich habe ein Modul, sagen Sie "M", das ein paar Clients hat, sagen Sie "C1", "C2", "C3". Ich möchte den Namespace des Moduls M, dh die Deklarationen der von ihm verfügbar gemachten APIs und Daten, so in Header-Dateien unterteilen, dass:

  1. Für jeden Client sind nur die erforderlichen Daten und APIs sichtbar. Der restliche Namespace des Moduls ist dem Client verborgen, dh es wird das Prinzip der Schnittstellensegregation befolgt .
  2. Eine Deklaration wird in mehreren Header-Dateien nicht wiederholt, verstößt also nicht gegen DRY .
  3. Modul M hat keine Abhängigkeiten von seinen Clients.
  4. Ein Kunde bleibt von den Änderungen unberührt, die an Teilen des Moduls M vorgenommen werden, die nicht von ihm verwendet werden.
  5. Bestehende Kunden bleiben von der Hinzufügung (oder Löschung) weiterer Kunden unberührt.

Derzeit beschäftige ich mich damit, indem ich den Namespace des Moduls in Abhängigkeit von den Anforderungen seiner Kunden teile. In der folgenden Abbildung werden beispielsweise die verschiedenen Teile des Namensraums des Moduls angezeigt, die von seinen drei Clients benötigt werden. Kundenanforderungen überschneiden sich. Der Namespace des Moduls ist in 4 separate Header-Dateien unterteilt: 'a', '1', '2' und '3' .

Modul-Namespace-Partitionierung

Dies verletzt jedoch einige der oben genannten Anforderungen, dh R3 und R5. Anforderung 3 wird verletzt, da diese Partitionierung von der Art der Clients abhängt. auch beim Hinzufügen eines neuen Clients ändert sich diese Partitionierung und verstößt gegen Anforderung 5. Wie auf der rechten Seite des obigen Bildes zu sehen ist, ist der Namespace des Moduls beim Hinzufügen eines neuen Clients jetzt in 7 Header-Dateien unterteilt - 'a ',' b ',' c ',' 1 ',' 2 * ',' 3 * 'und' 4 ' . Die Header-Dateien, die für 2 der älteren Clients bestimmt sind, ändern sich und lösen dadurch deren Neuerstellung aus.

Gibt es eine Möglichkeit, die Schnittstellentrennung in C auf nicht konstruierte Weise zu erreichen?
Wenn ja, wie würden Sie mit dem obigen Beispiel umgehen?

Ich stelle mir eine unwirkliche hypothetische Lösung vor:
Das Modul hat eine fette Header-Datei, die den gesamten Namespace abdeckt. Diese Header-Datei ist wie eine Wikipedia-Seite in adressierbare Abschnitte und Unterabschnitte unterteilt. Jeder Client hat dann eine spezielle Header-Datei, die auf ihn zugeschnitten ist. Die kundenspezifischen Header-Dateien sind lediglich eine Liste von Hyperlinks zu den Abschnitten / Unterabschnitten der Fat-Header-Datei. Das Build-System muss eine clientspezifische Header-Datei als "geändert" erkennen, wenn einer der Abschnitte, auf die es im Header des Moduls verweist, geändert wird.

work.bin
quelle
1
Warum ist dieses Problem spezifisch für C? Liegt es daran, dass C keine Vererbung hat?
Robert Harvey
Wird Ihr Design durch Verstöße gegen den Internetdienstanbieter verbessert?
Robert Harvey
2
C unterstützt keine OOP-Konzepte (wie Schnittstellen oder Vererbung). Wir kommen mit rohen (aber kreativen) Hacks aus. Auf der Suche nach einem Hack, um Interfaces zu simulieren. In der Regel ist die gesamte Headerdatei die Schnittstelle zu einem Modul.
work.bin
1
structverwenden Sie in C, wenn Sie eine Schnittstelle benötigen. Zugegeben, Methoden sind etwas schwierig. Das könnte Sie interessieren: cs.rit.edu/~ats/books/ooc.pdf
Robert Harvey
Ich konnte nicht kommen mit einer Schnittstelle gleichwertig mit structund function pointers.
work.bin

Antworten:

5

Die Schnittstellentrennung sollte im Allgemeinen nicht auf Kundenanforderungen basieren. Sie sollten den gesamten Ansatz ändern, um dies zu erreichen. Ich würde sagen, modularisieren Sie die Schnittstelle, indem Sie die Features in zusammenhängende Gruppen gruppieren. Das heißt, die Gruppierung basiert auf der Kohärenz der Features selbst und nicht auf den Kundenanforderungen. In diesem Fall verfügen Sie über eine Reihe von Schnittstellen, I1, I2 usw. Der Client C1 kann I2 allein verwenden. Client C2 kann I1 und I5 usw. verwenden. Beachten Sie, dass es kein Problem ist, wenn ein Client mehr als ein Ii verwendet. Wenn Sie die Schnittstelle in zusammenhängende Module zerlegt haben, ist das der Kern der Sache.

Auch hier ist ISP nicht clientbasiert. Es geht darum, die Schnittstelle in kleinere Module zu zerlegen. Wenn dies ordnungsgemäß durchgeführt wird, wird auch sichergestellt, dass Clients mit so wenigen Funktionen wie nötig konfrontiert werden.

Mit diesem Ansatz können Ihre Kunden auf eine beliebige Anzahl aufgestockt werden, ohne dass Sie M davon betroffen sind. Jeder Client verwendet je nach Bedarf eine oder mehrere der Schnittstellen. Gibt es Fälle, in denen ein Client (C) beispielsweise I1 und I3 einschließen muss, aber nicht alle Funktionen dieser Schnittstellen nutzt? Ja, das ist kein Problem. Es werden nur die wenigsten Schnittstellen verwendet.

Nazar Merza
quelle
Sie meinten sicherlich disjunkte oder nicht überlappende Gruppen, nehme ich an?
Doc Brown
Ja, disjunkt und nicht überlappend.
Nazar Merza
3

Das Prinzip der Schnittstellentrennung lautet:

Kein Client sollte gezwungen werden, sich auf Methoden zu verlassen, die er nicht verwendet. ISP teilt sehr große Schnittstellen in kleinere und spezifischere auf, sodass die Kunden nur die Methoden kennen müssen, die für sie von Interesse sind.

Hier sind einige Fragen offen. Eins ist:

Wie klein?

Du sagst:

Gegenwärtig beschäftige ich mich damit, indem ich den Namespace des Moduls in Abhängigkeit von den Anforderungen seiner Kunden teile.

Ich nenne das manuelle Entenschreiben . Sie erstellen Schnittstellen, die nur die Anforderungen eines Clients offen legen. Das Prinzip der Schnittstellentrennung besteht nicht nur in der manuellen Eingabe von Enten.

Der ISP ist jedoch nicht nur ein Aufruf für "kohärente" Rollenschnittstellen, die wiederverwendet werden können. Kein "kohärentes" Design der Rollenschnittstelle kann perfekt verhindern, dass ein neuer Client mit eigenen Rollenanforderungen hinzugefügt wird.

ISP ist eine Möglichkeit, Clients von den Auswirkungen von Änderungen am Dienst zu isolieren. Damit sollte der Build schneller ausgeführt werden, wenn Sie Änderungen vornehmen. Sicher hat es andere Vorteile, wie nicht Kunden zu brechen, aber das war der Hauptpunkt. Wenn ich die count()Signatur der Services- Funktion ändere, ist es hilfreich, wenn Clients, die sie nicht verwenden count(), nicht bearbeitet und neu kompiliert werden müssen.

Dies ist der Grund, warum mir das Prinzip der Schnittstellentrennung am Herzen liegt. Es ist nicht etwas, was ich für wichtig halte. Es löst ein echtes Problem.

So sollte die Art und Weise, wie es angewendet werden sollte, ein Problem für Sie lösen. Es gibt keine hirntote Möglichkeit, ISP anzuwenden, die nicht mit dem richtigen Beispiel für eine notwendige Änderung besiegt werden kann. Sie sollten sich ansehen, wie sich das System ändert, und Entscheidungen treffen, die die Dinge beruhigen. Lassen Sie uns die Optionen untersuchen.

Stellen Sie sich zunächst die Frage, ob es derzeit schwierig ist, Änderungen an der Service-Oberfläche vorzunehmen. Wenn nicht, geh nach draußen und spiele, bis du dich beruhigt hast. Dies ist keine intellektuelle Übung. Bitte stellen Sie sicher, dass die Heilung nicht schlimmer ist als die Krankheit.

  1. Wenn viele Clients dieselbe Teilmenge von Funktionen verwenden, spricht dies für "kohärente" wiederverwendbare Schnittstellen. Die Teilmenge konzentriert sich wahrscheinlich auf eine Idee, die wir uns als die Rolle vorstellen können, die der Service für den Kunden bereitstellt. Es ist schön, wenn das funktioniert. Das funktioniert nicht immer.

  2.  

    1. Wenn viele Clients unterschiedliche Teilmengen von Funktionen verwenden, verwendet der Client den Service möglicherweise tatsächlich über mehrere Rollen. Das ist in Ordnung, aber es macht die Rollen schwer zu sehen. Finde sie und versuche sie auseinander zu ziehen. Das könnte uns in Fall 1 zurückversetzen. Der Client nutzt den Service einfach über mehr als eine Schnittstelle. Bitte fange nicht an, den Dienst zu besetzen. Wenn überhaupt, würde dies bedeuten, dass der Service mehr als einmal an den Kunden weitergegeben wird. Das funktioniert, aber ich frage mich, ob der Service kein großer Schlammballen ist, der aufgebrochen werden muss.

    2. Wenn viele Clients unterschiedliche Teilmengen verwenden, Sie jedoch keine Rollen sehen, bei denen die Clients möglicherweise mehr als eine Rolle verwenden, gibt es nichts Besseres als das Eingeben von Enten für die Gestaltung Ihrer Benutzeroberflächen. Auf diese Weise wird beim Entwerfen der Schnittstellen sichergestellt, dass der Client nicht einer einzigen Funktion ausgesetzt ist, die er nicht verwendet. Es wird jedoch fast garantiert, dass beim Hinzufügen eines neuen Clients immer eine neue Schnittstelle hinzugefügt wird, die die Serviceimplementierung nicht kennen muss darüber die Schnittstelle, die die Rollenschnittstellen aggregiert. Wir haben einfach einen Schmerz gegen einen anderen getauscht.

  3. Wenn viele Clients unterschiedliche Teilmengen verwenden und sich überlappen, wird erwartet, dass neue Clients hinzugefügt werden, die unvorhersehbare Teilmengen benötigen, und Sie nicht bereit sind, den Dienst aufzulösen, sondern eine funktionalere Lösung in Betracht ziehen. Da die ersten beiden Optionen nicht funktionierten und Sie sich wirklich an einem schlechten Ort befinden, an dem nichts einem Muster folgt und weitere Änderungen bevorstehen, sollten Sie erwägen, jeder Funktion eine eigene Benutzeroberfläche bereitzustellen. Das bedeutet nicht, dass der ISP gescheitert ist. Wenn etwas fehlschlug, war es das objektorientierte Paradigma. Einzelmethodenschnittstellen folgen im Extremfall ISP. Es ist ein gutes Stück Tastatur-Eingabe, aber Sie werden feststellen, dass die Schnittstellen dadurch plötzlich wiederverwendbar werden. Stellen Sie sicher, dass es keine gibt.

Es stellt sich also heraus, dass sie tatsächlich sehr klein werden können.

Ich habe diese Frage als Herausforderung angesehen, um ISP in den extremsten Fällen anzuwenden. Bedenken Sie jedoch, dass Extreme am besten vermieden werden. In einem gut durchdachten Design, das andere SOLID-Prinzipien anwendet, treten diese Probleme normalerweise nicht auf oder spielen kaum eine Rolle.


Eine weitere unbeantwortete Frage lautet:

Wem gehören diese Schnittstellen?

Immer wieder sehe ich Schnittstellen, die mit einer "Bibliotheks" -Mentalität entworfen wurden. Wir haben uns alle der Affen-See-Affen-Do-Codierung schuldig gemacht, bei der Sie nur etwas tun, weil Sie es so gesehen haben. Wir sind an der gleichen Sache mit Schnittstellen schuld.

Wenn ich mir ein Interface anschaue, das für eine Klasse in einer Bibliothek entworfen wurde, habe ich gedacht: Oh, diese Leute sind Profis. Dies muss der richtige Weg sein, um eine Schnittstelle zu erstellen. Was ich nicht verstanden habe, ist, dass eine Bibliotheksgrenze ihre eigenen Bedürfnisse und Probleme hat. Zum einen kennt eine Bibliothek das Design ihrer Kunden überhaupt nicht. Nicht jede Grenze ist gleich. Und manchmal kann sogar dieselbe Grenze auf unterschiedliche Weise überschritten werden.

Es gibt zwei einfache Möglichkeiten, das Interface-Design zu betrachten:

  • Service-Schnittstelle. Einige Leute entwerfen jede Schnittstelle, um alles darzustellen, was ein Dienst tun kann. Sie können sogar Refactoring-Optionen in IDEs finden, die eine Schnittstelle für Sie schreiben, die die von Ihnen angegebene Klasse verwendet.

  • Client-Schnittstelle. Der ISP scheint zu argumentieren, dass dies richtig und der Service-Besitz falsch ist. Sie sollten jede Schnittstelle im Hinblick auf die Kundenanforderungen aufteilen. Da der Client die Schnittstelle besitzt, sollte er sie definieren.

Also, wer hat Recht?

Plugins berücksichtigen:

Bildbeschreibung hier eingeben

Wem gehören die Schnittstellen hier? Die Kunden? Die Dienste?

Es stellt sich heraus, beide.

Die Farben hier sind Schichten. Die rote Schicht (rechts) soll nichts über die grüne Schicht (links) wissen. Die grüne Schicht kann geändert oder ersetzt werden, ohne die rote Schicht zu berühren. Auf diese Weise kann eine beliebige grüne Schicht in die rote Schicht eingesteckt werden.

Ich mag es zu wissen, was darüber wissen soll und was nicht. Für mich ist "Was weiß wovon?" Die wichtigste architektonische Frage.

Lassen Sie uns einige Vokabeln klarstellen:

[Client] --> [Interface] <|-- [Service]

----- Flow ----- of ----- control ---->

Ein Client ist etwas, das verwendet.

Ein Service wird genutzt.

Interactor zufällig beides.

Laut ISP werden Schnittstellen für Clients getrennt. Gut, lassen Sie uns das hier anwenden:

  • Presenter(ein Dienst) sollte der Output Port <I>Schnittstelle nicht diktieren . Die Schnittstelle sollte auf die InteractorAnforderungen (hier als Client) beschränkt werden. Das bedeutet, dass sich die Schnittstelle, die über das Internet Interactorund den Internetdienstanbieter Bescheid weiß, damit ändern muss. Und das ist in Ordnung.

  • Interactor(hier als Dienst agierend) sollte der Input Port <I>Schnittstelle nicht diktieren . Die Schnittstelle sollte auf die ControllerBedürfnisse (eines Kunden) beschränkt werden. Das bedeutet, dass sich die Schnittstelle, die über das Internet Controllerund den Internetdienstanbieter Bescheid weiß, damit ändern muss. Und das ist nicht in Ordnung.

Die zweite ist nicht in Ordnung, weil die rote Schicht nichts über die grüne Schicht wissen soll. Ist ISP also falsch? Na irgendwie. Kein Prinzip ist absolut. Dies ist ein Fall, bei dem sich herausstellt, dass die Benutzeroberfläche, die alles zeigt, was der Service kann, in Ordnung ist.

Zumindest haben sie recht, wenn Interactorsie nichts anderes tun, als dies für den Anwendungsfall erforderlich ist. Wenn die InteractorDinge für andere Anwendungsfälle erledigt werden, gibt es keinen Grund, warum dies Input Port <I>bekannt sein sollte. Ich bin mir nicht sicher, warum ich mich Interactornicht nur auf einen Anwendungsfall konzentrieren kann. Dies ist kein Problem, aber es passiert etwas.

Aber das input port <I>Interface kann sich einfach nicht dem ControllerClient unterordnen und muss ein echtes Plugin sein. Dies ist eine Bibliotheksgrenze. Ein völlig anderer Programmierladen könnte die grüne Schicht Jahre nach der Veröffentlichung der roten Schicht schreiben.

Wenn Sie eine Bibliotheksgrenze überschreiten und das Gefühl haben, ISP anwenden zu müssen, obwohl Ihnen die Schnittstelle auf der anderen Seite nicht gehört, müssen Sie einen Weg finden, die Schnittstelle einzugrenzen, ohne sie zu ändern.

Eine Möglichkeit, dies zu erreichen, ist ein Adapter. Setzen Sie es zwischen Clients wie Controlerund der Input Port <I>Schnittstelle. Der Adapter übernimmt Interactorals ein Input Port <I>und die Arbeit delegiert. ControllerÜber eine Rollenschnittstelle oder Schnittstellen, die der grünen Ebene gehören , werden jedoch nur die Anforderungen der Clients offengelegt. Der Adapter folgt nicht dem ISP selbst, sondern ermöglicht komplexeren Klassen Controller, ISP zu genießen. Dies ist nützlich, wenn es weniger Adapter gibt, als solche Clients Controllerverwenden, und wenn Sie sich in einer ungewöhnlichen Situation befinden, in der Sie eine Bibliotheksgrenze überschreiten und die Bibliothek trotz Veröffentlichung nicht aufhört, sich zu ändern. Ich schaue dich an Firefox. Jetzt zerstören diese Änderungen nur Ihre Adapter.

Also, was bedeutet das? Es bedeutet ehrlich gesagt, dass Sie nicht genügend Informationen bereitgestellt haben, damit ich Ihnen sagen kann, was Sie tun sollten. Ich weiß nicht, ob Sie ein Problem haben, wenn Sie ISP nicht folgen. Ich weiß nicht, ob das Folgen nicht zu weiteren Problemen führen würde.

Ich weiß, dass Sie nach einem einfachen Leitsatz suchen. ISP versucht das zu sein. Aber es bleibt viel ungesagt. Ich glaube daran. Ja, bitte zwingen Sie Kunden nicht, sich ohne guten Grund auf Methoden zu verlassen, die sie nicht anwenden!

Wenn Sie einen guten Grund haben, z. B. das Entwerfen von Plugins, müssen Sie sich der Probleme bewusst sein, die nicht den Ursachen des Internetdienstanbieters entsprechen (es ist schwierig, Änderungen vorzunehmen, ohne die Clients zu beschädigen), und der Möglichkeiten, diese zu entschärfen (behalten Interactoroder sich zumindest Input Port <I>auf einen Stall zu konzentrieren) Anwendungsfall).

kandierte_orange
quelle
Danke für die Eingabe. Ich habe ein Dienstbereitstellungsmodul, das mehrere Clients hat. Sein Namespace hat logisch zusammenhängende Grenzen, aber die Clientanforderungen überschreiten diese logischen Grenzen. Das Aufteilen des Namensraums anhand logischer Grenzen hilft dem ISP dabei nicht. Ich habe daher den Namespace auf Basis der Kundenbedürfnisse aufgeteilt, wie im Diagramm in der Frage gezeigt. Dies macht es jedoch abhängig von Clients und einer schlechten Art, Clients an den Dienst zu koppeln, da Clients relativ häufig hinzugefügt / entfernt werden könnten, die Änderungen am Dienst jedoch minimal sind.
work.bin
Ich neige jetzt zu dem Dienst, der eine umfangreiche Schnittstelle bereitstellt, wie in seinem vollständigen Namespace, und es ist Sache des Kunden, über kundenspezifische Adapter auf diese Dienste zuzugreifen. In C-Begriffen wäre dies eine Datei mit Funktions-Wrappern, die dem Client gehören. Änderungen am Dienst würden eine Neukompilierung des Adapters erzwingen, jedoch nicht unbedingt des Clients. .. <Forts.>
work.bin
<contd> .. Dadurch werden die Erstellungszeiten auf jeden Fall minimiert und die Kopplung zwischen Client und Service auf Kosten der Laufzeit (Aufruf einer Intermediary-Wrapper-Funktion) "locker" gehalten, der Code-Speicherplatz vergrößert, die Stack-Nutzung erhöht und möglicherweise der Mindspace erhöht (Programmierer) bei der Wartung der Adapter.
work.bin
Meine derzeitige Lösung erfüllt jetzt meine Anforderungen, der neue Ansatz wird mehr Aufwand erfordern und möglicherweise gegen YAGNI verstoßen. Ich muss die Vor- und Nachteile jeder Methode abwägen und entscheiden, welchen Weg ich hier einschlagen möchte.
work.bin
1

Also dieser Punkt:

existent clients are unaffected by the addition (or deletion) of more clients.

Gibt auf, dass Sie gegen ein anderes wichtiges Prinzip verstoßen, nämlich YAGNI. Ich würde mich darum kümmern, wenn ich Hunderte von Kunden habe. Wenn Sie im Vorfeld über etwas nachdenken, stellt sich heraus, dass Sie für diesen Code keine zusätzlichen Clients haben, ist dies besser als der Zweck.

Zweite

 partitioning depends on the nature of clients

Warum verwendet Ihr Code kein DI, keine Abhängigkeitsinversion, nichts, nichts in Ihrer Bibliothek sollte von Ihrer Client-Natur abhängen.

Schließlich scheint es, als bräuchten Sie eine zusätzliche Ebene unter Ihrem Code, um die Anforderungen für überlappende Inhalte zu erfüllen (DI, sodass Ihr Code für die Vorderseite nur von dieser zusätzlichen Ebene und Ihre Kunden nur von Ihrer Schnittstelle für die Vorderseite abhängt). Auf diese Weise schlagen Sie den DRY.
Das würdest du wirklich tun. Also machst du dasselbe Zeug, das du in deiner Modulebene unter einem anderen Modul verwendest. Auf diese Weise erreichen Sie mit der Schicht darunter:

Für jeden Client sind nur die erforderlichen Daten und APIs sichtbar. Der restliche Namespace des Moduls ist dem Client verborgen, dh es wird das Prinzip der Schnittstellensegregation befolgt.

Ja

Eine Deklaration wird in mehreren Header-Dateien nicht wiederholt, verstößt also nicht gegen DRY. Modul M hat keine Abhängigkeiten von seinen Clients.

Ja

Ein Kunde ist von den Änderungen an Teilen des Moduls M, die er nicht verwendet, nicht betroffen.

Ja

Bestehende Kunden bleiben von der Hinzufügung (oder Löschung) weiterer Kunden unberührt.

Ja

Mateusz
quelle
1

In der Definition werden immer die gleichen Angaben wie in der Deklaration wiederholt. So funktioniert diese Sprache. Ebenfalls, Wiederholen einer Deklaration in mehreren Header-Dateien verletzt DRY nicht . Es ist eine ziemlich häufig verwendete Technik (zumindest in der Standardbibliothek).

Das Wiederholen der Dokumentation oder der Implementierung würde gegen DRY verstoßen .

Ich würde mich nicht damit beschäftigen, wenn der Client-Code nicht von mir geschrieben wird.

Maciej Chałapuk
quelle
0

Ich lehne meine Verwirrung ab. Ihr praktisches Beispiel zieht jedoch eine Lösung in meinen Kopf. Wenn ich es in meine eigenen Worte fassen kann: Alle Partitionen im Modul Mhaben viele zu viele exklusive Beziehung zu jedem und allen Kunden.

Beispielstruktur

M.h      // fat header
 - P1    // Partition 1
 - P2    // ... 2
   - P21 // ... 2 section 1
 - P3    // ... 3
C1.c     // Client 1 (Needs to include P1, P3)
C2.c     // ... 2 (Needs to include P2)
C3.c     // ... 3 (Needs to include P1, P21, P3)

Mh

#ifdef P1
#define _PREF_ P1_             // Define Prefix ("PREF") = P1_
 void _PREF_init();            // Some partition specific function
#endif /* P1 */

#ifdef P2
#define _PREF_ P2_
 void _PREF_init();
#endif /* P2 */

#if defined(P21) || defined (P2) // Part 2.1
#define _PREF_ P2_1_
 void _PREF_oddone();
#endif /* P21 */

#ifdef P3
#define _PREF_ P3_
 void _PREF_init();
#endif /* P3 */

Mc

In der Mc-Datei müssten Sie die #ifdefs nicht verwenden, da das, was Sie in die .c-Datei einfügen, die Client-Dateien nicht beeinflusst, solange die von den Client-Dateien verwendeten Funktionen definiert sind.

#include "M.h"
#define _PREF_ P1_        
void _PREF_init() { ... };

#define _PREF_ P2_
void _PREF_init() { ... }

#define _PREF_ P2_1_
void _PREF_oddone() { ... }

#define _PREF_ P3_
void _PREF_init() { ... }

C1.c

#define P1     // "invite" P1
#define P3     // "invite" P3
#include "M.h" // Open the door, but only the invited come in.

void main()
{
    P1_init();
    //P2_init();
    //P2_1_oddone();
    P3_init();
}

C2.c

#define P2
#include "M.h

void main()
{
    //P1_init();
    P2_init();
    P2_1_oddone();
    //P3_init();
}

C3.c

#define P1
#define P21
#define P3  
#include "M.h" 

void main()
{
    P1_init();
    //P2_init();
    P2_1_oddone();
    P3_init();
}

Auch hier bin ich mir nicht sicher, ob Sie danach fragen. Also nimm es mit einem Körnchen Salz.

Sanchke Dellowar
quelle
Wie sieht Mc aus? Definierst du P1_init() und P2_init() ?
work.bin
@ work.bin Ich gehe davon aus, dass Mc wie eine einfache .c-Datei aussehen würde, mit der Ausnahme, dass der Namespace zwischen Funktionen definiert wird.
Sanchke Dellowar
Angenommen, es gibt sowohl C1 als auch C2 - was bedeutet P1_init()und P2_init()womit verknüpft?
work.bin
In der Mh / Mc-Datei wird der Präprozessor durch den _PREF_zuletzt definierten ersetzt. Das _PREF_init()liegt auch P1_init()an der letzten #define-Anweisung. Dann setzt die nächste definiere Anweisung PREF gleich P2_ und erzeugt so P2_init().
Sanchke Dellowar