[C 語言] 程式設計教學:如何實作組合 (composition) 和繼承 (inheritance)

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    在物件導向程式中,類別繼承 (inheritance) 的意圖有二:(1) 重用程式碼 (2) 子類型 (subtyping);前者用於減少重覆撰碼,後者則是實踐多型 (polymorphism) 的手法。在 C++ 中採多重繼承,某個類別可以繼承任意個類別。人們發現這樣做的弊大於利,後來的語言多採用單一繼承,再搭配介面 (interface) 或 mixin 等進行受限制的多重繼承。C 語言無法從語法上直接獲得繼承這項特性。

    物件組合 (object composition) 則是另一種重用程式碼的方式。在物件組合中,類別僅使用另一個類別,但兩者間沒有子類型的關係。物件組合可再細分為兩者:(1) 組合 (composition) (2) 聚合 (aggregation)。組合的例子像是汽車中有引擎,我們將引擎物件視為汽車物件的一部分。聚合的例子像是池塘中有鴨子,但我們不認為鴨子物件是池塘物件的一部分。在 C 語言中,我們用物件組合來達到程式碼重用以及模擬繼承。

    在本文中,我們用物件組合來實作 employee_t 物件。我們先來看一下 employee_t 物件如何使用:

    #include <string.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include "employee.h"
    
    #define ERROR(msg) \
        fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, msg);
    
    int main()
    {
        employee_t *ee = employee_new("Michael", 37, "Google", 1000);
        if (!ee) {
            perror("Failed too allocate ee\n");
            goto ERROR;
        }
    
        if (!(strcmp(employee_name(ee), "Michael") == 0)) {
            ERROR("Wrong name");
            goto ERROR;
        }
    
        if (!(employee_age(ee) == 37)) {
            ERROR("Wrong age");
            goto ERROR;
        }
    
        if (!(strcmp(employee_company(ee), "Google") == 0)) {
            ERROR("Wrong company");
            goto ERROR;
        }
    
        if (!(employee_salary(ee) == 1000)) {
            ERROR("Wrong salary");
            goto ERROR;
        }
    
        /* Mutate `ee`. */
        employee_set_name(ee, "Tommy");
        employee_set_age(ee, 28);
        employee_set_company(ee, "Microsoft");
        employee_set_salary(ee, 1200);
    
        if (!(strcmp(employee_name(ee), "Tommy") == 0)) {
            ERROR("Wrong name");
            goto ERROR;
        }
    
        if (!(employee_age(ee) == 28)) {
            ERROR("Wrong age");
            goto ERROR;
        }
    
        if (!(strcmp(employee_company(ee), "Microsoft") == 0)) {
            ERROR("Wrong company");
            goto ERROR;
        }
    
        if (!(employee_salary(ee) == 1200)) {
            ERROR("Wrong salary");
            goto ERROR;
        }
    
        employee_delete(ee);
    
        return 0;
    
    ERROR:
        if (ee)
            employee_delete(ee);
        
        return 1;
    }

    說實在的,從這裡看不出來有物件組合的跡象。

    接著,來看 employee_t 類別的公開介面:

    #pragma once
    
    typedef struct employee_t employee_t;
    
    employee_t* employee_new(
        char *name, unsigned int age, char *company, double salary);
    char* employee_name(employee_t *self);
    void employee_set_name(employee_t *self, char *name);
    unsigned int employee_age(employee_t *self);
    void employee_set_age(employee_t *self, unsigned int age);
    char* employee_company(employee_t *self);
    void employee_set_company(employee_t *self, char *company);
    double employee_salary(employee_t *self);
    void employee_set_salary(employee_t *self, double salary);
    void employee_delete(void *self);

    同樣地,我們也無法透過 employee_t 的公開介面看出物件組合的跡象。

    接著,我們來看 employee_t 類別的實作:

    #include <stdlib.h>
    #include <assert.h>
    #include "person.h"
    #include "employee.h"
    
    struct employee_t {
        person_t* super;
        char *company;
        double salary;
    };
    
    // Private helper function declaration.
    static void check_salary(double salary);
    
    employee_t* employee_new(
        char *name, unsigned int age, char *company, double salary)
    {
        check_salary(salary >= 0.0);
    
        // Create parent object.
        person_t *super = person_new(name, age);
        if (!super)
            return NULL;
    
        // Create child object.
        employee_t *ee = malloc(sizeof(employee_t));
        if (!ee) {
            person_delete(super);
            return ee;
        }
    
        ee->super = super;
        ee->company = company;
        ee->salary = salary;
    
        return ee;
    }
    
    char* employee_name(employee_t *self)
    {
        return person_name(self->super);
    }
    
    void employee_set_name(employee_t *self, char *name)
    {
        person_set_name(self->super, name);
    }
    
    unsigned int employee_age(employee_t *self)
    {
        return person_age(self->super);
    }
    
    void employee_set_age(employee_t *self, unsigned int age)
    {
        person_set_age(self->super, age);
    }
    
    char* employee_company(employee_t *self)
    {
        return self->company;
    }
    
    void employee_set_company(employee_t *self, char *company)
    {
        self->company = company;
    }
    
    double employee_salary(employee_t *self)
    {
        return self->salary;
    }
    
    void employee_set_salary(employee_t *self, double salary)
    {
        check_salary(salary);
    
        self->salary = salary;
    }
    
    void employee_delete(void *self)
    {
        if (!self) {
            return;
        }
        
        person_delete(((employee_t *)self)->super);
        free(self);
    }
    
    static void check_salary(double salary)
    {
        assert(salary >= 0.0);
    }

    在這裡,我們發現 employee_t 類別中另外使用了 person_t 類別:

    struct employee_t {
        person_t* super;
        char *company;
        double salary;
    };

    在一些方法中,employee_t 類別並沒有實作相關的內容,而是由 person_t 類別來處理,如下例:

    void employee_set_name(employee_t *self, char *name)
    {
        person_set_name(self->super, name);
    }

    最後要釋放記憶體時要由內而外釋放:

    void employee_delete(void *self)
    {
        if (!self) {
            return;
        }
        
        person_delete(((employee_t *)self)->super);
        free(self);
    }

    在我們這個例子中,employee_t 類別並沒有存取 person_t 的私有屬性,僅用 person_t 類別的公開界面操作 person_t 物件。

    接著我們來看 person_t 類別的公開界面:

    #pragma once
    
    typedef struct person_t person_t;
    
    person_t* person_new(char *name, unsigned int age);
    char* person_name(person_t *self);
    void person_set_name(person_t *self, char *name);
    unsigned int person_age(person_t *self);
    void person_set_age(person_t *self, unsigned int age);
    void person_delete(void *self);

    其實就是基本的 getters 和 setters,沒有什麼困難的地方。

    最後來看 person_t 的實作:

    #include <stdlib.h>
    #include "person.h"
    
    struct person_t {
        char *name;
        unsigned int age;
    };
    
    person_t* person_new(char *name, unsigned int age)
    {
        person_t* p = malloc(sizeof(person_t));
        if (!p)
            return p;
    
        p->name = name;
        p->age = age;
    
        return p;
    }
    
    char* person_name(person_t *self)
    {
        return self->name;
    }
    
    void person_set_name(person_t *self, char* name)
    {
        self->name = name;
    }
    
    unsigned int person_age(person_t *self)
    {
        return self->age;
    }
    
    void person_set_age(person_t *self, unsigned int age)
    {
        self->age = age;
    }
    
    void person_delete(void *self)
    {
        if (!self) {
            return;
        }
        
        free(self);
    }

    基本上就是一些 getters 和 setters 的實作,對實作部分不另作說明。

    我們這個實作的結果如下:

    • employee_tperson_t 都是可用的公開類別
    • employee_t 有呼叫 person_t 的方法
    • employee_tperson_t 無子類別的關係

    由此可知,在這個實作中,有程式碼重用,但沒有子類別,所以無法實踐由子類別所帶來的多型。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk