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

PUBLISHED ON SEP 30, 2018 — PROGRAMMING
FacebookTwitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    物件導向程式中,若物件有進行封裝 (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 語言的物件導向程式中,封裝是最容易達成的特性,即使我們之後完全不用其他的物件導向特性,也應該用封裝保護物件應有的強健性。