【在主画面加入捷径】
       
【选择语系】
繁中 简中

Perl 6 程序设计教学:类 (Class) 和物件 (Object)

【赞助商连结】

    面向对象程序设计 (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)。

    【赞助商连结】