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

C 語言物件
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

在物件導向程式中,類別繼承 (inheritance) 的意圖有二:(1) 重用程式碼 (2) 子類型 (subtyping);前者用於減少重覆撰碼,後者則是實踐多型 (polymorphism) 的手法。

在 C++ 中採多重繼承,某個類別可以繼承任意個類別。人們發現這樣做的弊大於利,後來的語言多採用單一繼承,再搭配介面 interface 或 mixin 等進行受限制的多重繼承。

物件組合 (object composition) 則是另一種重用程式碼的方式。在物件組合中,類別僅使用另一個類別,但兩者間沒有父子型態的關係。

物件組合可再細分為兩者:(1) 組合 (composition) (2) 聚合 (aggregation)。組合的例子像是汽車中有引擎,我們將引擎物件視為汽車物件的一部分。聚合的例子像是池塘中有鴨子,但我們不認為鴨子物件是池塘物件的一部分。

在 C 語言中模擬繼承的思維

C 語言無法從語法上直接獲得繼承這項特性,所以我們要將繼承從表面上的意義抽離,思考繼承在程式碼的本質。

基本上,繼承是一種共用程式碼的方式,藉由將程式碼分別在父類別及子類別中各自實作,用來減少重覆實作的部分。雖然 C 沒有繼承的語法,但我們可以在「子類別」中呼叫「父類別」的函式,就不需要重新實作「父類別」中已有的函式。也就是說,我們用物件組合來達到程式碼重用以及模擬繼承的目的。

然而,在這樣模擬手法下,「父類別」和「子類別」是兩個獨立的類別。我們的確無法從「繼承」的過程實踐子類別。

實作範例:Person 類別和 Employee 類別

在本文中,我們用物件組合來實作 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("Michelle", 37, "Google", 1000);
    if (!ee) {
        perror("Failed too allocate ee\n");
        goto ERROR;
    }

    if (!(strcmp(employee_name(ee), "Michelle") == 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 無子類別的關係

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

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。