C 語言程式設計教學:封裝 (Encapsulation)

PUBLISHED ON SEP 30, 2018 — PROGRAMMING

物件導向程式中,若物件有進行封裝 (encapsulation),除了透過公閍介面外,我們無法更動該物件內部的狀態;在程式設計中,就是要透過該物件相關的函式呼叫來存取物件的屬性。封裝主要是強化物件的強健性 (robustness),避免預料之外的狀況發生。封裝並不是物件導向必備的特性,Python 的物件基本上無法達到真正的封裝,但人們仍然廣泛地使用 Python 撰寫的程式進行各種任務。

我們同樣使用二維空間的點 (point) 來展示物件,這次加上一些封裝的手法。先看一下封裝過的 Point 物件如何使用:

#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include "point.h"

int main(void)
{
    bool failed = false;
    
    // Create a Point object.
    Point* pt = point_new(0, 0);
    if (!pt) {
        perror("Failed to allocate Point pt");
        failed = true;
        goto POINT_FREE;
    }

    // Check x and y.
    if (!(point_x(pt) == 0)) {
        failed = true;
        goto POINT_FREE;
    }
    
    if (!(point_y(pt) == 0)) {
        failed = true;
        goto POINT_FREE;
    }
    
    // Mutate x and y.
    if (!point_set_x(pt, 3)) {
        failed = true;
        goto POINT_FREE;
    }
    
    if (!point_set_y(pt, 4)) {
        failed = true;
        goto POINT_FREE;
    }
    
    // Check x and y again.
    if (!(point_x(pt) == 3)) {
        failed = true;
        goto POINT_FREE;
    }
    
    if (!(point_y(pt) == 4)) {
        failed = true;
        goto POINT_FREE;
    }
    
POINT_FREE:
    // Free the object.
    point_free(pt);
    
    if (failed) {
        exit(EXIT_FAILURE);
    }

    return 0;
}

細心的讀者可發現,這個例子幾乎和前文的例子一模一樣。封裝並不影響物件的公開方法,而是保護未公開的屬性和方法,避免外部程式不當的存取。

接著來看 Point 類別的公開方法:

#ifndef POINT_H
#define POINT_H

// Declare Point class with hidden fields.
typedef struct point Point;

// The constructor of Point.
Point* point_new(double x, double y);

// The getters of Point.
double point_x(Point *self);
double point_y(Point *self);

// The setters of Point.
void point_set_x(Point *self, double x);
void point_set_y(Point *self, double y);

// The destructor of Point.
void point_free(void *self);

#endif // POINT_H

眼尖的讀者應該已經發現這個版本的 Point 類別沒有宣告其屬性,我們會將屬性藏在 C 原始碼中。但關鍵的 C 語法特性是 forward declaration,如下例:

typedef struct point Point;

我們可以在尚未宣告 struct point 時就先用 typedef 重定義其別名,藉此達到封裝的目的。其實也可以不用 typedef,僅宣告 struct point,如下例:

struct point;

筆者本身習慣用 typedef 重定義別名,之後的語法會較簡潔。

接著我們來看 Point 類別內部的實作:

#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
#include "point.h"

// x and y are hidden from external programs.
struct point {
    double x;
    double y;
};

// Declaration for private methods.
static bool _is_x_valid(double);
static bool _is_y_valid(double);

// The constructor of Point.
Point* point_new(double x, double y)
{
    if (!(_is_x_valid(x) && _is_y_valid(y))) {
        return NULL;
    }

    Point* pt = (Point *) malloc(sizeof(Point));
    if (!pt) {
        return pt;
    }
    
    pt->x = x;
    pt->y = y;
    
    return pt;
}

// The getter of x.
double point_x(Point *self)
{
    assert(self);

    return self->x;
}

// The setter of x.
bool point_set_x(Point *self, double x)
{
    if (!_is_x_valid(x)) {
        return false;
    }
    
    assert(self);

    self->x = x;
    
    return true;
}

// The getter of y.
double point_y(Point *self)
{
    assert(self);

    return self->y;
}

// The setter of y.
bool point_set_y(Point *self, double y)
{
    if (!_is_y_valid(y)) {
        return false;
    }

    assert(self);

    self->y = y;
    
    return true;
}

// Private validator for x.
static bool _is_x_valid(double x)
{
    return x >= 0.0;
}

// Private validator for y.
static bool _is_y_valid(double y)
{
    return y >= 0.0;
}

// The destructor of Point.
void point_free(void *self)
{
    if (!self) {
        return;
    }
        
    free(self);
}

我們在 C 程式碼中補上 struct point 的宣告,對外部程式來說,這部分就是隱藏的,藉此達到封裝的效果。

我們另外加上兩個私有方法 (private methods),這兩項方法是要確認屬性是否合法 (valid):

// Private validator for x.
static bool _is_x_valid(double x)
{
    return x >= 0.0;
}

// Private validator for y.
static bool _is_y_valid(double y)
{
    return y >= 0.0;
}

利用 static 宣告函式,該函式的可視域 (scope) 就限縮在同一個檔案中,藉此達到封裝的特性。

在 C 語言的物件導向程式中,封裝是最容易達成的特性,即使我們之後完全不用其他的物件導向特性,也應該用封裝保護物件應有的強健性。

comments powered by Disqus