Wilkening-Online Logo

Verwenden Sie keine Bool-Typen in Schnittstellen



Von Detlef Wilkening
04.01.2014
Version: 2

1. Regel: Verwenden Sie kein "bool" in Schnittstellen

Okay, ich gebe zu - diese Regel ist sehr krass und provozierend - aber das soll sie auch sein. Denn es ist mehr als ein kleines Stück Wahrheit an ihr. Schauen wir uns mal ein kleines Beispiel an - eine einfache Funktion "set_visible", die einen GUI Check-Button sichtbar bzw. unsichtbar schaltet.

class check_button
{
public:
   ...
   void set_visible(bool);
   ...
};


check_button cb;
...
cb.set_visible(true);
...
cb.set_visible(false);

Was soll daran schlecht sein? Es ist ohne Dokumentation sofort klar, was die Funktion bewirkt und wie sie zu nutzen ist. Und auch die Aufrufe sind problemlos lesbar. Was soll daran schlecht sein?

Schaun wir uns ein zweites Beispiel an. Hier werden zwei Check-Buttons erzeugt, jeweils mit Titel und den Informationen ob der Check-Button "checked" bzw. "unchecked", "grayed" bzw. "non-grayed", "enabled" bzw. "disabled" und "visible" bzw. "invisible" ist.

class check_button
{
public:
   explicit check_button(
      const std::string& title,
      bool checked = false,
      bool grayed = false,
      bool enable = true,
      bool visible = true
   );
   ...
};


check_button cb1("Regeln aktiv", true, false, false, true);
check_button cb2("Direkt auswerten", false, false, true, false);

Oh! Während sich die Konstruktor-Deklaration aufgrund der guten Parameter-Namen noch akzeptabel lesen läßt, sind die Konstruktor-Aufrufe absolut unleserlich. Was für Check-Buttons werden denn hier mit welchen Einstellungen erzeugt?

Bei einem Bool-Parameter mag sich ein Funktions-Aufruf ja noch ganz gut lesen lassen, bei mehreren Bool-Parametern oder Konstruktoren oder Operatoren sind die True's und False's nur noch verwirrend. Und auch bei einem einzelnem Bool-Parameter ist die Bedeutung nicht immer klar, z.B.:

container.insert(object, true);

Was soll das True-Argument hier ausdrücken? Entweder man kennt die Parameter-Bedeutung, oder man hat verloren. Ohne Blick in die Dokumentation oder zumindest auf die hoffentlich vorhandenen guten Parameter-Namen in der Funktions-Deklaration hat man keine Chance zu verstehen was hier passiert.

Nachdem wir nun gesehen haben, dass Bool-Parameter wirklich nicht die beste Wahl sind, stellt sich die Frage: Was nehmen wir statt dessen?

1.1 Enum- statt Bool-Parameter

Sinnvoll wäre ein Argument, dass selbsterklärend ist, daher seine Bedeutung im Namen trägt - also z.B. Konstanten. Und da es immer nur ein begrenzte Anzahl an möglichen Werten gibt (bei Bool-Parametern genau 2), bieten sich Enums als Argumente an. Wir werden später sehen, dass wir damit noch weitere Vorteile gegenüber Bool-Parametern gewinnen - aber bleiben wir erstmal bei dem Thema "Lesbarkeit". Schreiben wir beide Beispiele mal auf Enums um.

enum enable_state { enabled, disabled };
enum visible_state { visible, invisible };
enum check_state { checked, unchecked };
enum gray_state { grayed, ungrayed };

class check_button
{
public:
   explicit check_button(
      const std::string& title,
      check_state = unchecked,
      gray_state = ungrayed,
      enable_state = enabled,
      visible_state = visible
   );

   ...
   void set_visible(visible_state);
   ...
};


check_button cb1("Regeln aktiv", checked, ungrayed, disabled, visible);
check_button cb2("Direkt auswerten", unchecked, ungrayed, enabled, invisible);
...
cb2.set_visible(visible);

Nun sind auch die Aufrufe direkt verständlich und damit problemlos lesbar. Und die Deklarationen bleiben auch dann lesbar, wenn - wie im Beispiel oben - keine Parameter-Namen angegeben sind.

2. Vorteile

