これは Perl Advent Calendar 2018 における21日目の記事です。
TL; DR
- ある程度のクラス図は機械的に出力できるが、サブルーチン内部は書く必要がある
- 関係は表現しずらいが、それ以外は機械的に出力できる
- 図は見やすい(議論の余地あり)
始めに
クラス図、書いていますか?
いえ、書いている人が多くないことはわかっています。
特に、Perlのようなインタプリタ言語を利用するにあたって、いちいちクラス図を書くのは面倒ですよね。
書かない理由は、次の3つが主な原因ではないかと推察します。
- クラス図を書いてもコードができるわけではなく必要性が感じられない
- コードの修正をするとクラス図も修正しなければならず手間が増える
- 読み書きできる人が多くない
今回は、上記の問題に対処するため、クラス図とPerlソースコードの対応関係を提案します。
- クラス図を書けばPerlを(機械的に)生成できる
- クラス図を修正すればコードを修正したことになる
- Perlを読める人はクラス図を読めるようになり親しみやすさを覚える
更に、クラス図 to Perlツールを誰かが作ってくれたら、もうPerlを手で書く必要はありません!(超誇大広告)
これを読めば、巷にあふれるクラス図からPerlを用いたソースコードを大量生産できること間違いなしです!
なお、この内容は必ずしも正確であるわけではなく、独自の考えを含んでいます。
Mouseモジュールなどを利用すればもっと簡単に書けるようになると思いますが、自動生成コードっぽくするためにそれらのモジュールは使わずに書いてみます。
また、すでにPerlのソースコードからクラス図を出力できるツールはいくつか1存在します。
それらとの違いについては、次に示す通りです。
- 汎化関係以外の関係を議論
- クラス以外の要素を議論
例
こういうものは、一覧をバンッと表示するより、具体例で示した方がわかりやすいことが往々にしてあります。
よって、今回はクラス図の例と、それをPerlで記述するとどうなるか見ていきます。
クラス図
簡単なお絵かきアプリを作る際に必要となる、キャンバスを表現するクラス図です。
CanvasクラスがFigureインタフェースの実装クラスのインスタンス(図形)の集合を描画します。
createFigure(shape : Shape)
関数で生成したFigureインタフェースのインスタンスを変数 figures
に格納します。
インタフェースを直接インスタンス化できないため、依存の関係として記述しています。
FigureインタフェースはFigureDrawer抽象クラスで実現していますが、肝心の draw()
関数は未実装のままです。
FigureDrawer抽象クラスでは、図形の左上隅と右下隅の座標を設定できるようにしています。
FigureDrawer抽象クラスは、次の4つのクラスで実装しています。
- Circleクラス
- Lineクラス
- Quadrangleクラス
- Polygonクラス
Circleクラスでは、FigureDrawerクラスで設定した左上隅と右下隅の座標を元に、円の中心の座標と半径を計算し描画します。
Lineクラスでは、コンポジション関係の begin
とコンポジション関係の end
を元に描画しますが、 draw(begin : Point, end : Point)
でも同様のことができます。
Quadrangleクラスでは、FigureDrawerクラスで設定した左上隅と右下隅の座標を元に、四隅の座標を計算し、Lineクラスのインスタンスを4つ生成します。
Lineクラスの4つのインスタンス lines
を描画すれば、四角形が描画できるという仕組みです。
Polygonクラスでは、隅の配列を入力することで、Lineクラスのインスタンスのリストを生成していきます。
Quadrangleクラスと同様、Lineクラスのインスタンスの配列 lines
を描画すれば、多角形が描画できるという仕組みです。
Pointクラスは座標を表現するクラスです。
<<Create>>
というステレオタイプを付随した関数を2つ用意しており、 <<Create>> + Point(x : Integer, y : Integer)
ではインスタンス生成時に座標のX軸およびY軸の値を設定できます。
Shape列挙子はCanvasクラス内で生成する図形を判定する際に利用します。
Canvasクラスと依存の関係として記述していますが、特に理由はありません。
宙ぶらりんの状態がなんとなく嫌だったのと、他のクラスに関係がない事を強調する意味合いを含めてみました。
Canvasクラス以外はdiagramパッケージに格納しています。
余談ですが、Integer型はUMLで定義しているプリミティブ型であり、各プログラミング言語には依存しません。
Perlソースコード
私が実装するならこうします、という例を挙げます。
ここに答えはないため、皆さんも考えてみてください。
リポジトリはこちらです。
ディレクトリ構造を以下に示します。
lib
│ Canvas.pm
│
└─diagram
Circle.pm
Figure.pm
FigureDrawer.pm
Line.pm
Point.pm
Polygon.pm
Quadrangle.pm
Shape.pm
Shape列挙型
package Shape;
use strict;
use warnings;
use enum qw/ circle line quadrangle polygon /;
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
}
sub Circle {
my $class = shift;
return circle;
}
sub Line {
my $class = shift;
return line;
}
sub Quadrangle {
my $class = shift;
return quadrangle;
}
sub Polygon {
my $class = shift;
return polygon;
}
1;
new
サブルーチンの必要性は感じられませんが、クラスメソッドを大量に作りました。
呼出し側は、 Shape->Circle
などで利用できます。
Canvasクラス
package Canvas;
use strict;
use warnings;
use diagram::Shape;
use diagram::Circle;
use diagram::Line;
use diagram::Quadrangle;
use diagram::Polygon;
sub new {
my $class = shift;
my $self = {
figures => []
};
bless $self, $class;
}
sub create_figure {
my ($self, $shape) = @_;
if ($shape == Shape->Circle) {
push @{$self->{figures}}, Circle->new;
} elsif ($shape == Shape->Line) {
push @{$self->{figures}}, Line->new;
} elsif ($shape == Shape->Quadrangle) {
push @{$self->{figures}}, Quadrangle->new;
} elsif ($shape == Shape->Polygon) {
push @{$self->{figures}}, Polygon->new;
}
}
sub draw {
my $self = shift;
$_->draw foreach @{$self->figures};
}
# アクセサ
sub figures {
my $self = shift;
if (@_) {
$self->{figures} = $_[0];
}
return $self->{figures};
}
1;
クラス図におけるキャメルケースで記述していた createFigure
をスネークケースの create_figure
に変換しました。
これは、クラス図がキャメルケースを推奨しているためです(キャメルケースがダメなわけではないです)。
しかし、Perlではスネークケースの方をよく見る(し私もスネークケースの方が好きである)ため、わざわざ変換しています。
Shape列挙型は if
文内での判定に利用しています。
本来、クラス図ではメソッド内部までの構造を正確には記述できません。
あくまで今回は例である、ということを踏まえていただけると幸いです。
属性として figures
を持っているので、アクセサを生成しました。
Perlではセッター、ゲッターをアクセサとして、両方の機能を兼ねる書き方が一般的です。
C++など他言語でも少しずつ使われているらしい......?
以降、殆ど同じ書き方のためにアクセサは省略します。
作っていて思ったのですが、Figureの実態をShapeで判定して作ってくれる機構を別に作った方がよかったですね。
せっかくFigureパッケージのみとの関係にしようと思ったのに、ソースコードで4つの図形のパッケージと関係を持ってしまったのは失敗でした。
Pointクラス
package Point;
use strict;
use warnings;
sub new {
my $class = shift;
my $self = {
x => 0,
y => 0
};
if (@_ == 2) {
$self->{x} = shift;
$self->{y} = shift;
}
bless $self, $class;
}
sub update {
my $self = shift;
if (@_ == 2) {
$self->{x} = shift;
$self->{y} = shift;
}
}
# アクセサ
sub x {}
sub y {}
1;
<<Create>>
はUMLで正式に定義しているステレオタイプではありません [要検証] が、UMLの仕様書内でも利用されているくらい一般的なものなので、コンストラクタとして既知のものであると定義して記述しました。
コンストラクタの名前はPerlでは一般的に new
なので、この中に引数がある場合の処理を追加しました。
update
関数はおまけです。
Figureインタフェース
package Figure;
use strict;
use warnings;
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
}
sub draw {
my $self = shift;
die "Please override!!\n";
}
1;
draw
サブルーチンをFigureクラスのインスタンスから直接実行しようとすると死ぬようにしています。
これでオーバーライドを促しています。
しかし実行時エラーじゃわかりづらいですよね、テストで見つけられたら万々歳です。
FigureDrawer抽象クラス
package FigureDrawer;
use strict;
use warnings;
use diagram::Point;
use diagram::Figure;
use base qw/ Figure /;
sub new {
my $class = shift;
my $self = $class->SUPER::new();
$self->{upper_left} = Point->new;
$self->{bottom_right} = Point->new;
bless $self, $class;
}
# アクセサ
sub upper_left {}
sub bottom_right {}
1;
Figureインタフェースを継承しつつ、 upper_left
アクセサおよび bottom_right
アクセサを記述しています。
Circleクラス
package Circle;
use strict;
use warnings;
use diagram::Point;
use diagram::FigureDrawer;
use base qw/ FigureDrawer /;
sub new {
my $class = shift;
my $self = $class->SUPER::new();
$self->{center} = Point->new;
$self->{radius} = 0;
bless $self, $class;
}
sub draw {
my $self = shift;
# 実装を記述
}
sub calculate_circle {
my $self = shift;
# 実装を記述
}
# アクセサ
sub radius {}
sub center {}
1;
Circleクラスで、遂に draw
サブルーチンを実装しています。
今回は中身に焦点を当てないため省略しています。
皆さんもぜひPerlTkなどで実装してみては(殴
また、calculate_circleは今回地味に書いているのですが、本題とそれるのでここには書きませんでした。
リポジトリにはあります。
Lineクラス
package Line;
use strict;
use warnings;
use diagram::Point;
use diagram::FigureDrawer;
use base qw/ FigureDrawer /;
sub new {
my $class = shift;
my $self = $class->SUPER::new();
$self->{begin} = Point->new;
$self->{end} = Point->new;
if (@_ == 2) {
$self->begin($_[0]) if $_[0]->isa("Point");
$self->end($_[1]) if $_[1]->isa("Point");
}
bless $self, $class;
}
sub draw {
my $self = shift;
if (@_ == 2) {
# 引数が2つの実装を記述
$self->draw_two_params($_[0], $_[1]);
} else {
# 引数が空の実装を記述
$self->draw_without_params;
}
}
sub draw_without_params {
my $self = shift;
# 実装する
}
sub draw_two_params {
my $self = shift;
my $begin = shift;
my $end = shift;
# 実装する
}
# アクセサ
sub begin {}
sub end {}
1;
クラス図のLineクラスにおける操作 draw()
と操作 draw(begin : Point, end, Point)
は1つのdrawサブルーチンにまとめています。
UMLでは同じ名前の操作でも引数が異なる場合は別の操作として共存できますが、Perlでは(特別なモジュールを利用しない限り)共存できません。
しかし、内部処理ではPointクラスと同様の引数判定を行うことで、同じような挙動ができると思います。
今回は、引数が無い場合と2つある場合のサブルーチンを作りました。
この辺は生成システムに依存すると思います。
Quadrangleクラス
package Quadrangle;
use strict;
use warnings;
use diagram::FigureDrawer;
use base qw/ FigureDrawer /;
sub new {
my $class = shift;
my $self = $class->SUPER::new();
$self->{lines} = [];
bless $self, $class;
}
sub draw {
my $self = shift;
# 実装を記述
$_->draw foreach @{$self->lines};
}
sub calculate_lines {
my $self = shift;
# 実装を記述
}
# アクセサ
sub lines {}
1;
calculate_lines
で生成した配列 lines
の draw
関数を順次呼出すことで、四角形を描画するようにしています。
calculate_lines
の中身は省略します。
Polygonクラス
殆ど Quadrangle.pm
ファイルと同じため、省略します。
クラス図の要素まとめ
簡単に、クラス図が保持できる要素をまとめてみました。
なお、全てを記入しているわけではありません。
一般的に知られている部分は太字で表記しています。
パッケージについては、正確にはパッケージ図に含まれるものですが、今回は同じ土俵にあげておきました。
-
クラス
- クラス名
- 属性
- 操作
- パッケージ
- インタフェース
- 抽象クラス
-
関係
- 依存
- 関連
- 集約
- コンポジション
- 汎化
- 実現
- 関連端名
随分多いことがわかりますが、これでも一部であることを考えると、いったい誰が全てを把握しているのかわからなくなってきますね。
ですが、比較的有名なものが多いため、意外とどうにかなりそうです。
1つづつ見ていきましょう。
対応関係
クラス図を表すPlantUMLの要素をPerlソースコードに変換していきます。
これで、機械的に変換できるように表現しています。
なお、一部そりゃそうだと思われるような部分については説明を省略しています。
クラス
クラス名
package ClassName;
use strict;
use warnings;
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
}
Perlではクラス名をパッケージ名として記述します、ややこしいですね。
new
サブルーチンも一緒に生成しています。
属性
package ClassName;
use strict;
use warnings;
sub new {
my $class = shift;
my $self = {
attribute_id => 0,
attribute_name => ""
};
bless $self, $class;
}
sub attribute_id {
my $self = shift;
if (@_) {
$self->{attribute_id} = $_[0];
}
return $self->{attribute_id};
}
sub attribute_name {
my $self = shift;
if (@_) {
$self->{attribute_name} = $_[0];
}
return $self->{attribute_name};
}
1;
パッケージの持つ要素として表現できます。
また、アクセサも自動的に生成すれば有用性はあがるかもしれません。
キャメルケースとスネークケースの変換はご自由にどうぞ。
操作
package ClassName;
use strict;
use warnings;
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
}
sub print {
my $self = shift;
# 実装を記述
}
sub attribute_name {
my $self = shift;
return if @_ >= 2;
my $begin = $_[0];
my $end = $_[1];
return if $begin->isa("Point");
return if $end->isa("Point");
# 実装を記述
}
1;
引数も戻り値もない操作は、空のサブルーチンを生成すればいいと思います。
引数が複数存在する場合は、その数に応じて型判別などしてもいいかもしれません。
個人的に型判別は冗長かなとも思っていますが......
パッケージ
lib
│ GodClass.pm
│
└─parent
│ ParentClass.pm
│
└─child
ChildClass.pm
パッケージはディレクトリとして反映できます。
パスには注意して生成するとよいでしょう。
大枠のlibディレクトリをパッケージにしてもかまいません。
記述する場合は、おそらく他のディレクトリ(scriptなど)から呼出していることを書きたいのかもしれません。
インタフェース
package InterfaceName;
use strict;
use warnings;
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
}
sub draw {
my $self = shift;
die "Please override!!\n";
}
1;
die
関数や例外を投げるような構造を全てのサブルーチンに入れることに意味があるのかはわかりません。
しかし、呼出さないようにするという意味ではまだましかもしれません。
本当はコンパイルエラーを出してくれるといいかもしれませんが、そもそもインタフェースがPerlで必要性を感じないです......
抽象クラス
package AbstractClassName;
use strict;
use warnings;
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
}
sub print {
my $self = shift;
# 実装を記述
}
sub draw {
my $self = shift;
die "Please override!!\n";
}
1;
抽象クラスとインタフェースの違いはほぼありません。
しいて言えば、1つでも実装操作を持っていれば抽象クラスで、全て抽象操作であればインタフェースと考えるのが1つの手だと思います。
関係
汎化および実現
package SuperClass;
use strict;
use warnings;
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
}
package SubClass;
use strict;
use warnings;
use SubClass;
use base qw/SuperClass/;
sub new {
my $class = shift;
my $self = $class->SUPER::new();
bless $self, $class;
}
1;
実現はインタフェースを実装する関係、汎化は普通のクラスまたは抽象クラスを特化する関係です。
この2つは、Perlソースコード上ではどちらも継承関係で記述できます。
よって、Perlにおける違いはありません。
関連端名
package SubClass;
use strict;
use warnings;
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
}
package SuperClass;
use strict;
use warnings;
use SubClass;
sub new {
my $class = shift;
my $self = {
sub_class => SubClass->new
};
bless $self, $class;
}
sub sub_class {
my $self = shift;
if (@_) {
$self->{sub_class} = $_[0];
}
return $self->{sub_class};
}
1;
関係元のインスタンス名として、属性と同じように生成できます。
残る関係は、次の4つがあります。
- 依存
- 関連
- 集約
- コンポジション
しかし、これらを実装抜きで判断することはできません。
というのも、4つの関係は他のクラスでどのように関係しているかを、実装内で見ないとわからないからです。
よって、構文解析レベルではクラス図およびPerlソースコードを見ても判断できませんが、次のように考えることはできます。
- 依存
- 下の関係3つのどれにもあたらないが、パッケージ内で利用している場合
-
use
やrequire
で呼出しているパッケージが相当するかも - インタフェースの実態を持っていることを表現している場合
- 関連
- 下の関係2つのどれにもあたらないが、パッケージ内でインスタンスを生成している場合
- 生存期間がサブルーチン内でとどまっている場合が相当するかも
- 集約
- インスタンスを他のクラスでも参照している場合
- リファレンスを子クラスのインスタンスに代入している場合などが相当するかも
- コンポジション
- インスタンスを自クラス内でのみ利用している場合
- クラス図のクラスにおける属性とほぼ同義
関係の強さについては、他言語2でも構文解析だけでは表現できないらしいです。
終わりに
クラス図の読み方、Perlでの書き方はなんとなく理解できたでしょうか。
図を見れば全体像を俯瞰できるため、有用性は今でもあると思いますが、いかんせん誰も使っていないため、せっかくの言語非依存の図がもったいないです。
もし一連のコードを読んだ後に再度図を見れば、各要素と関係がより理解できると思います。
皆さんもこの機会に、少しでも書いてみてはいかがでしょうか!
次回は