Perl 6 程式設計教學:類別 (Class) 和物件 (Object)

PUBLISHED ON DEC 19, 2017 — PROGRAMMING
FacebookTwitter LinkedIn LINE Skype EverNote GMail Email Email

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

    十分鐘的物件導向概論

    很多語言都直接在語法機制中支援物件導向,然而,每個語言支援的物件導向特性略有不同,像 C++ 的物件系統相當完整,而 Perl 5 在引入 Moose 前的物件系統則相對原始。物件導向在理論上是和語言無關的程式設計模式,但在實務上卻受到語言特性的影響。學習物件導向時,除了學習在某個特定語言下的實作方式外,更應該學習其抽象層次的思維,有時候,暫時放下實作細節,從更高的視角看物件及物件間訊息的流動,對於學習物件導向有相當的幫助。

    物件導向是一種將程式碼以更高的層次組織起來的方法。大部分的物件導向以類別 (class) 為基礎,透過類別可產生實際的物件 (object) 或實體 (instance) ,類別和物件就像是餅乾模子和餅乾的關係,透過同一個模子可以產生很多片餅乾。物件擁有屬性 (field) 和方法 (method),屬性是其內在狀態,而方法是其外在行為。透過物件,狀態和方法是連動的,比起傳統的程序式程式設計,更容易組織程式碼。

    許多物件導向語言支援封裝 (encapsulation),透過封裝,程式設計者可以決定物件的那些部分要對外公開,那些部分僅由內部使用,封裝不僅限於靜態的資料,決定物件應該對外公開的行為也是封裝。當多個物件間互動時,封裝可使得程式碼容易維護,反之,過度暴露物件的內在屬性和細部行為會使得程式碼相互糾結,難以除錯。

    物件間可以透過組合 (composition) 再利用程式碼。物件的屬性不一定要是基本型別,也可以是其他物件。組合是透過有… (has-a) 關係建立物件間的關連。例如,汽車物件有引擎物件,而引擎物件本身又有許多的狀態和行為。繼承 (inheritance) 是另一個再利用程式碼的方式,透過繼承,子類別 (child class) 可以再利用父類別 (parent class) 的狀態和行為。繼承是透過是… (is-a) 關係建立物件間的關連。例如,研究生物件是學生物件的特例。然而,過度濫用繼承,容易使程式碼間高度相依,造成程式難以維護。可參考組合勝過繼承 (composition over inheritance) 這個指導原則來設計自己的專案。

    透過多型 (polymorphism) 使用物件,不需要在意物件的實作,只需依照其公開介面使用即可。例如,我們想用汽車物件執行開車這項行為,不論使用 Honda 汽車物件或是 Ford 汽車物件,都可以執行開車這項行為,而不需在意不同汽車物件間的實作差異。多型有許多種形式,如:

    • 特定多態 (ad hoc polymorphism):
      • 函數重載 (functional overloading):同名而不同參數型別的方法 (method)
      • 運算子重載 (operator overloading) : 對不同型別的物件使用相同運算子 (operator)
    • 泛型 (generics):對不同型別使用相同實作
    • 子類型 (Subtyping):不同子類別共享相同的公開介面,不同語言有不同的繼承機制

    以物件導向實作程式,需要從宏觀的角度來思考,不僅要設計單一物件的公開行為,還有物件間如何互動,以達到良好且易於維護的程式碼結構。除了閱讀本書或其他程式設計的書籍以學習如何實作物件外,可閱讀關於物件導向分析及設計 (object-oriented analysis and design) 或是設計模式 (design pattern) 的書籍,以增進對物件導向的了解。

    建立類別和物件

    使用 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";
    

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

    如果想要提供預設值,可略為修改一下 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) 呼叫,見下文。

    宣告方法

    我們將 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";
    

    Perl 6 的建構子 (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 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 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。

    組合

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

    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)。