Aber Enum- statt Bool-Parameter bieten viele Vorteile: Zusätzlich geben uns Enum- statt Bool-Parameter aber noch alternative nicht so offensichtliche Möglichkeiten, unseren Quelltext anders zu gestalten. Ich formuliere hier extra sehr vorsichtig "Möglichkeiten den Quelltext anders zu gestalten", da diese Änderungen nicht so eindeutig positiver Natur sind und auch kontrovers diskutiert werden können. Dies werden wir in einem weiteren Artikel[1]. detaillierter anschauen und werden dann auch die Vorteile und Möglichkeiten in Ruhe besprechen.

Hier wollen wir jetzt erstmal mit den einfachen Vorteilen von Enum- statt Bool-Schnittstellen fortfahren.

2.1 Typsicherheit

Enum- statt Bool-Parameter bieten noch mehr Vorteile. Einer davon ist die Typsicherheit. Da jeder Parameter eindeutig typisiert ist, kann es keine fehlerhaften Zuordnungen geben.

Da wollte jemand die Checkbox "enabled" und "invisible" erstellen, hat sich dann aber in der Reihenfolge der Parameter 4 und 5 vertan. Dieser Fehler fällt bei Bool-Parametern erst zur Laufzeit auf - falls gut getestet wird.

check_button cb("Regeln aktiv", true, false, false, true);
                                             ^      ^                 Parameter falsch rum

Im Falle von Enums meckert schon der Compiler.

check_button cb("Regeln aktiv", checked, ungrayed, invisible, enabled);
                                                   ^          ^            Compiler-Fehler

2.2 Änder- und Erweiterbarkeit

Ein zusätzlicher Nachteil von Bool-Parametern ist, dass er auf die zwei Werte "true" und "false" eingeschränkt ist. Nun werden Sie vielleicht sagen, dass das kein Nachteil ist, sondern ein absichtliches und gewünschtes Feature eines boolschen Typs. Ja, das stimmt schon - aber dieses Feature kann in zukünftigen Situation zum Handikap werden.

Stellen Sie sich vor, dass sich die GUI Klassen-Bibliothek weiterentwickelt, und Check-Buttons nun neben "sichtbar" und "unsichtbar" auch noch "transparent" sein können. Mit den Bool-Parametern stehen wir jetzt auf dem Schlauch, da ein "bool" eben keinen dritten Wert unterstützen kann. Spätestens jetzt müssen wir den Typ wechseln - sinnvollerweise natürlich zu einem Enum.

class check_button
{
public:
   explicit check_button(
      const std::string& title,
      bool checked = false,
      bool grayed = false,
      bool enable = true,
      visible_state = visible          // Schnittstellen-Aenderung
   );
   ...
};

Nur brechen wir damit die bisherige Schnittstelle. Zum Glück findet der Compiler in einem begrenzten Projekt alle nun fehlerhaften Aufruf-Stellen. Aber wenn dies in einer Bibliothek passiert, dann haben Sie wieder verloren. Denn nun haben Sie eine inkompatible Änderung, und tausende von Projekten draussen im Feld, die auf Ihrer Bibliothek basieren, müssen angepaßt werden. Typischerweise löst man dieses Problem, indem man eine zusätzliche Funktion anbietet, und die alte unangetastet läßt - sie in der Dokumentation aber nun als "deprecated" ausweist. Aber eine schöne Lösung ist dies nicht, da spätestens ab jetzt Ihre Bibliothek Altlasten mit sich rumschleppt, die Sie realistisch nie entsorgen können.

Hätten wir statt des Bool-Parameter einen Enum genommen, wären wir jetzt fein raus. Es würde sich ja keine Schnittstelle ändern. Unser Enum hätte nur einen neuen weiteren Enum-Wert - und das wär's.

enum visible_state { visible, invisible, transparent };             // Neuer Wert

class check_button
{
   ...
   void set_visible(visible_state);                                 // Keine Aenderung
   ...
};


cb.set_visible(visible);                                            // Funktioniert weiterhin

Intern - in unseren Funktions-Implementierungen - müßten wir den neuen Wert natürlich auswerten, aber das ist nur intern und bedeutet keine Änderung für den Benutzer. So eine Änderung ist abwärtskompatibel.

2.3 Parameter fallen weg

Eine andere Situation ist, wenn sich Parameter nicht ändern, sondern Alte wegfallen oder Neue hinzukommen. Auch hier sollte der Compiler daraus resultierende Inkompatibilitäten finden und anmeckern. Bei Bool-Parametern ist dies nicht zwingend der Fall - jedenfalls nicht wenn die Bool-Parameter hinten in der Parameterliste stehen und mit Default-Argumenten abgedeckt sind.

