前言
在物件導向程式中,類別繼承 (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_t
和person_t
都是可用的公開類別employee_t
有呼叫person_t
的方法employee_t
和person_t
無子類別的關係
由此可知,在這個實作中,有程式碼重用,但沒有子類別,所以無法實踐由子類別所帶來的多型。