2009年9月9日 星期三

類別的抽象意涵

到底基於什麼樣的誤會會讓人讓 Point3d 繼承 Point2d?有人會說「空間的點」是一種「平面的點」嗎?

class Point2d {
private:
    int x;
    int y;
public:
    Point2d(): x(0), y(0) {}
    Point2d(int x_, int y_) : x(x_), y(y_) {}

    int get_x() const { return x; }
    int get_y() const { return y; }

    void set_x(int x_) { x = x_; }
    void set_y(int y_) { y = y_; }
};

class Point3d : public Point2d {
private:
    int z;
public:
    Point3d(): Point2d(0, 0), z(0) {}
    Point3d(int x_, int y_, int z_): Point2d(x_, y_), z(z_) {}

    int get_z() const { return z; }
    void set_z(int z_) { z = z_; }
};

上面的程式碼看似可以重復使用 Point2d 的部分,基於「儘可能重複使用原則」,這樣寫好像沒有錯。可是如果今天我有一個函式長這樣呢?

int dist(Point2d const &a, Point2d const &b) {
    return sqrt(pow((double)(a.get_x() - b.get_x()), 2) +
                pow((double)(a.get_y() - b.get_y()), 2));
}

然後假設經過很久的時間,你早已忘記你的 Point3d 還有 dist 是怎麼寫得。有一天我們忽然必需要計算空間中的二點的距離,於是你很自然地寫下下面的程式碼:

Point3d pa(0, 0, 0);
Point3d pb(0, 0, 10);

double d1 = dist(pa, pb); // d1 = 0

這時你會得到一個讓你意外的結果!為什麼是 0?沒有任何的機制阻止你犯下錯誤。當然,當你發現數值不對的時候,也許會認為在多補一個 Point3d 的 dist 函式就可以了(事實上你也應該這麼做)。但是事情有這麼簡單嗎?

Point2d pc(0, 0);
Point3d pd(0, 0, 10);

double d2 = dist(pc, pd);

上面的程式出了什麼問題?對,沒有錯,dist 又把 Point3d 的 instance 當成 Point2d 的 instance。然後就是你的程式怎麼死得都不知道!

這組 class 的設計有個根本的問題就是隨便使用繼承的語意,看到「重複的程式碼」就開槍!結果反而寫出意想不到的程式碼。根本的解決方式就是抄一次 int x, y ... 等程式碼,而且多抄一次錯誤的機會反而會更少。

當然我知道有人會反駁說我應該使用 private inheritance。對,private inheritance 的確有 has-a 的語意,可是你認為這樣的程式碼比較直觀嗎?

class Point3d : private Point2d {
private:
    int z;
public:
    Point3d(): Point2d(0, 0), z(0) {}
    Point3d(int x_, int y_, int z_): Point2d(x_, y_), z(z_) {}

    using Point2d::get_x;
    using Point2d::get_y;
    int get_z() const { return z; }

    using Point2d::set_x;
    using Point2d::set_y;
    void set_z(int z_) { z = z_; }
};

不要自欺欺人了,要是「重複使用」這麼重要,你也應該使用 composition 與 delegation 來完成 has-a 的語意。
class Point3d {
private:
    Point2d xy;
    int z;
public:
    Point3d(): xy(0, 0), z(0) {}
    Point3d(int x_, int y_, int z_): xy(x_, y_), z(z_) {}

    int get_x() const { return xy.get_x(); }
    int get_y() const { return xy.get_y(); }
    int get_z() const { return z; }

    void set_x(int x_) { xy.set_x(x_); }
    void set_y(int y_) { xy.set_y(y_); }
    void set_z(int z_) { z = z_; }
};

我今天看書的時候還看到更扯的例子:橢圓是圓形的子類別,因為橢圓要二個參數來描述,圓只需要一個,同理矩形是正方形的子類別。還有,因為向量表示法很相似,所以三角形是一種矩型。照這種亂七八糟繼承法推演下去,所有的形狀都可以是正方形的一種(is-a-kind-of),你看正方形所需的參數最少嘛!?

這樣為了重複使用而重複使用對嗎?明明是毫不相干的類別我們應該讓他們互相繼承嗎?原本是超集的集合現在要變成別人的子集對嗎?敝人不才,我不懂這些聰明絕頂的人在想什麼。

1 則留言: