9. クラス & オブジェクト
前の章では、Perl6の関数プログラミングの機能について学びました。
この章では、Perl6でのオブジェクト指向プログラミングを見ていきます。
9.1. 導入
オブジェクト指向 プログラミングは、近年広く使われているパラダイムです。
オブジェクト とは、いくつかの変数とサブルーチンをひとまとめにしたものです。
これらの変数は 属性、サブルーチンは メソッド と呼ばれます。
属性はオブジェクトの 状態 を、メソッドはオブジェクトの 振る舞い を定義します。
クラス は オブジェクト の構造を定義します。
この関係を理解するために、下の例を見てみましょう:
部屋の中に4名いる |
オブジェクト ⇒ 4名 |
これら4名は人間である |
クラス ⇒ 人間 |
それぞれ異なる名前、年齢、性別、国籍を持っている |
属性 ⇒ 名前, 年齢, 性別, 国籍 |
オブジェクト指向 の用語では、オブジェクトはクラスの インスタンス であると言います。
次のスクリプトを見てください:
class Human {
has $name;
has $age;
has $sex;
has $nationality;
}
my $john = Human.new(name => 'John', age => 23, sex => 'M', nationality => 'American');
say $john;
class
キーワードを使ってクラスを定義します。
has
キーワードでクラスの属性を定義します。
.new()
メソッドは コンストラクタ と呼ばれます。メソッド呼び出しに使われたクラスのインスタンスとしてオブジェクトを作成します。
上のスクリプトでは、新しい変数 $john
が、 Human.new()
で定義された新しい "Human" インスタンスへの参照を保持しています。
.new()
メソッドに渡された引数は、元になるオブジェクトの属性をセットするのに使われています。
クラスを レキシカルスコープ にするには、 my
を使います:
my class Human {
}
9.2. カプセル化
カプセル化とは、データとメソッドをセットにするというオブジェクト指向の概念です。
オブジェクト内のデータ(属性)は プライベート 、つまりオブジェクト内からのみアクセス可能であるべきです。
オブジェクトの外から属性にアクセスするには、 アクセサ と呼ばれるメソッドを使います。
下の2つのスクリプトは同じ結果になります。
my $var = 7;
say $var;
my $var = 7;
sub sayvar {
$var;
}
say sayvar;
sayvar
メソッドがアクセサです。アクセサを使うことで、変数に直接アクセスすることなく、変数の値にアクセスできます。
Perl6では ツイジル を使うことで容易にカプセル化ができます。
ツイジルとは、2つ目の シジル で、シジルと属性名の間に入ります。
クラスでは2種類のツイジルが使われます:
-
!
を使い、その属性がプライベートであることを明示的に宣言します。 -
.
を使うと、その属性のアクセサが自動的に生成されます。
デフォルトでは、すべての属性はプライベートです。しかし、常に !
ツイジルを使うのが良い習慣です。
そうであれば、上のクラスは次のように書き直すべきでしょう:
class Human {
has $!name;
has $!age;
has $!sex;
has $!nationality;
}
my $john = Human.new(name => 'John', age => 23, sex => 'M', nationality => 'American');
say $john;
このスクリプトに次の分を付け足してみてください: say $john.age;
すると次のようなエラーが帰ってくるはずです: Method 'age' not found for invocant of class 'Human'
理由は、 $!age
がプライベートで、オブジェクト内からしか利用できないからです。
オブジェクトの外からアクセスしようとするとエラーが返されます。
今度は、 has $!age
を has $.age
に置き換えて、say $john.age;
の結果がどうなるかを試してみてください。
9.3. 名前付き引数とポジショナル引数
Perl6では、すべてのクラスはデフォルトの .new()
コンストラクタを継承します。
これを使って、引数を渡してオブジェクトを作成することができます。
デフォルトのコンストラクタには、 名前付き引数 しか渡すことができません。
上の例をよく見てみると、 .new()
に渡される引数はすべて名前で定義されていることに気づくでしょう:
-
name ⇒ 'John'
-
age ⇒ 23
もし、新しいオブジェクトを作成するたびにいちいち属性の名前を渡したくない場合にはどうすればいいのでしょうか?
その場合には、 ポジショナル引数 を取る新しいコンストラクタを作る必要があります。
class Human {
has $.name;
has $.age;
has $.sex;
has $.nationality;
#new constructor that overrides the default one.
method new ($name,$age,$sex,$nationality) {
self.bless(:$name,:$age,:$sex,:$nationality);
}
}
my $john = Human.new('John',23,'M','American');
say $john;
ポジショナル引数を取る上のように定義されます。
9.4. メソッド
9.4.1. 導入
メソッドとは、オブジェクトの サブルーチン のことです。
サブルーチンと同様に、メソッドはいくつかの操作をまとめたものであり、 引数 を取り、 シグネチャ を持ち、 multi として定義することができます。
メソッドは method
キーワードで定義されます。
一般的な状況では、オブジェクトの属性に関して何らかの操作を行うためにメソッドが使われます。
また、実際にそうすることで、カプセル化を推進することができます。オブジェクトの属性はオブジェクト内からメソッドを使ってのみ操作できるようになるからです。
外の世界からは、オブジェクトのメソッドを通しての交流があるだけで、属性へのアクセスはありません。
class Human {
has $.name;
has $.age;
has $.sex;
has $.nationality;
has $.eligible;
method assess-eligibility {
if self.age < 21 {
$!eligible = 'No'
} else {
$!eligible = 'Yes'
}
}
}
my $john = Human.new(name => 'John', age => 23, sex => 'M', nationality => 'American');
$john.assess-eligibility;
say $john.eligible;
メソッドがクラス内で定義されると、オブジェクトに対して ドット記法 で呼び出すことができます:
オブジェクト . メソッド
、あるいは上の例で言えば: $john.assess-eligibility
メソッド定義内で、他のメソッドを呼び出すためにそのオブジェクトを参照するには、 self
キーワードを使います。
メソッド定義内で、属性を参照するには !
を使います。たとえ .
を使って定義されていてもです。
原理の説明をすると、 .
ツイジルで定義するのは、 !
付きのアトリビュートを宣言し、自動的にアクセサ作成をするのと同じことなのです。
上の例では if self.age < 21
と if $!age < 21
は同じ効果があります。ただし、技術的には違うものです:
-
self.age
は.age
メソッド(アクセサ)を呼び出しています。
$.age
と書くこともできます。 -
$!age
は変数を直接呼び出しています。
9.4.2. プライベートメソッド
普通のメソッドはオブジェクトに対してクラスの外から呼び出すことができます。
プライベートメソッド クラス内からのみ呼び出せるメソッドです。
使い道としては、他のメソッドを呼び出して何かをさせるメソッド作れます。
こうすることで、ひとつのメソッドを外の世界に対してパブリックにしながら、参照されている方のもうひとつをプライベートにしておくことができます。
ユーザーに直接呼び出してほしくないメソッドを、プライベートとして宣言しておくわけです。
プライベートメソッドを宣言するには !
ツイジルを名前の前につけます。
プライベートメソッドは !
を付けて呼び出します。 .
ではありません。
method !iamprivate {
#code goes in here
}
method iampublic {
self!iamprivate;
#do additional things
}
9.5. クラス属性
クラス属性 は、クラスのオブジェクトにではなく、クラス自身の属性のことです。
クラス属性は定義の際に初期化されます。
クラス属性は、 my
を使って宣言します。 has
ではありません。
クラスに対して呼び出されます。そのオブジェクトに対してではありません。
class Human {
has $.name;
my $.counter = 0;
method new($name) {
Human.counter++;
self.bless(:$name);
}
}
my $a = Human.new('a');
my $b = Human.new('b');
say Human.counter;
9.6. アクセスタイプ
ここまで見た例では全て、アクセサをオブジェクト属性を取得するために使用してきました。
では、属性の値を変更したい場合にはどうするのでしょうか?
その場合には、is rw
キーワードを使って、 読み/書き のラベル付けをする必要があります。
class Human {
has $.name;
has $.age is rw;
}
my $john = Human.new(name => 'John', age => 21);
say $john.age;
$john.age = 23;
say $john.age;
すべての属性はデフォルトで 「読み」のみ になりますが、 is readonly
を使って明示的にそうすることもできます。
9.7. 継承
9.7.1. 導入
継承 は、オブジェクト指向プログラミングで使われる別の概念のひとつです。
クラスを定義していると、すぐに多くのクラスが共通の属性/メソッドを持つことに気づきます。
何度も同じコードを書かなきゃいけませんか?
そんなことはありません! 継承 を使いましょう。
「人間」と「従業員」を表す2つのクラスを定義するとしましょう。
人間には2つの属性があります:名前と年齢です。
従業員には4つの属性があります:名前、年齢、会社、給与です。
次のようなクラスを作りたくなるかもしれません:
class Human {
has $.name;
has $.age;
}
class Employee {
has $.name;
has $.age;
has $.company;
has $.salary;
}
このコードは技術的には問題ありませんが、もう少し何とかなりそうな気がします。
次のように書いた方がずっと良いでしょう:
class Human {
has $.name;
has $.age;
}
class Employee is Human {
has $.company;
has $.salary;
}
継承は is
キーワードを使って定義します。
オブジェクト指向の用語では、従業員クラスは人間クラスの 子 であると言い、人間クラスは従業員クラスの 親 であると言います。
すべての子クラスは、属性とメソッドを親クラスから継承します。ですので、再び定義する必要はありません。
9.7.2. オーバーライド
クラスはすべての属性とメソッドを親クラスから継承します。
しかし時には、子クラスのメソッドが親クラスから継承したメソッドと違う動作をしてほしいことがあります。
その場合には、子クラス内で、メソッドを再定義します。
これをメソッドの オーバーライド(上書き) と呼びます。
下の例では、メソッド introduce-yourself
を従業員クラスから継承しています。
class Human {
has $.name;
has $.age;
method introduce-yourself {
say 'Hi i am a human being, my name is ' ~ self.name;
}
}
class Employee is Human {
has $.company;
has $.salary;
}
my $john = Human.new(name =>'John', age => 23,);
my $jane = Employee.new(name =>'Jane', age => 25, company => 'Acme', salary => 4000);
$john.introduce-yourself;
$jane.introduce-yourself;
次のようにオーバーライドできます:
class Human {
has $.name;
has $.age;
method introduce-yourself {
say 'Hi i am a human being, my name is ' ~ self.name;
}
}
class Employee is Human {
has $.company;
has $.salary;
method introduce-yourself {
say 'Hi i am a employee, my name is ' ~ self.name ~ ' and I work at: ' ~ self.company;
}
}
my $john = Human.new(name =>'John',age => 23,);
my $jane = Employee.new(name =>'Jane',age => 25,company => 'Acme',salary => 4000);
$john.introduce-yourself;
$jane.introduce-yourself;
どのクラスのオブジェクトかによって、それ用のメソッドが呼ばれます。
9.7.3. サブメソッド
サブメソッド とは、子クラスに継承されないメソッドです。
そのメソッドが定義されたクラスの中からしかアクセスできません。
submethod
キーワードを使って定義されます。
9.8. 多重継承
Perl6では多重継承ができます。ひとつのクラスが他の複数のクラスから継承できます。
class bar-chart {
has Int @.bar-values;
method plot {
say @.bar-values;
}
}
class line-chart {
has Int @.line-values;
method plot {
say @.line-values;
}
}
class combo-chart is bar-chart is line-chart {
}
my $actual-sales = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);
my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
line-values => [9,8,10,7,6,9]);
say "Actual sales:";
$actual-sales.plot;
say "Forecast sales:";
$forecast-sales.plot;
say "Actual vs Forecast:";
$actual-vs-forecast.plot;
出力
Actual sales: [10 9 11 8 7 10] Forecast sales: [9 8 10 7 6 9] Actual vs Forecast: [10 9 11 8 7 10]
combo-chart
クラスは、2つの数列を持つことができます。ひとつは棒グラフになる実際の値、もうひとつは線グラフになる予測値です。
これが、 line-chart
と bar-chart
の両方の子クラスとして定義した理由です。
combo-chart
の plot
メソッドを呼び出しても期待した結果が得られないことに気づくでしょう。
ひとつの数列しかプロットされません。
なぜでしょう?
combo-chart
は line-chart
と bar-chart
から継承していて、どちらにも plot
というメソッドがあります。
そのため、combo-chart
のそのメソッドを呼び出すと、Perl6はそのうちのひとつを呼び出すことで、継承されたメソッド名の衝突を解決しようとします。
正しく動作させるには、 combo-chart
の plot
メソッドをオーバーライドします。
class bar-chart {
has Int @.bar-values;
method plot {
say @.bar-values;
}
}
class line-chart {
has Int @.line-values;
method plot {
say @.line-values;
}
}
class combo-chart is bar-chart is line-chart {
method plot {
say @.bar-values;
say @.line-values;
}
}
my $actual-sales = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);
my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
line-values => [9,8,10,7,6,9]);
say "Actual sales:";
$actual-sales.plot;
say "Forecast sales:";
$forecast-sales.plot;
say "Actual vs Forecast:";
$actual-vs-forecast.plot;
出力
Actual sales: [10 9 11 8 7 10] Forecast sales: [9 8 10 7 6 9] Actual vs Forecast: [10 9 11 8 7 10] [9 8 10 7 6 9]
9.9. ロール
ロール は、それが属性とメソッドをまとめたものであるという意味では、クラスのようなものです。
ロールは role
キーワードで宣言されます。そしてクラスにロールを実装するには、 does
キーワードを使います。
role bar-chart {
has Int @.bar-values;
method plot {
say @.bar-values;
}
}
role line-chart {
has Int @.line-values;
method plot {
say @.line-values;
}
}
class combo-chart does bar-chart does line-chart {
method plot {
say @.bar-values;
say @.line-values;
}
}
my $actual-sales = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);
my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
line-values => [9,8,10,7,6,9]);
say "Actual sales:";
$actual-sales.plot;
say "Forecast sales:";
$forecast-sales.plot;
say "Actual vs Forecast:";
$actual-vs-forecast.plot;
上のスクリプトを実行すると、同じ結果が得られるでしょう。
きっと今こう思っているでしょう。ロールがクラスと同じような動きをするなら、一体なぜ必要なんだろう?
その質問に答えるために、最初の多重継承の最初の例を変更してみましょう。
plot
メソッドのオーバーライドを 忘れた 方のやつです。
role bar-chart {
has Int @.bar-values;
method plot {
say @.bar-values;
}
}
role line-chart {
has Int @.line-values;
method plot {
say @.line-values;
}
}
class combo-chart does bar-chart does line-chart {
}
my $actual-sales = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);
my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
line-values => [9,8,10,7,6,9]);
say "Actual sales:";
$actual-sales.plot;
say "Forecast sales:";
$forecast-sales.plot;
say "Actual vs Forecast:";
$actual-vs-forecast.plot;
出力
===SORRY!=== Method 'plot' must be resolved by class combo-chart because it exists in multiple roles (line-chart, bar-chart)
同じクラスに割り当てた複数のロールで衝突が起こった場合、コンパイルエラーになります。
これは衝突がエラーにならずに単に実行時に解決されるより多重継承よりも、ずっと安全なアプローチです。
ロールは、衝突があると教えてくれるのです。
9.10. イントロスペクション
イントロスペクション とは、型や属性、メソッドなど、オブジェクトの情報を取得することを言います。
class Human {
has Str $.name;
has Int $.age;
method introduce-yourself {
say 'Hi i am a human being, my name is ' ~ self.name;
}
}
class Employee is Human {
has Str $.company;
has Int $.salary;
method introduce-yourself {
say 'Hi i am a employee, my name is ' ~ self.name ~ ' and I work at: ' ~ self.company;
}
}
my $john = Human.new(name =>'John',age => 23,);
my $jane = Employee.new(name =>'Jane',age => 25,company => 'Acme',salary => 4000);
say $john.WHAT;
say $jane.WHAT;
say $john.^attributes;
say $jane.^attributes;
say $john.^methods;
say $jane.^methods;
say $jane.^parents;
if $jane ~~ Human {say 'Jane is a Human'};
イントロスペクションには、以下が使えます:
-
.WHAT
は、オブジェクトを作成したクラスを返します。 -
.^attributes
は、オブジェクトのすべての属性が入ったリストを返します。 -
.^methods
は、オブジェクトに対して呼び出せるすべてのメソッドを返します。 -
.^parents
オブジェクトが属しているクラスのすべての親クラスを返します。 -
~~
はスマートマッチ演算子と呼ばれます。 オブジェクトが、比較対象のクラスもしくはそこから継承したものから作成されている場合に True に評価されます。