Edited at
PerlDay 17

クラス図とPerlの対応付け

これは Perl Advent Calendar 2018 における21日目の記事です。


TL; DR


  • ある程度のクラス図は機械的に出力できるが、サブルーチン内部は書く必要がある

  • 関係は表現しずらいが、それ以外は機械的に出力できる

  • 図は見やすい(議論の余地あり)


始めに

クラス図、書いていますか?

いえ、書いている人が多くないことはわかっています。

特に、Perlのようなインタプリタ言語を利用するにあたって、いちいちクラス図を書くのは面倒ですよね。

書かない理由は、次の3つが主な原因ではないかと推察します。


  • クラス図を書いてもコードができるわけではなく必要性が感じられない

  • コードの修正をするとクラス図も修正しなければならず手間が増える

  • 読み書きできる人が多くない

今回は、上記の問題に対処するため、クラス図とPerlソースコードの対応関係を提案します。


  • クラス図を書けばPerlを(機械的に)生成できる

  • クラス図を修正すればコードを修正したことになる

  • Perlを読める人はクラス図を読めるようになり親しみやすさを覚える

更に、クラス図 to Perlツールを誰かが作ってくれたら、もうPerlを手で書く必要はありません!(超誇大広告)

これを読めば、巷にあふれるクラス図からPerlを用いたソースコードを大量生産できること間違いなしです!


なお、この内容は必ずしも正確であるわけではなく、独自の考えを含んでいます。

Mouseモジュールなどを利用すればもっと簡単に書けるようになると思いますが、自動生成コードっぽくするためにそれらのモジュールは使わずに書いてみます。

また、すでにPerlのソースコードからクラス図を出力できるツールはいくつか1存在します。

それらとの違いについては、次に示す通りです。


  • 汎化関係以外の関係を議論

  • クラス以外の要素を議論


こういうものは、一覧をバンッと表示するより、具体例で示した方がわかりやすいことが往々にしてあります。

よって、今回はクラス図の例と、それをPerlで記述するとどうなるか見ていきます。


クラス図

簡単なお絵かきアプリを作る際に必要となる、キャンバスを表現するクラス図です。

キャンバスのクラス図

PlantUML文


キャンバスのクラス図

@startuml

skinparam classAttributeIconSize 0

package diagram {

class Point {
- x : Integer = 0
- y : Integer = 0
<<Create>> + Point()
<<Create>> + Point(x : Integer, y : Integer)
+ update(x : Integer, y : Integer)
}

interface Figure {
+ draw()
}

abstract FigureDrawer {
+ setUpperLeft(upperLeft : Point)
+ setBottomRight(bottomRight : Point)
{abstract} + draw()
}

class Circle {
- radius : Integer
+ draw()
+ calculateCircle()
}

class Line {
+ draw()
+ draw(begin : Point, end : Point)
}

class Quadrangle {
+ draw()
+ calculateLines()
}

class Polygon {
+ draw()
+ createLines(points : Point [*])
}

enum Shape {
Circle
Line
Quadrandle
Polygon
}
}

class Canvas {
+ createFigure(shape : Shape)
+ draw()
}

Figure <|.. FigureDrawer
FigureDrawer <|-- Line
FigureDrawer <|-- Circle
FigureDrawer <|-- Quadrangle
FigureDrawer <|-- Polygon

Canvas ..> "*\n - figures" Figure

FigureDrawer *-> " 1\n - upperLeft" Point
FigureDrawer *-> "1 \n - bottomRight" Point
Circle *-> "1 \n - center" Point
Quadrangle *--> " 4\n - lines" Line
Polygon *--> " *\n - lines" Line
Point " 1\n - begin" <--* Line
Point "1\n - end" <--* Line

Canvas .> Shape
@enduml



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列挙型


diagram/Shape.pm

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クラス


diagram/Canvas.pm

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クラス


diagram/Point.pm

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インタフェース


diagram/Figure.pm

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抽象クラス


diagram/FigureDrawer.pm

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クラス


diagram/Circle.pm

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クラス


diagram/Line.pm

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クラス


diagram/Quadrangle.pm

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 で生成した配列 linesdraw 関数を順次呼出すことで、四角形を描画するようにしています。

calculate_lines の中身は省略します。


Polygonクラス

殆ど Quadrangle.pm ファイルと同じため、省略します。


クラス図の要素まとめ

簡単に、クラス図が保持できる要素をまとめてみました。

なお、全てを記入しているわけではありません。

一般的に知られている部分は太字で表記しています。

パッケージについては、正確にはパッケージ図に含まれるものですが、今回は同じ土俵にあげておきました。



  • クラス


    • クラス名

    • 属性

    • 操作



  • パッケージ

  • インタフェース

  • 抽象クラス


  • 関係


    • 依存

    • 関連

    • 集約

    • コンポジション

    • 汎化

    • 実現

    • 関連端名



随分多いことがわかりますが、これでも一部であることを考えると、いったい誰が全てを把握しているのかわからなくなってきますね。

ですが、比較的有名なものが多いため、意外とどうにかなりそうです。

1つづつ見ていきましょう。


対応関係

クラス図を表すPlantUMLの要素をPerlソースコードに変換していきます。

これで、機械的に変換できるように表現しています。

なお、一部そりゃそうだと思われるような部分については説明を省略しています。


クラス


クラス名

クラス名のみのクラスのクラス図

PlantUML文


クラス名のみのクラス

@startuml

class ClassName
@enduml



対応するPerlソースコード

package ClassName;

use strict;
use warnings;

sub new {
my $class = shift;
my $self = {};
bless $self, $class;
}


Perlではクラス名をパッケージ名として記述します、ややこしいですね。

new サブルーチンも一緒に生成しています。


属性

属性を2つ持つクラスのクラス図

PlantUML文


属性を2つ持つクラス

@startuml

skinparam classAttributeIconSize 0

class ClassName {
- attributeID : Integer
- attributeName : String
}
@enduml




対応するPerlソースコード

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;


パッケージの持つ要素として表現できます。

また、アクセサも自動的に生成すれば有用性はあがるかもしれません。

キャメルケースとスネークケースの変換はご自由にどうぞ。


操作

操作を2つ持つクラスのクラス図

PlantUML文


操作を2つ持つクラス

@startuml

skinparam classAttributeIconSize 0

class ClassName {
- print()
- createLine(begin : Point, end : Point) : Line
}
@enduml




対応するPerlソースコード

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;


引数も戻り値もない操作は、空のサブルーチンを生成すればいいと思います。

引数が複数存在する場合は、その数に応じて型判別などしてもいいかもしれません。

個人的に型判別は冗長かなとも思っていますが......


パッケージ

パッケージが入れ子になっているクラスのクラス図

PlantUML文


パッケージが入れ子になっているクラス

@startuml

skinparam classAttributeIconSize 0

class GodClass

package parent {
class ParentClass
package child {
class ChildClass
}
}
@enduml




対応するPerlソースコードのディレクトリ構造

lib

│ GodClass.pm

└─parent
│ ParentClass.pm

└─child
ChildClass.pm

パッケージはディレクトリとして反映できます。

パスには注意して生成するとよいでしょう。

大枠のlibディレクトリをパッケージにしてもかまいません。

記述する場合は、おそらく他のディレクトリ(scriptなど)から呼出していることを書きたいのかもしれません。


インタフェース

インタフェースのクラス図

PlantUML文


インタフェース

@startuml

skinparam classAttributeIconSize 0

interface InterfaceName {
+ draw()
}
@enduml




対応するPerlソースコード

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で必要性を感じないです......


抽象クラス

抽象クラスのクラス図

PlantUML文


抽象クラス

@startuml

skinparam classAttributeIconSize 0

abstract AbstractClassName {
+ print()
{abstract} + draw()
}
@enduml




対応する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つの手だと思います。


関係


汎化および実現

汎化および実現を記述しているクラス図

PlantUML文


汎化を持つクラスと実現を持つクラス

@startuml

skinparam classAttributeIconSize 0

class extends.SuperClass
class extends.SubClass
interface implements.SuperClass
class implements.SubClass

extends.SuperClass <|-- extends.SubClass
implements.SuperClass <|.. implements.SubClass
@enduml




対応するPerlソースコード

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における違いはありません。


関連端名

関連端名を記述しているクラス図

PlantUML文


関連端名を持つクラス

@startuml

skinparam classAttributeIconSize 0

class SuperClass
class SubClass

SuperClass --> "- subClass" SubClass
@enduml




対応する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つのどれにもあたらないが、パッケージ内で利用している場合


    • userequire で呼出しているパッケージが相当するかも

    • インタフェースの実態を持っていることを表現している場合



  • 関連


    • 下の関係2つのどれにもあたらないが、パッケージ内でインスタンスを生成している場合

    • 生存期間がサブルーチン内でとどまっている場合が相当するかも



  • 集約


    • インスタンスを他のクラスでも参照している場合

    • リファレンスを子クラスのインスタンスに代入している場合などが相当するかも



  • コンポジション


    • インスタンスを自クラス内でのみ利用している場合

    • クラス図のクラスにおける属性とほぼ同義



関係の強さについては、他言語2でも構文解析だけでは表現できないらしいです。


終わりに

クラス図の読み方、Perlでの書き方はなんとなく理解できたでしょうか。

図を見れば全体像を俯瞰できるため、有用性は今でもあると思いますが、いかんせん誰も使っていないため、せっかくの言語非依存の図がもったいないです。

もし一連のコードを読んだ後に再度図を見れば、各要素と関係がより理解できると思います。

キャンバスのクラス図

皆さんもこの機会に、少しでも書いてみてはいかがでしょうか!

次回は