類別 (Class) 和物件 (Object)

    前言

    物件導向程式設計 (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)。

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