C 語言程式設計教學:組合 (composition) 和繼承 (inheritance)

PUBLISHED ON OCT 7, 2018 — PROGRAMMING
FacebookTwitter LinkedIn LINE Skype EverNote GMail Email Email

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

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

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

    #include <assert.h>
    #include <string.h>
    #include <stdlib.h>
    #include "employee.h"
    
    int main()
    {
        Employee *ee = employee_new("Michael", 37, "Google", 1000);
    
        assert(strcmp(employee_name(ee), "Michael") == 0);
        assert(employee_age(ee) == 37);
        assert(strcmp(employee_company(ee), "Google") == 0);
        assert(employee_salary(ee) == 1000);
    
        employee_set_name(ee, "Tommy");
        employee_set_age(ee, 28);
        employee_set_company(ee, "Microsoft");
        employee_set_salary(ee, 1200);
    
        assert(strcmp(employee_name(ee), "Tommy") == 0);
        assert(employee_age(ee) == 28);
        assert(strcmp(employee_company(ee), "Microsoft") == 0);
        assert(employee_salary(ee) == 1200);
    
        employee_free(ee);
    
        return EXIT_SUCCESS;
    }

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

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

    #ifndef EMPLOYEE_H
    #define EMPLOYEE_H
    
    typedef struct employee Employee;
    
    Employee* employee_new(
        char *name, unsigned int age, char *company, double salary);
    char* employee_name(Employee *self);
    void employee_set_name(Employee *self, char *name);
    unsigned int employee_age(Employee *self);
    void employee_set_age(Employee *self, unsigned int age);
    char* employee_company(Employee *self);
    void employee_set_company(Employee *self, char *company);
    double employee_salary(Employee *self);
    void employee_set_salary(Employee *self, double salary);
    void employee_free(void *self);
    
    #endif // EMPLOYEE_H

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

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

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

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

    struct employee {
        Person* super;
        char *company;
        double salary;
    };

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

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

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

    void employee_free(void *self)
    {
        if (!self) {
            return;
        }
        
        person_free(((Employee *) self)->super);
        free(self);
    }

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

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

    #ifndef PERSON_H
    #define PERSON_H
    
    typedef struct person Person;
    
    Person* person_new(char *name, unsigned int age);
    char* person_name(Person *self);
    void person_set_name(Person *self, char *name);
    unsigned int person_age(Person *self);
    void person_set_age(Person *self, unsigned int age);
    void person_free(void *self);
    
    #endif // PERSON_H

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

    最後來看 Person 的實作:

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

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

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

    • EmployeePerson 都是可用的公開類別
    • Employee 有呼叫 Person 的方法
    • EmployeePerson 無子類別的關係

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