前言
物件導向程式設計 (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) x
和 y
直接對外公開,在實務上,這樣的方式比較不好,因為我們無法控管公開屬性。比較好的方法是將屬性私有化,再用公開方法 (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)。