美思 [Raku] 程式設計教學:類別 (Class) 和物件 (Object)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

物件導向程式設計 (object-oriented programming) 是目前主流的程式設計模範 (paradigm),大部分主流的程式語言都支援物件導向程式。本文介紹 Raku 的物件系統。

建立類別和物件

使用 class 保留字可以建立類別 (class)。建立類別後,再由該類別建立物件 (object),如下例:

class Point {
    has Numeric $.x;
    has Numeric $.y;
}

my $p = Point.new(x => 3, y => 4);
$p.x == 3 or die "Wrong value";
$p.y == 4 or die "Wrong value";

如果想要設置固定位置參數,則需修改建構子:

class Point {
    has Numeric $.x;
    has Numeric $.y;

    # Overriding the constructor.
    method new ($x, $y) {
        self.bless(x => $x, y => $y);
    }
}

my $p = Point.new(3, 4);
$p.x == 3 or die "Wrong value";
$p.y == 4 or die "Wrong value";

但這種方式比較不符合 Raku 社群的習慣,因為將參數位置寫死,比較不靈活。

如果想要提供預設值,可略為修改一下 Point 類別:

class Point {
    has Numeric $.x is rw;
    has Numeric $.y is rw;

    submethod BUILD(:$x, :$y) {
        with $x {
            self.x = $x;
        } else {
            self.x = 0;
        }

        with $y {
            self.y = $y;
        } else {
            self.y = 0;
        }
    }
}

my $o = Point.new();
$o.x == 0 or die "Wrong value";
$o.y == 0 or die "Wrong value";

my $p = Point.new(x => 3, y => 4);
$p.x == 3 or die "Wrong value";
$p.y == 4 or die "Wrong value";

在本例中,屬性 (field) xy 直接對外公開,在實務上,這樣的方式比較不好,因為我們無法控管公開屬性。比較好的方法是將屬性私有化,再用公開方法 (public method) 呼叫,見下文。

使用實體方法 (Instance Method)

我們將 Point 類別改寫如下:

class Point {
    has Numeric $!_x;
    has Numeric $!_y;

    submethod BUILD(:$x, :$y) {
        self!x($x);
        self!y($y);
    }

    method x() {
        $!_x;
    }

    method y() {
        $!_y;
    }

    # Private setter for $_x.
    method !x($x) {
        $!_x = $x;
    }

    # Private setter for $_y
    method !y($y) {
        $!_y = $y;
    }
}

my $p = Point.new(x => 3, y => 4);
$p.x == 3 or die "Wrong value";
$p.y == 4 or die "Wrong value";

Raku 的建構子 (constructor) 預設使用 new 方法,我們要修改建構子的話,就要透過 BUILD 方法來間接修改。另外,在本例中,setter 是私有的,而 getter 是公開的,透過這樣的方式,建立 Point 物件後就不能修改其值。

註:setter 指修改屬性的方法,getter 指取得屬性的方法。

如果我們要將 setter 轉為公開,則需改寫如下:

class Point {
    has Numeric $!_x;
    has Numeric $!_y;

    submethod BUILD(:$x, :$y) {
        self.x($x);
        self.y($y);
    }

    multi method x() {
        $!_x;
    }

    multi method y() {
        $!_y;
    }

    multi method x($x) {
        $!_x = $x;
    }

    multi method y($y) {
        $!_y = $y;
    }
}

my $p = Point.new(x => 0, y => 0);
$p.x == 0 or die "Wrong value";
$p.y == 0 or die "Wrong value";

$p.x(3);
$p.y(4);
$p.x == 3 or die "Wrong value";
$p.y == 4 or die "Wrong value";

由於 getter 和 setter 都使用同樣的方法名稱,要使用 multi 來重載方法。

雖然在我們這個例子中,暫時看不到使用私有屬性搭配公開方法的好處,這樣修改類別後,就可以控管屬性。例如,我們將 setter 設為唯讀,類別使用者就不能修改屬性;或者,我們可以限定屬性 $!_x$!_y 的範圍等。

