CObject und seine Probleme

Viele Leute kennzeichnen ihre Klassen mit einem Prefix, meistens C. Doch was bringt es ihnen?

"Damit ich weiß ob es eine Klasse, struct, builtin, etc. ist" oder "Es liest sich leichter, wenn man weiß was man vor sich hat" sowie "Es ist praktisch, wenn der Name (einer Variablen, Klasse, struct,...) auch Informationen über den Typen bereithält".

Diese Argumente werden häufig von Befürworten von Klasse-Prefixen gebracht. Doch stimmen sie auch?

In der Tat, sie stimmen teilweise, aber doch nicht ganz. So ist es nicht immer praktisch wenn der Name mehr Informationen beinhaltet, als er muss.

    Int32 i32;
    Int64 i64;
    Int128 i128;

    i32=7;
    cout<<i32+3; //(1)

    i128=i64+i32; //(2)
    

In der Objekt Orientierten Programmierung interessieren wir uns nicht für die implementations Details. Der obige Code ist doch gut zu lesen - bei (1) wird 10 ausgegeben, egal ob Int32 ein typedef auf int oder auf eine Klasse ist. Ebenso können wir uns bei (2) sicher sein, dass i128 das Ergebnis der Addition von i32 und i64 zugewiesen wird. Es ist absolut egal wie diese Typen implementiert sind. Vielleicht ist Int64 noch eine Klasse zum verwalten von großen Zahlen, aber wird später auf geändert werden, indem Int64 später zu einem typedef auf long long wird.

Doch es gibt ein weiteres Problem: nicht nur unnütze Information, sondern zuviel Information:

    class Document
    {
    public:
        Document(...);
        virtual void print();
        virtual void edit() ;
        ...
    };

    // Irgendwo anders dann
    class DocumentFactory
    {
    public:
        Document* create(char const* type);
        ...
    };

    // im Anwendungscode:
    Document* doc1 = documentFactory::create("Winword-Document");
    ...
    Document* doc2 = documentFactory::create("XML-Document");
    ...


    //die prefix Version:
    class CDocument
    {
    public:
        CDocument(...);
        virtual void print();
        virtual void edit() ;
        ...
    };

    // Irgendwo anders dann
    class CDocumentFactory
    {
    public:
        CDocument* create(char const* type);
        ...
    };

    // im Anwendungscode:
    CDocument* doc1 = documentFactory::create("Winword-Document");
    ...
    CDocument* doc2 = documentFactory::create("XML-Document");
    ...

CDocument - wenn man das sieht, denken viele Leute automatisch an MFC. Das ist logisch, denn die MFC wurde entwickelt, bevor es Namensräume gab und C wurde sozusagen als Namespace ersatz genommen. (Borland nahm T als Namespace). Doch heute gibt es Namensräume und wir brauchen diese "Namensraum-Ersätze" nicht. Es führt nur zu Verwirrung wenn CDocument nicht CDocument der MFC ist. Nehmen wir also an, statt CDocument schreiben wir C_Document - so verhindern wir Verwechslungen.

Nehmen wir nun an, wir wollen unseren Code verbessern. Die Firma stellt um - C_Document soll ein reines Interface sein. Diese Änderung ist in Null-Komma-Nix erledigt:

    class Document
    {
    public:
        virtual void print() = 0;
        virtual void edit() = 0;
        ...
    };

    // Irgendwo anders dann
    class DocumentFactory
    {
    public:
        Document* create(char const* type);
        ...
    };

    // im Anwendungscode:
    Document* doc1 = documentFactory::create("Winword-Document");
    ...
    Document* doc2 = documentFactory::create("XML-Document");
    ...


    //die prefix Version:
    class I_Document
    {
    public:
        virtual void print() = 0;
        virtual void edit() = 0;
        ...
    };

    // Irgendwo anders dann
    class C_DocumentFactory
    {
    public:
        I_Document* create(char const* type);
        ...
    };

    // im Anwendungscode:
    I_Document* doc1 = documentFactory::create("Winword-Document");
    ...
    I_Document* doc2 = documentFactory::create("XML-Document");
    ...

Doch moment mal: bei der Prefix-Version haben wir den Anwendungscode geändert. Das ist schlimm. Ein Document verhält sich immer noch gleich, doch der Name hat sich geändert (da ein Interface sich ja doch von einer "normalen" Klasse unterscheidet). Jetzt muss der Anwendungscode geändert werden, obwohl es technisch nicht nötig wäre - denn wir haben ja extra eine Factory um Dokumente zu erzeugen.

Bei generischen Code (also Templates), kann man diese Prefixe auch nicht verwenden - folglich kann man dieses Prefixe-Konzept auch nicht konsequent durchziehen.

Manche Leute gehen einen Schritt weiter und kennzeichnen auch Variablen mit Prefixen. Doch in C++ braucht man diese Prefixe nicht mehr. Stellen wir uns einen Algorithmus vor, der mittels eines std::vectors implementiert wurde. Beim profilen entdecken wir, dass std::vector nicht schnell genug ist, weil wir zB öfters aus der Mitte löschen, als wir anfangs dachten. Wir ändern den Container von std::vector auf std::list. Alles ist perfekt, wir ändern den Typen an einer Stelle - da das Interface ja nahezu Identisch ist, müssen wir uns nicht weiter darum kümmern. Doch wenn wir Prefixe verwenden, also vecContainer geschrieben haben, müssen wir jetzt den ganzen Code durchgehen und überall vecContainer durch lstContainer ersetzen. Natürlich können wir das automatisieren. Also ist es doch nicht so schlimm?

Doch, es ist trotzdem schlimm: wenn wir Iteratoren betrachten, dann müssen wir konsequenterweise auch überall statt iterator vecIterator verwendet haben (iteratoren werden normalerwise hinter einem typedef versteckt). uU hat unsere Klasse auch iterator nach aussenhin bekannt gegeben - unter dem Namen vecIterator. Dass wir damit implementations Details verraten haben, lassen wir einmal links liegen. Wir müssten also auch den Anwendercode ändern. Da wir aber keine Implementationsdetails verraten wollen, haben wir den Iterator wahrscheinlich "nur" iterator genannt. Doch dann haben wir die Prefixe wieder nicht konsequent durchgezogen.

Betrachten wir Prefixe bei generischer Programmierung: ein SmartPointer<int,refcountPolicy,OwnerPolicy> p; - ist nicht selten. Ein Policy-basierender SmartPointer. Doch wie nennen wir jetzt die Variable? SpIntRefcountOwner? Naja, es geht nicht vernünftig. Und jedesmal wenn wir ein Policy ändern müssen wir den Namen ändern. Dabei interessiert es uns eigentlich nicht sonderlich ob der SmartPointer ein Reference-Counting macht, oder ob er als Ring implementiert. Er tut was er tun soll, der rest ist implementations Sache.

Weiters soll der Anwendercode auch nicht viel Wert darauf legen, ob er mit einem SmartPointer oder einem echten Zeiger hantiert - sie verhalten sich ja identisch.

Es gibt soviele Klasse, da kann man nie für jede Klasse ein Prefix erfinden (OK, erfinden schon, aber nicht merken). Da sind Prefixe einfach sinnlos. Denn in der OOP interessiert uns nicht der Typ (Klasse), sondern die Variable (Objekt). Es heißt ja auch Objekt Orientierte Programmierung und nicht Klassen Orientierte Programmierung.

In C++ verwenden wir kleine Funktionen, nicht mehr als 10 Zeilen. Variablen werden dort definiert, wo man sie braucht und nicht am Block-Anfang. So dass wir immer die Definition der Variable auf dem Bildschirm sehen.

top