控制物件的存取權限以實現封裝

    前言

    在先前的範例程式中,我們不強調屬性和訊息的存取權限。也就是說,在預設情形下,類別使用者看得到該類別的屬性,所有的訊息都是公開的。但我們有時候想要隱藏一部分屬性和訊息,僅保持最小量的公開界面。本文展示在 Objective-C 類別中實作私有屬性和私有訊息的方式。

    陣列類別 Array 的公開界面宣告

    本範例程式是一個動態陣列 (dynamic array)。但我們不會實作該資料結構所有的抽象資料結構 (ADT),只會實作少數訊息,用來說明物件的存取權限。

    我們先來看此陣列的公開界面:

    /* array.h */
    #pragma once
    
    #import <Foundation/Foundation.h>
    #include <stdbool.h>
    #include <stddef.h>
    
    typedef struct array_data_t array_data_t;
    
    @interface Array : NSObject {
        array_data_t *data;
    }
    
    -(Array *) init;
    -(void) dealloc;
    -(size_t) size;
    -(id) at: (size_t) index;
    -(bool) push: (id) obj;
    @end
    

    類別 Array 的屬性 data 表面上是公開的,但 data 本身是 opaque pointer,對類別 Array 的使用者來說,其屬性 data 實質上是隱藏的。另外,我們也實作了一個私有訊息,但從公開界面中無法看到該訊息。

    陣列類別 Array 的內部實作

    接著,我們來看該類別內部的實作。

    我們先看私有屬性的部分:

    struct array_data_t {
        size_t size;
        size_t capacity;
        size_t head;
        size_t tail;
        id *elems;
    };
    

    我們用結構體 array_data_t 把真正的屬性包在裡面。由於該結構體放在實作檔中,對外部程式來說,無法直接存取此結構體內的屬性,故類別 Array 的屬性視為私有。

    另外,我們來看私有訊息的部分:

    @interface Array ()
    -(bool) expand;
    @end
    

    在這裡,我們用到了 Objective-C 的語法特性 category。原本 category 的用意是用來延伸現存的類別,我們把 Array 的 category 界面藏在實作檔中,對於類別使用者來說同樣視為不可見。

    由於 Objective-C 是動態語言,其實類別使用者仍然可以呼叫這個訊息。但該訊息對類別使用者來說是不可見的,類別使用者若硬要呼叫該訊息時,編譯器會發出警告訊息。

    我們來看如何初始化此物件:

    -(Array *) init
    {
        if (!self)
            return self;
    
        self = [super init];
    
        data = \
            (array_data_t *) malloc(sizeof(array_data_t));
        if (!data) {
            [self release];
            self = nil;
            return self;
        }
            
        data->size = 0;
        data->capacity = 2;
        data->head = 0;
        data->tail = 0;
            
        data->elems = \
            (id *) malloc(data->capacity * sizeof(id));
        if (!(data->elems)) {
            free(data);
            [self release];
            self = nil;
            return self;
        }
    
        return self;
    }
    

    我們在內部偷偷地使用標準 C 的 malloc() 函式來配置記憶體,因為 data 本質上是指向結構體的指標,所以無法用 Objective-C 提供的 alloc 訊息來配置記憶體。在 Objective-C 程式碼中混用純 C 程式碼是合法的,只要寫得正確即可。

    由於我們使用到 malloc() 函式,我們在 dealloc 訊息中也要自己寫上相對應的 free() 函式,不能完全依賴 Objective-C 現成的 dealloc 訊息。我們的 dealloc 訊息的實作如下:

    -(void) dealloc
    {
        if (!self)
            return;
    
        if (!data) {
            [super dealloc];
            return;
        }
    
        for (size_t i = 0; i < data->size; ++i) {
            size_t index = (i + data->head) % data->size;
            if (nil != data->elems[index])
                [data->elems[index] release];
        }
    
        free(data->elems);
        free(data);
    
        [super dealloc];
    }
    

    在這個 dealloc 訊息中,使用 malloc() 所配置的記憶體,得自行用 free() 釋放掉。其他的部分則使用 Objective-C 現有的 dealloc 訊息來釋放記憶體即可。如同純 C 程式,要由內部向外部逐一釋放,以免因 dangling pointer 無法正確釋放記憶體。

    接著,我們來看如何把物件推入此陣列:

    -(bool) push: (id)obj
    {
        if (![self expand])
            return false;
        
        if (data->size > 0)
            data->tail = (data->tail + 1) % data->capacity;
    
        data->elems[data->tail] = obj;
        data->size += 1;
        
        return true;
    }
    

    一開始,要先確認動態陣列的容量是充足的,所以我們用 [self expand] 預擴展好足夠的空間。expand 對於此範例程式來說算是私有訊息,因為 Array 的公開界面中沒有宣告此訊息。

    當陣列大小不為零時,我們得把 data->tail 以環狀陣列的手法平移 1。最後將 obj 放入陣列即可。

    剩下的部分基本上和存取權限無關,單純是資料結構方面的議題,我們就不逐一說明。讀者可試著自行閱讀我們提供的範例程式碼:

    /* array.m */
    #include <stdbool.h>
    #include <stdlib.h>
    #import "array.h"
    
    /* Private fields. */
    struct array_data_t {
        size_t size;
        size_t capacity;
        size_t head;
        size_t tail;
        id *elems;
    };
    
    /* Private messages. */
    @interface Array ()
    - (bool) expand;
    @end
    
    @implementation Array
    -(Array *) init
    {
        if (!self)
            return self;
    
        self = [super init];
    
        data = (array_data_t *) malloc(sizeof(array_data_t));
        if (!data) {
            [self release];
            self = nil;
            return self;
        }
            
        data->size = 0;
        data->capacity = 2;
        data->head = 0;
        data->tail = 0;
            
        data->elems = (id *) malloc(data->capacity * sizeof(id));
        if (!(data->elems)) {
            free(data);
            [self release];
            self = nil;
            return self;
        }
    
        for (size_t i = 0; i < data->capacity; ++i)
            data->elems[i] = NULL;
    
        return self;
    }
    
    -(void) dealloc
    {
        if (!self)
            return;
    
        if (!data) {
            [super dealloc];
            return;
        }
    
        for (size_t i = 0; i < data->size; ++i) {
            size_t index = (i + data->head) % data->size;
            if (nil != data->elems[index])
                [data->elems[index] release];
        }
    
        free(data->elems);
        free(data);
    
        [super dealloc];
    }
    
    -(size_t) size
    {
        return data->size;
    }
    
    -(id) at: (size_t)index
    {
        NSAssert(index < data->size, @"Invalid index\n");
    
        size_t i = (index + data->head) % data->size;
    
        return data->elems[i];
    }
    
    -(bool) push: (id) obj
    {
        if (![self expand])
            return false;
        
        if (data->size > 0)
            data->tail = (data->tail + 1) % data->capacity;
    
        data->elems[data->tail] = obj;
        data->size += 1;
        
        return true;
    }
    
    -(bool) expand
    {
        if (data->size < data->capacity)
            return true;
        
        data->capacity <<= 1;
        id *old_elems = data->elems;
        id *new_elems = (id *) malloc(data->capacity * sizeof(id));
        if (!new_elems)
            return false;
    
        size_t i = data->head;
        size_t j = 0;
        size_t sz = 0;
        while (sz < data->size) {
            new_elems[j] = old_elems[i];
    
            i = (i + 1) % data->size;
            j = (j + 1) % data->capacity;
            sz++;
        }
    
        data->head = 0;
        data->tail = data->size - 1;
        
        data->elems = new_elems;
        free(old_elems);
        
        return true;
    }
    @end
    

    使用該類別的外部程式

    最後,我們藉由一個簡短的外部程式來看 Array 類別如何使用:

    /* main.m */
    #import <Foundation/Foundation.h>
    #include <stdio.h>
    #import "array.h"
    
    int main(void)
    {
        NSAutoreleasePool *pool = \
            [[NSAutoreleasePool alloc] init];
        if (!pool)
            return 1;
    
        Array *arr = nil;
    
        arr = [[[Array alloc] init] autorelease];
        if (!arr) {
            perror("Failed to allocate arr\n");
            goto ERROR;
        }
        
        NSArray *data = [NSArray arrayWithObjects:
            [NSNumber numberWithInt: 3],
            [NSNumber numberWithInt: 4],
            [NSNumber numberWithInt: 5],
            [NSNumber numberWithInt: 6],
            [NSNumber numberWithInt: 7],
            nil
        ];
        for (id obj in data) {
            if (![arr push: obj]) {
                perror("Failed to push data\n");
                goto ERROR;
            }
        }
    
        for (size_t i = 0; i < [arr size]; i++) {
            int a = [[arr at:i] intValue];
            int b = [[data objectAtIndex: i] intValue];
            if (!(a == b)) {
                fprintf(stderr, "Unequal value: %d %d\n", a, b);
                goto ERROR;
            }
        }
    
        [pool drain];
    
        return 0;
    
    ERROR:
        [pool drain];
        
        return 1;
    }
    

    一開始,我們建立 Array 物件 arr。然後,我們用陣列實字建立 NSArray 物件 data。我們利用 for 迴圈把 data 內的資料逐一推入 arr 中。最後再用另一個 for 迴圈逐一確認兩邊的資料相等。

    我們的範例程式所做的事很簡單,只是加上錯誤處理的程式碼,所以看起來稍長一些。

    編譯此範例程式

    在 Mac 上用 Clang 編譯此程式時,不需指定 Cocoa 的路徑,所以指令較為簡單:

    $ clang -o array array.m main.m -lobjc -framework Foundation
    

    在類 Unix 系統上用 GCC 編譯此指令時,由於 GNUstep 通常不位於標準路徑,所以要加入 GNUstep 位置相關的參數:

    $ gcc -std=c11  -o array array.m main.m -lobjc -lgnustep-base -I /usr/include/GNUstep -L /usr/lib/GNUstep -fconstant-string-class=NSConstantString
    

    在類 Unix 系統上使用 Clang 時也是相同的情形。此外,還要引入 Objective-C 低階物件標頭檔相關的路徑:

    $ clang -std=c11 -o arrayDemo array.m main.m -lobjc -lgnustep-base -I /usr/include/GNUstep -I /usr/lib/gcc/x86_64-linux-gnu/7/include -L /usr/lib/GNUstep -fconstant-string-class=NSConstantString
    

    結語

    實作私有屬性和私有訊息背後的意圖就是實現封裝 (encapsulation)。對於類別來說,我們應該只開放最小足量的訊息,並隱藏所有的屬性,以免外部程式過度依賴類別的內部實作,造成日後修改程式碼的困難。

    雖然封裝不是物件必需的條件,良好的封裝的確可以增進程式碼的品質。如果讀者想封裝 Objective-C 物件,可以參考本範例程式的手法。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Yahoo
    【追蹤本站】
    Facebook Facebook Twitter