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

PUBLISHED ON OCT 7, 2018 — PROGRAMMING

在物件導向程式中,類別繼承 (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 無子類別的關係

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

comments powered by Disqus