Nehmen wir das Beispiel von oben:

class check_button
{
public:
   explicit check_button(
      const std::string& title,
      bool checked = false,
      bool grayed = false,
      bool enable = true,
      bool visible = true
   );
   ...
};


check_button cb("Regeln aktiv", true, false);       // Erzeugt Check-Button mit:
                                                    // - checked
                                                    // - not grayed
                                                    // - enabled
                                                    // - visible

Manche GUIs erlauben keine frei wählbaren grauen Zustände für Check-Buttons - und hier kommt doch wirklich jemand auf die Idee, die GUI Klassen-Bibliothek einfach so an diese Plattform anzupassen und dafür den "grayed" Parameter zu entfernen. Das Ergebnis läßt sich immer noch compilieren - hat aber ein ganz anderes Programm zur Folge.

class check_button
{
public:
   explicit check_button(
      const std::string& title,
      bool checked = false,
                                                    // Kein "grayed" Parameter mehr
      bool enable = true,
      bool visible = true
   );
   ...
};


check_button cb("Regeln aktiv", true, false);       // Erzeugt Check-Button mit:
                                                    // - checked
                                                    // - disabled (vorher enabled)
                                                    // - visible

Nach der Änderung bekommt der Programmierer keine Compiler-Meldung, aber der Benutzer des Programms statt eines aktiven Check-Buttons einen inaktiven. Ob das so gewollt war?

Mit Enum-Parametern wäre dies nicht passiert. Nun hätte der Programmierer eine Fehlermeldung bekommen, da die Argumente nicht mehr zur Parameter-Liste passen.


class check_button
{
public:
   explicit check_button(
      const std::string& title,
      check_state = unchecked,
                                                    // Kein "grayed" Parameter mehr
      enable_state = enabled,
      visible_state = visible
   );
};


check_button cb("Regeln aktiv", checked, ungrayed);
                                         ^ Compiler-Fehler - "enable_state" Argument erwartet

2.4 Neue Parameter kommen hinzu

Ähnlich ist das Ergebnis, wenn mitten in die Parameter-Liste ein weiterer Bool-Parameter eingefügt wird. Ich denke, da das Szenario sehr ähnlich dem vorherigen ist, reicht hier eine reduzierte Diskussion.

Wir wollen in unseren Check-Button Konstruktor einen neuen zweiten Parameter aufnehmen, der angibt, ob das GUI-Element direkt nach der Konstruktion den Focus erhält. Mit Bool-Parametern führt dies natürlich wieder zu Problemen.

class check_button
{
public:
   explicit check_button(
      const std::string& title,
      bool focus = true,                            // Neuer Parameter
      bool checked = false,
      bool grayed = false,
      bool enable = true,
      bool visible = true
   );
   ...
};


check_button cb("Regeln aktiv", true, false);       // Erzeugt nun Check-Button mit:
                                                    // - focus
                                                    // - unchecked (vorher checked)
                                                    // - not grayed
                                                    // - enabled
                                                    // - visible

Und wieder wäre das mit Enum-Parametern nicht passiert.

check_button cb("Regeln aktiv", checked, ungrayed);
                                ^ Compiler-Fehler - "focus_state" Argument erwartet

Okay - ich gebe zu: dieser letzte Vorteil ist etwas konstruiert und unrealistisch. Kein Programmierer wird eine Schnittstelle erweitern und den neuen Parameter mitten in der Parameter-Liste unterbringen, sondern immer am Ende, da das unkritischer ist.

Aber sind Sie absolut sicher, dass das wirklich nie passiert? Ich nicht! Parameter-Listen sind doch meist semantisch sortiert - z.B. zusammengehörige bzw. ähnliche Parameter stehen zusammen, oder je "unwichtiger" ein Parameter ist, umso weiter hinten steht er. Wenn ein hinten angefügter Parameter solche "ästhetischen" Gesichtspunkte stark verletzt - keine Ahnung, was dann passiert. Mit Enum-Parametern ist man jedenfalls auf der sicheren Seite.

3. Schluss-Bemerkungen

4. Links

  1. Artikel "Status-Abfragen in C++" von Detlef Wilkening

5. Danksagung

Bedanken möchte ich mich bei folgenden Personen (in alphabetischer Reihenfolge), die diesen Artikel gegengelesen und mit vielen kleinen und großen Hinweisen dafür gesorgt haben, dass er besser ist als am Anfang:

6. Versions-Historie

Die Versions-Historie dieses Artikels:
Schlagwörter: