6/09/2011

GObject 入門 (Part 5) - 存取物件屬性

一般來說,在C語言允許使用者直接存取資料結構(data structure)的成員(member),前提是使用者必須知道該資料結構的定義(definition),當資料被封裝起來時,使用者就必須可能就必須透過額外的介面存取。例如以下情況:
... ... ...

typedef struct _CirclePrivate CirclePrivate;

    ... ... ...

struct _Circle
{
    Shape parent;

    CirclePrivate* priv;
};

struct _CircleClass
{
    ShapeClass parent;
};

    ... ... ...

void  circle_set_radius(Circle* circle, guint radius);
guint circle_get_radius(Circle* circle);

    ... ... ...
在上段程式碼中,CirclePrivate僅提供型別的宣告(declaration)並不提供定義(definition),使用者無法直接存取priv變數的內容,面對此不公開(opaque)的資料結構,使用者被迫只能靠程式介面來存取資料內容(在上面的例子中就是circle_set_radius與circle_get_radius),如此一來原設計者就輕易達成資料封裝的目的。
GObject system實做了一個泛用的物件屬性讀寫機制(generic get/set mechanism for object properties),此機制可讓使用者在知道物件屬性的名稱的狀況下透過GObject的g_object_get_property讀取或以g_object_set_property寫入資料。這套機制很強大,但同時也容易令人卻步,因為如果你設計的類別要支援這個功能,將會需要額外撰寫不少如裹腳布般的程式碼,況且GObject system也沒有規定一定要實作此功能程式才能運作,像前幾篇短文(GObject 入門 Part2Part3Part4)中提出的範例並無相關實作亦能正常運行無誤,基於程式設計族群懶惰的天性,很多人就當沒這回事。是否一定要在專案中導入此功能個人覺得可以審慎評估,若是想要降低專案中各元件的耦合度(coupling degree)或是為將來方便撰寫其它程式語言連結(binding to other language)鋪路,此功能值得深思,像GStreamer架構中允許元件(plugin)於執行時期載入並動態設定參數的特性,依靠的就是GObject提供的功能。GStreamer將GLIb與GObject發揮得淋漓盡致,本身功能亦十分強大,將來會另有專題介紹。:)
要讓類別支援GObject整合至g_object_get_propertyg_object_set_property不難,首先要在GClassInit()階段,先行透過g_object_class_install_property()註冊類別的各個屬性並改寫(override)GObject類別之set_property與get_property函式。g_object_class_install_property之宣告如下:

void g_object_class_install_property(GObjectClass *oclass,
                                     guint property_id,
                                     GParamSpec *pspec);
由該宣告可知,要註冊型別需要property id與property spec,其中property id乃用來代表類別屬性之整數,並不會對外公開;property spec則是用來描述屬性的特徵(metadata),如:名稱、資料型態、最大值及最小值等等。
上述步驟完成後,每當g_object_get_propertyg_object_set_property被呼叫時,覆寫之set_property與get_property函式便會被喚起,當下只要正確處理屬性的讀取或設置即可。舉例如下:
... ... ...

enum
{
    PROP_UNSPECIFIED,

    PROP_RADIUS,
};

    ... ... ...

static void circle_class_init(CircleClass* klass)
{
    GParamSpec* spec;

    g_print("Circle(0x%08x)::class::initialize\n", (guint)klass);

    circle_parent_class = g_type_class_peek_parent(klass);

    G_OBJECT_CLASS(klass)->constructor = circle_constructor;
    G_OBJECT_CLASS(klass)->dispose     = circle_dispose;
    G_OBJECT_CLASS(klass)->finalize    = circle_finalize;

    G_OBJECT_CLASS(klass)->set_property = circle_set_property;
    G_OBJECT_CLASS(klass)->get_property = circle_get_property;

    spec = g_param_spec_uint("radius",
                             "radius",
                             "Set/Get circle's radius",
                             0,         /* minimum value */
                             G_MAXUINT, /* maximum value */
                             1,         /* default value */
                             G_PARAM_CONSTRUCT | G_PARAM_READWRITE);
    g_object_class_install_property(G_OBJECT_CLASS(klass),
                                    PROP_RADIUS,
                                    spec);

    SHAPE_CLASS(klass)->calculateArea = (void*)circle_calculate_area;

    g_type_class_add_private(klass, sizeof(CirclePrivate));
}

    ... ... ...

static void circle_set_property(GObject*      object,
                                guint         property_id,
                                const GValue* value,
                                GParamSpec*   pspec)
{
    Circle* self = CIRCLE(object);

    switch (property_id) {
    case PROP_RADIUS:
        self->priv->radius = g_value_get_uint(value);
        g_print("Circle(0x%08x)::SetProperty::Radius(%u)\n",
                (guint)self, self->priv->radius);
        break;
    default:
        /* We don't have any other property... */
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

static void circle_get_property(GObject*    object,
                                guint       property_id,
                                GValue*     value,
                                GParamSpec* pspec)
{
    Circle* self = CIRCLE(object);

    switch (property_id) {
    case PROP_RADIUS:
        g_print("Circle(0x%08x)::GetProperty::Radius(%u)\n",
                (guint)self, self->priv->radius);
        g_value_set_uint(value, self->priv->radius);
        break;
    default:
        /* We don't have any other property... */
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

    ... ... ...
當加入此機制後,g_object_new也將支援物件於建構時期之自動屬性設定(property binding)。
支援此機制之前的程式碼:
... ... ...
Circle* circle_new(guint radius)
{
    Circle* circle;

    circle = g_object_new(CIRCLE_TYPE, NULL);
    g_assert(circle != NULL);

    circle->priv->radius = radius;

    g_print("Circle(0x%08x, r=%u) is created.\n",
            (guint)circle, circle->priv->radius);

    return circle;
}
    ... ... ...
支援此機制之後:
... ... ...
Circle* circle_new(guint radius)
{
    Circle* circle;

    circle = g_object_new(CIRCLE_TYPE, "radius", radius, NULL);
    g_assert(circle != NULL);

    g_print("Circle(0x%08x, r=%u) is created.\n",
            (guint)circle, circle->priv->radius);

    return circle;
}
    ... ... ...
GObject system的屬性連結機制令人又愛又恨,冗長的程式不保證帶來等量的效益,從軟體工程角度,考量此功能進場的時間點,說不定是一個較佳的解答。To property binding or not to bind, that is the question.


No comments:

Post a Comment