使用類別方法 (Class Method)

類別方法 (class method) 指的是不和特定物件綁定的方法,透過類別本身來呼叫,而不透過物件。如下例:

class Point {
    has Numeric $!_x;
    has Numeric $!_y;

    submethod BUILD(:$x, :$y) {
        self!x($x);
        self!y($y);
    }

    method x() {
        $!_x;
    }

    method y() {
        $!_y;
    }

    method !x($x) {
        $!_x = $x;
    }

    method !y($y) {
        $!_y = $y;
    }

    # Class method.
    our sub dist($p, $q) {
        sqrt(($p.x - $q.x) ** 2 + ($p.y - $q.y) ** 2);
    }
}

my $p = Point.new(x => 3, y => 4);
$p.x == 3 or die "Wrong value";
$p.y == 4 or die "Wrong value";

my $q = Point.new(x => 0, y => 0);
my $dist = Point::dist($p, $q);
$dist == 5 or die "Wrong value";

使用類別屬性 (Class Field)

類別屬性不屬於物件,而屬於類別本身。

class Point {
    # Class fields
    my Int $c = 0;

    # Instance fields
    has Numeric $!_x;
    has Numeric $!_y;

    submethod BUILD(:$x, :$y) {
        self!x($x);
        self!y($y);

        $c++;
    }

    method x() {
        $!_x;
    }

    method y() {
        $!_y;
    }

    method !x($x) {
        $!_x = $x;
    }

    method !y($y) {
        $!_y = $y;
    }

    # Class methods.
    our sub count() {
        $c;
    }
}

my $p = Point.new(x => 1, y => 2);
my $q = Point.new(x => 3, y => 4);
my $r = Point.new(x => 5, y => 6);
Point::count() == 3 or die "Wrong count";

註:經筆者實測,Perl 6 沒有解構子,當物件減少時,本程式會產生 bug。

利用組合 (Composition) 重用物件

除了使用基本型別外,也可以將物件組合起來,形成一個新的物件。可見下例:

class Point {
    has Numeric $!_x;
    has Numeric $!_y;

    submethod BUILD(:$x, :$y) {
        self!x($x);
        self!y($y);
    }

    method x() {
        $!_x;
    }

    method !x($x) {
        $!_x = $x;
    }

    method y() {
        $!_y;
    }

    method !y($y) {
        $!_y = $y;
    }
}

class Rectangle {
    has Numeric $!_width;
    has Numeric $!_height;
    has Point $!_point;

    submethod BUILD(:$point, :$width, :$height) {
        self!width($width);
        self!height($height);
        self!point($point);
    }

    method width {
        $!_width;
    }

    method !width($w) {
        if $w <= 0 {
            die "Invalid width.";
        }

        $!_width = $w;
    }

    method height {
        $!_height;
    }

    method !height($h) {
        if $h <= 0 {
            die "Invalid height";
        }

        $!_height = $h;
    }

    method point {
        $!_point;
    }

    method !point($p) {
        $!_point = $p;
    }

    method area {
        $!_width * $!_height;
    }
}

my $r = Rectangle.new(
        width => 10,
        height => 5,
        point => Point.new(x => 2, y => 3),
    );

$r.width == 10 or die "Wrong value";
$r.height == 5 or die "Wrong value";
$r.area == 50 or die "Wrong area";

$r.point.x == 2 or die "Wrong value";
$r.point.y == 3 or die "Wrong value";

在這個例子中,我們將 Point 做為 Rectangle 類別的一部分,藉此重覆使用程式碼。

組合是一種相對簡單的程式碼重用的方式,透過組合,可以將類別的權責劃分出來,避免同一個類別有太多的功能,也就是俗稱的上帝物件 (God object)。

關於作者

身為資訊領域碩士,美思認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

美思喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,美思將所學寫成文章,放在這個網站上和大家分享。