MPがありません。

$liiu->mp == 0

MySQLの INFORMATION_SCHEMA.COLUMNS CHARACTER_MAXIMUM_LENGTH で得られる値についての調査

概要

諸事情によりMySQLのテーブルの文字列型カラムに入る最大値を取得したい欲求が発生し、 INFORMATION_SCHEMA データベースの COLUMNS テーブルの CHARACTER_MAXIMUM_LENGTH*1 を利用することになったので、
CHARACTER_MAXIMUM_LENGTH を参照した時、それぞれのデータ型がどのような値を返すのかのメモを残します。

まとめ

  • 文字列型の場合にのみ、そのカラムに入る文字列の長さの最大値が返ってきます。
  • サイズが指定できる文字列型(VARCHAR, VARBINARYなど)の場合、指定されたサイズが返ってきます。
  • サイズが指定できないような文字列型(TEXTなど)の場合、その型に格納できる文字列の長さの最大値が返ってきます。
  • ENUM型、SET型の場合は少し特殊です
    • ENUM型の場合、要素の中で最も長い文字列の長さを返します
    • SET型の場合、sum(各要素の文字列の長さ + 1) - 1 と言った風になります(例えば、 SUM('A', 'BB', 'CCC') となっているカラムがあれば、 8が返ってきます)
  • 文字列型以外のカラムの型の情報を見ようとすると NULL が返されます

調査内容について

  • 環境 : MySQL5.6, MySQL8.0
  • 調査のために作ったテーブル
CREATE TABLE sandbox (
  `char`          CHAR(64),
  `char_big`      CHAR(255),
  `varchar`       VARCHAR(16),
  `varchar_big`   VARCHAR(255),
  `tinytext`      TINYTEXT,
  `text`          TEXT,
  `mediumtext`    MEDIUMTEXT,
  `longtext`      LONGTEXT,
  `binary`        BINARY(32),
  `binary_big`    BINARY(255),
  `varbinary`     VARBINARY(128),
  `varbinary_big` VARBINARY(255),
  `tinyblob`      TINYBLOB,
  `blob`          BLOB,
  `mediumblob`    MEDIUMBLOB,
  `longblob`      LONGBLOB,
  `enum` ENUM(
    'hoge',
    'fugafuga',
    'piyopiyopiyo'
  ),
  `set` SET(
    'foo',
    'barbar',
    'bazbazbaz'
  ),
  `set2` SET(
    'wktk',
    'hshs',
    'blahblahblah',
    'orz'
  ),
  `date`      DATE,
  `datetime`  DATETIME,
  `timestamp` TIMESTAMP,
  `time`      TIME,
  `year`      YEAR,
  `tinyint`   TINYINT,
  `smallint`  SMALLINT,
  `mediumint` MEDIUMINT,
  `int`       INT,
  `bigint`    BIGINT,
  `decimal` DECIMAL,
  `float`   FLOAT,
  `double`  DOUBLE,
  `bit` BIT,
  `geometory`           GEOMETRY,
  `point`               POINT,
  `line_string`         LINESTRING,
  `polygon`             POLYGON,
  `geometorycollection` GEOMETRYCOLLECTION,
  `multipoint`          MULTIPOINT,
  `multilinestring`     MULTILINESTRING,
  `multipolygon`        MULTIPOLYGON
);

実行結果

SELECT COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'sandbox'; 
+---------------------+--------------------------+
| COLUMN_NAME         | CHARACTER_MAXIMUM_LENGTH |
+---------------------+--------------------------+
| char                |                       64 |
| char_big            |                      255 |
| varchar             |                       16 |
| varchar_big         |                      255 |
| tinytext            |                      255 |
| text                |                    65535 |
| mediumtext          |                 16777215 |
| longtext            |               4294967295 |
| binary              |                       32 |
| binary_big          |                      255 |
| varbinary           |                      128 |
| varbinary_big       |                      255 |
| tinyblob            |                      255 |
| blob                |                    65535 |
| mediumblob          |                 16777215 |
| longblob            |               4294967295 |
| enum                |                       12 |
| set                 |                       20 |
| set2                |                       26 |
| date                |                     NULL |
| datetime            |                     NULL |
| timestamp           |                     NULL |
| time                |                     NULL |
| year                |                     NULL |
| tinyint             |                     NULL |
| smallint            |                     NULL |
| mediumint           |                     NULL |
| int                 |                     NULL |
| bigint              |                     NULL |
| decimal             |                     NULL |
| float               |                     NULL |
| double              |                     NULL |
| bit                 |                     NULL |
| geometory           |                     NULL |
| point               |                     NULL |
| line_string         |                     NULL |
| polygon             |                     NULL |
| geometorycollection |                     NULL |
| multipoint          |                     NULL |
| multilinestring     |                     NULL |
| multipolygon        |                     NULL |
+---------------------+--------------------------+

Types::Standard で利用できる、知っておくと便利な型

概要

Perlで型チェックを行うモジュールに Type::Tiny というものがあります。
Type::Tiny には Types::Standard というライブラリがついてきて、いろいろな型が利用可能になるわけですが、
これはその Types::Standard で利用可能な、個人的に知っていると便利だったりわかりやすいコードがかけていいなーと思う型を紹介する記事です。

Enum[@list]

列挙型です。
引数として渡した文字列のリストのうち、いずれかにマッチする値だけを許容する型です。

use Test2::V0;
use Types::Standard qw( Enum );

my $constraint = Enum[qw( green blue red )];
ok $constraint->check('green');
ok !$constraint->check('hoge');

(これはほとんどの人が知ってそう)

Maybe[T]

未定義値あるいは型パラメータ(に相当するもの)として渡した型制約を満たす値を受け付ける型です。

use Test2::V0;
use Types::Standard qw( Maybe Int );

my $constraint = Maybe[Int]; # 未定義値か整数を受け付ける型制約
ok $constraint->check(1);
ok $constraint->check(undef);
ok !$constraint->check([]);

これを使うと何が嬉しいかというと、この型制約でチェックする値はnullableであるということが明示的になることです。

例えば、引数としてユーザーのIDを渡し、DBからユーザーの名前を取得するようなメソッドを書くとします。
DBには指定されたユーザーIDに対応するユーザー名がない場合があるわけですが、 Function::Return と Maybe型 で返り値のチェックをさせると、このように未定義値が文字列が返ってくるメソッドであるということが明示的になります。

use Types::Standard qw( Maybe Int );
use Function::Return;

sub get_user_name :Return(Maybe[Str]) {
  my ($self, $id) = @_;
  return $self->db->select(user => +{ id => $id });
}

Dict[ $key => $constraint, ... ]

列挙された全てのkeyと対応するvalueの型制約を満たすハッシュリファレンスのみを受け付ける型を作ることができるものです。
ネストさせることも可能です。

例えば、 idというキーがInt型の値を持ち、nameというキーがStr型の値を持つようなハッシュリファレンスのみを受け付ける型は、以下のようにして作ることができます。

use Test2::V0;
use Types::Standard qw( Dict Int Str );

my $constraint = Dict[
  id   => Int,
  name => Str,
];
ok $constraint->check(+{
  id   => 0,
  name => 'anon',
});
ok !$constraint->check(+{});
ok !$constraint->check(+{ status => 'success' });

ハッシュリファレンスで作られているデータ構造を受け渡ししているようなサブルーチンとかで使うと、どんなデータを受け渡しているのかが明示的になって読みやすいコードになります。

TypeScriptのオブジェクト型と似たようなものだと思います。

Tuple[...]

タプルです。
ちゃんと説明すると、列挙された全ての型制約を満たすような要素をもつ配列リファレンスのみを受け付ける型を作ることができるものです。
ネストさせることも可能です。

例えば、0, 1番目が数値で、2番目が文字列である配列リファレンスのみを受け付ける型は、以下のようにして作ることができます。

use Test2::V0;
use Types::Standard qw( Tuple Num Str );

my $constraint = Tuple[Num, Num, Str];
ok $constraint->check([0.4, 10, 'hoge']);
ok !$constraint->check([]);
ok !$constraint->check(['foo', 'bar', []]);

順序づけられた組であることを明示的にコードに書きたい場合や、手っ取り早くデータ構造を作りたい時などに便利なのではないでしょうか。

Optional[T]

Dict, Tuple と一緒に使う型で、省略可能な要素を作れます。

例えば先ほどのDict型のnameをキーとする組を省略可能にしたい場合は、次のようになります。

use Test2::V0;
use Types::Standard qw( Dict Optional Num Str );

my $dict = Dict[
  id   => Num,
  name => Optional[Str],
];
ok $dict->check({
  id   => 0,
  name => 'anon',
});
ok $dict->check({ id => 0 });

Tuple型の最後の要素を省略可能にしたい場合は次のようになります。

use Test2::V0;
use Types::Standard qw( Tuple Optional Num Str );

my $tuple = Tuple[Num, Num, Optional[Str]];
ok $tuple->check([0.4, 10, 'hoge']);
ok $tuple->check([0.4, 10]);
ok !$tuple->check([]);
ok !$tuple->check(['foo', 'bar', []]);

(Optional単独でも使えますが、その場合の動作は未定義のようなので使うべきではありません。)

HasMethods[@methods]

引数として渡された全てのメソッドをもつクラスのインスタンスのみ受け付ける型です。

以下のコードでは do_something メソッドをもつインスタンスのみを受け付ける型制約を作っています。

package Foo {

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

  sub do_something {}

}

package Bar {

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

}

use Test2::V0;
use Types::Standard qw( HasMethods );

my $constraint = HasMethods['do_something'];
ok $constraint->check(Foo->new);
ok !$constraint->check(Bar->new);

duck typing を利用してメソッドを呼び出している場所があるメソッドの引数のバリデーションに利用するなどすると、どんな引数を渡せばいいのかが明示的なコードが書けてよいです。

use Type::Params qw( compile );
use Types::Standard qw( HasMethods );

sub hoge {
  state $check = compile(HasMethods['do_something']);
  my ($foo) = $check->(@_);

  $foo->do_something();
  ...
}

ちなみに、 Types::Util の duck_type 関数で同等のことができます。
どちらを使うかは好みになるでしょうか・・・。

おわりに

これらの型を活用することでよりわかりやすいコードが書けると思うのでぜひ活用してみてください。
あと、 Types::Standard には他にも知っているとためになるような型がいろいろあるので、時間があればよく目を通しておくといいかなとおもいます。
例えばtieとかoverloadをよく使っているコードがあるのなら、tieされている値やoverloadされている値しか受け付けない型みたいなのも利用できます。

Perlにおける動的なモジュールロードのメリットとデメリットについて

概要

  • Perlにおける動的なモジュールロードのメリットとデメリットについて、そしてどういう場合に動的なモジュールロードを行うといいか、僕が考えていることを記します
  • 例のコードや具体例の説明などはPerlの文脈で行っていますが、動的なモジュールロードが可能な言語ならどの言語でも似たような利点と欠点が発生することになると思います

ご指摘などあればコメント欄で教えていただけると幸いです。

動的なモジュールロードを行うことによるメリット

アプリケーションの起動にかかる時間を短縮できる

モジュールをロードするタイミングをコードを実行する直前にまで遅らせることができるので、アプリケーションの起動にかかる時間を短くすることができます。
高速化を意識しているCPANモジュールなどで、このようなコードを見かけることが多い気がしています。

例えば Class::Accessor::Lite ではこのようなことをしています。
https://metacpan.org/release/Class-Accessor-Lite/source/lib/Class/Accessor/Lite.pm#L7

コード量を少なくできる

例えば、下記のコードのようにたくさんのモジュールを1個1個useするのは結構面倒だったりしますが、動的にモジュールをロードする場合はfor文で一括でロードできちゃったりします。

use MyApp::Foo;
use MyApp::Bar;
use MyApp::Baz;
use Module::Load qw( load ); # コアモジュールのモジュールローダーです
load "MyApp::$_" for qw( Foo Bar Baz );

また、Catalystのモジュールローダーのように*1、useなどモジュールをロードする関数を呼び出さずに、モジュールローダーを介して直接他のモジュールのメソッドを呼び出させることもできます。
この場合、長いモジュール名のモジュールをある程度短い名前で呼び出せるというメリットもあるかなと思います。

use MyApp::Model::Foo;
sub hoge {
    my $self = shift;
    MyApp::Model::Foo->do_something();
}
use Module::Load qw( load );

sub hoge {
    my $self = shift;
    # モジュールローダー(modelメソッド)は MyApp::Model::Foo をまだロードしていない場合ロードして、
    # 'MyApp::Model::Foo' という文字列を返す。 
    # -> 演算子は左側の文字列をパッケージ名としてメソッドを呼びだそうとするので、 
    # MyApp::Model::Foo->do_something() と同じ処理を行う
    $self->model('Foo')->do_something();
}

sub model {
    my ($self, $module_name) = @_;
    my $package_name = 'MyApp::Model::' . $module_name;
    load $package_name;
    die $@ if $@;
    return $package_name;
}

モジュールをロードする順番を動的に決めることができる

モジュールロードを実行時に行うと、モジュールをロードする順番を動的に決めることができたり、状況に応じて依存するモジュールを変えれたりします。

例えば(そもそもこのような設計はよくないのですが)、以下のコードのように相互に依存するモジュールがある場合、静的に互いのモジュールをロードしていると、コンパイル時に互いを再帰的にロードしようとしてしまうためか(?)、それぞれのファイルのコンパイル時に Subroutine redefined の警告が出ます。

Foo.pm
package Foo;

use strict;
use warnings;
use feature 'say';
use Bar;    

sub hoge {
    say 'hoge';
}
    
sub fuga {
    Bar::hogera();
    say 'fuga';
}

1;
Bar.pm
package Bar;

use strict;
use warnings;
use feature 'say';
use Foo;

sub piyo {
    Foo::hoge();
    say 'piyo';
}

sub hogera {
    say 'hogera';
}

1;

しかし、以下のように依存しているモジュールが必要になる直前に動的にモジュールロードを行った場合、そのような警告は出ません。

Foo.pm
package Foo;

use strict;
use warnings;
use feature 'say';
    
sub hoge {
    say 'hoge';
}
    
sub fuga {
    require Bar;
    Bar::hogera();
    say 'fuga';
}

1;
Bar.pm
package Bar;

use strict;
use warnings;
use feature 'say';

sub piyo {
    require Foo;
    Foo::hoge();
    say 'piyo';
}

sub hogera {
    say 'hogera';
}

1;

動的なモジュールロードを行うことによるデメリット

依存しているモジュールで何かエラーが起きていても、実際にロードされるまでわからない

依存しているモジュールで何かエラーが起きていても、実際にロードされるまでわからないため、アプリケーションの起動後にSyntax errorなどコンパイル時にわかるようなエラーで一部の機能が使えない・・・といった事態が発生してしまう恐れがあります。
全てのモジュールのテストが書かれていればこのようなデメリットを無視できますが、通常必ず全てのモジュールにテストが用意されていることに期待をすべきではないでしょう。

静的解析を利用したツールの恩恵にあずかれなくなる。また、読みにくいコードになる恐れがある

これはモジュールロード時にモジュール名を動的に組み立てている場合に発生する問題で、動的なモジュールロードによる問題というよりは動的にトークンを組み立てる問題と言えそうですが、動的にモジュールロードを行うときに一緒によく起きがちな問題なので取り上げます。

例えば先ほど上げたCatalyst風のモジュールローダーでモジュールをロードしている場合、静的解析ではどのようなモジュールをロードしているかはわからないため、コードジャンプやメソッド名を補完するようなツールの中には正常に動作しないものがけっこうでてきます。
そのようなツールに特別な対応させることで動作させることは可能かもしれませんが、その分の対応コストがかなりかかるでしょう。

sub hoge {
    my $self = shift;
    # modelメソッドがどういうモジュール名を返すのかは,
    # 実際にコードを実行してみないとわからない!
    $self->model('Foo')->do_something();
}

また、動的にモジュールロードされていることを知らない人がコードを読んだ場合、依存モジュールがどこで使われているのかがわかりにくくなってしまう恐れがあります。

モジュール間の依存関係がわかりにくくなってしまって、変な設計をしてしまう原因になる

  • モジュール間で相互依存が発生している場合も警告がでないので気づけなくなってしまう
  • モジュール間の依存関係を機械的に解析することが難しくなる
  • 個人的な感覚ですが、コードの途中でモジュールがロードされているより、モジュールのある一箇所で依存しているモジュールがまとめてuseされている方が依存関係がわかりやすいと思います

他にも、CHECK, INITコードブロックに書かれたコードが実行されないなどといった細かい問題などもあります。

所感

僕としては、特に長期的にメンテナンスしていく必要のあるコードや他の人が読むようなコードおいてはデメリットの方が目立ってしまうため、プロダクトではなるべく動的なモジュールロードは行わない方がよい開発体験が得られるのではないかと思っています。
動的なモジュールロードを行ってもよい場面は、個人的に使う本当に使い捨てのスクリプトを書くような場面や、CPANモジュールを作るときにアプリケーションの起動が遅くならないようにしたい場合や、Plackのようなエンジンを作るなどといった場合に限られるのではないでしょうか。

VimでPerlを書くときにctagsを導入して使ってみた

コードジャンプを利用したい場合にctagsを導入するという方法がありますが、最近VimPerlを書くときに導入してみたところ、非常に良い感じだったので、インストール方法と簡単な使い方を記しておきます。

インストールするものについて

ctagsの実装(?)にはいろいろ種類があるようですが、今も活発に開発が続いている universal-ctags をインストールするのが良さそうです。

ctags.io

1. インストールする

僕の個人機のOSはUbuntu18.04なので、今回はgithub上にあるレポジトリからcloneしてきて手動でビルドしました。

$ git clone https://github.com/universal-ctags/ctags
$ ./autogen.sh
$ ./configure
$ make
$ make install

詳しいことが知りたい場合は Readme や docs/autotools.rst を見てください。 Max OSだとbrewからインストールもできるようです。

2.tagsファイルを生成する

エディタでコードジャンプできるようにするには、まず対象のコードに対してどのようなパターンの時にどのファイルのどの場所にジャンプするのか、みたいなことを定義したtagsファイルを生成する必要があります。
tagsファイルはctagsコマンドで生成できます。
今回はPerlのコードのみを対象にしているので、プロジェクトのルートディレクトリで以下のようなコマンドを実行しtagsファイルを生成します。

$ ctags -R --languages=Perl

実行すると、 tags というファイルが生成されているのが確認できます。
tagsファイルは直接プロジェクトに関係ないので、ついでに .gitignore に追加しておくといいと思います。

注意点として、tagsファイルはコードに変更がある度に生成し直さないと正しくコードジャンプできなくなるので、コードに変更がある度に再生成する必要があります。
tagsファイルを再生成する方法はいろいろありますが、僕は git で commit した時にフック処理をさせるようにして、commitする度にtagsファイルの再生成を行うようにさせました。

Cartonを利用して開発している場合、 local/ 以下に依存モジュールが入っていると思うので、依存モジュールの定義元にもコードジャンプできてより開発が捗ります。

3. Vimキーバインドについて

  • 定義元へのジャンプ : <C - ]>
  • 複数の候補がある場合の定義元へのジャンプ : <gC - ]>
    • 継承してoverrideしているようなモジュールが複数ある場合などに使います
  • コードジャンプ前の位置に戻る : <C - t>

他にもいろいろありますが、とりあえずこれらのキーバインドを覚えとくといいと思います

所感

Perlでコードを書いてると、どんな引数を渡せばよくわからなくて関数の定義元に飛びたい!、というような状況がよくあるのでとても便利です。
しかし、Mooseのシンタクスシュガーやkeywordプラグインを使っているようなモジュールには対応していないので、それらを使っている場合は別の方法でtagsファイルを生成する必要がありそうです。(CPANにそういうライブラリがあがってるっぽいのでそのうち試したい。)

Function::Parameters + Mouse::Util::TypeConstraints = パフォーマンス激強

概要

Perlのサブルーチンの引数バリデーションをするモジュールはいろいろありますが、 Function::Parameters が他の引数のバリデーションを行うモジュールと比べて高速にバリデーションできるという話を聞いて、であれば Mouse::Util::TypeConstraints と組み合わせて使えばとても良いパフォーマンスがでるのではと思い、実際にベンチマークをとってみました。

比較対象は以下のとおりです。
最近良く使われている印象があるモジュールと、何もチェックを行わない場合とで比較してみました。
Smart::ArgsData::Validator は、引数にハッシュを渡すほうがリストを渡すよりもパフォーマンスが良い(あるいはほとんど変わらない)ようなので、ハッシュを渡しています。

  • 何もチェックしない場合
  • Data::Validator 1.07
  • Smart::Args 0.14
  • Function::Parameters 2.001003 + Type::Tiny 1.004002
  • Function::Parameters 2.001003 + Mouse::Util::TypeConstraints v2.5.4

ベンチマークスクリプト

use v5.28;
use warnings;
use utf8;
use DDP;
use Benchmark qw( timethese cmpthese );
use Smart::Args qw( args args_pos );
use Data::Validator;

sub nomal {
  my ($id, $name, $pass) = @_;
}

sub smart_args {
  args my $id => 'Str', my $name => 'Str', my $pass => 'Str';
}

sub data_validator {
  state $rule = Data::Validator->new(
    id => 'Str',
    name => 'Str',
    pass => 'Str',
  );
  my $args = $rule->validate(@_);
  my ($id, $name, $pass) = @$args{qw/ id name pass /};
}

{
  use Function::Parameters;
  use constant Str => Mouse::Util::TypeConstraints::find_or_create_isa_type_constraint('Str');
  fun function_parameters_mouse(Str $id, Str $name, Str $pass) {
  }
  use Types::Standard qw( Str );
  fun function_parameters_types_standard(Str $id, Str $name, Str $pass) {
  }
}

my $args = {
  id   => 'mp0liiu',
  name => 'Maybe_mp0liiu',
  pass => 'hogehoge!!!',
};
my @args = @$args{qw/ id name pass /};

cmpthese(
  timethese(
    100_0000,
    {
      nomal                              => sub { nomal(@args) },
      smart_args                         => sub { smart_args($args) },
      data_validator                     => sub { data_validator($args) },
      function_parameters_mouse          => sub { function_parameters_mouse(@args) },
      function_parameters_types_standard => sub { function_parameters_types_standard(@args) },
    }
  )
);

結果

                                        Rate smart_args data_validator function_parameters_types_standard function_parameters_mouse nomal
smart_args                           23068/s         --           -77%                               -93%                      -97%  -99%
data_validator                       99404/s       331%             --                               -68%                      -86%  -96%
function_parameters_types_standard  313480/s      1259%           215%                                 --                      -56%  -87%
function_parameters_mouse           719424/s      3019%           624%                               129%                        --  -69%
nomal                              2325581/s      9981%          2240%                               642%                      223%    --

感想

Function::ParametersMouse::Util::TypeConstraints を併用した場合の速度がとても速くて驚きました。
誤差などあると思いますが、何もチェックしない関数の1/3程度の時間でちゃんと引数の個数や型をチェックをしてくれるってすごくないですか???
Function::Parameters の書き方は微妙に気に入らないところもあるのですが、このパフォーマンスで引数の個数と型をチェックしてくれるのは非常に魅力的なので個人の開発では利用していきたいと思います。
ただ、型チェックオブジェクトを Types::Standard みたいにクールにかけないので、 Mouse::Util::TypeConstraints を使う場合も同じようにクールに書けると良さそうかなあ、、、

ドメイン駆動設計の実践 勉強会メモ

8/22に開催された【TECH x GAME COLLEGE #1】ドメイン駆動設計の実践 - connpassに参加した時のメモです。
とても勉強になる面白い話を聞かせていただきました。

アジェンダ

  • DDDの基礎知識
  • ゲーム開発への活用の可能性
  • ゲーム開発に取り入れる時の障害

DDDの基礎知識

ソフトウェア設計の一般原則

  • 関心の分離
  • モジュール構造

全体を複数の関心事に分離し、それぞれの関心事を独立したモジュールとして開発する。
そしてそれらを組み合わせて全体を構築する。

DDDの特徴

DDDではどのように関心事を分離しモジュール構造を構築するか?

  • ドメインロジックに焦点を合わせる
  • OOPでモジュール化する
  • インクリメンタルに設計する
    • 上記のことを前提すると、アップフロントに綺麗に設計はできない
    • ドメインロジックをコードに落としてモジュール化の実験をしながら徐々に完成させていく

従来のソフトウェアとの関心事の違い

  • DDD
    • ビジネスロジックに焦点を合わせる
    • 主な関心事はドメインの状況を表す値の種類, 値を使った計算、判定ロジック, 計算結果、判定結果の表現方法
  • 従来の設計
    • 入出力
    • 主な関心事はUI, DB, 通信など

ゲーム分野におけるドメインロジックの構成要素

  • ゲームを構成する概念(世界観、キャラクター設定、ストーリーに登場する言葉, etc)
  • ゲームの状況を表現する値(金, HP, MP, Exp, SP, アイテム種類, 武器種類, 防具種類, etc)
  • 戦闘開始状況の判定ルール, ダメージなどの計算ルール, 勝敗の判定ルール

これらを1つ1つの機能として整理してモジュール化する。

ドメインロジック以外の関心事

ビジュアルデザイン, SE, インタラクションデザインなど
これらはドメインロジックに従属する要素である。
DDDで設計されたソフトウェアはドメインロジックのまわりにこれらがぶら下がる感じで出来上がる。

ドメインロジックに焦点を合わせる理由

  1. ソフトウェアの複雑さの原因はドメインロジック(計算、判定ロジック)にある
  2. ドメインロジックと入出力が混在しているとドメインロジックの全体像や構造が見えてこない
  3. 入力の関心事を分離してドメインロジックだけを対象にするとドメインロジックの輪郭や構造がはっきりしてくる
  4. ドメインロジックの輪郭や構造がみえてくると計算、判定ロジックがはっきりしてくる
  5. ロジックの整理がしやすくなると入出力もシンプルになる
  6. この実感を感じられるようになるとDDDの恩恵がわかるようになる(入出力がシンプルになるから)
  7. 結果的にソフトウェア全体の見通しが良くなる
  8. どこに何があるかわかる
  9. コード変更による影響範囲がわかる

MVCモデルのソフトウェアのモデル部分をビジネスロジックと入出力処理に分離するイメージといえそう。

ゲーム開発への活用の可能性

ゲーム開発でのメリット

  • 計算ロジック、判定ロジックの記述が整理できる
    • 同一ロジックの重複記述の防止
    • ロジックの組み合わせ方の見通しをよくできる
  • 世界観、キャラクター設定、シナリオ構成と計算ロジック、判定ロジックの記述を直接的に関係づける
    • 全体が整合する
      • 逆に言いうと整合が取れていないとビジネスロジックを考えなおしてコンセプト(=世界観)などに合わせていくべき
    • 変更の対象箇所、影響範囲がわかりやすい
  • 入出力処理の記述がシンプル
    • 計算や判定の記述をドメイン層に外だし出来る
    • 一番実感しやすい

OOPでモジュール化するとは?

  • ソフトウェアのモジュール
    • コードを扱いやすい塊にグループ化して、グループごとに別ファイルで記述する
    • モジュール間の依存関係を少なくする
  • モジュール化のアプローチ
    • 入出力の単位ごと
    • 計算する値の種類ごと

入出力単位でモジュール化する方法

  • 入力を起点に、出力するための処理手順の単位でファイルを分ける
  • 機能分割し機能ごとに入力と出力を定義する
  • 出力に必要な一連の処理を時系列に記述する(手続き)
    • (Stringクラス の length と substr するのに時間は関係ないよね)

計算する値の種類ごとにモジュール化する方法

  • 計算や判定に登場する値の種類を見つける
    • レベル, HP, MP ...
    • 武器種類 ...
    • ダメージの判定結果、勝敗判定結果
  • 計算する値の種類ごとに必要な計算(演算)を整理する
    • 一致、不一致の判定
    • 大小判定
    • 境界値判定
    • 四則演算
    • 値のリテラル表現への変換
    • リテラル表現から値への変換

値の種類は多くて計算の種類は少ない

OOPのモジュール化の考え方とやり方を学ぶには?

  • 値オブジェクト, 区分オブジェクト, コレクションオブジェクト
  • 現場で役立つシステム設計の原則(登壇者の著書), オブジェクト指向入門とか参考になるよって言っていた

インクリメンタルに開発するとは?

  • 最初から良い設計はでいない
    • 開発を始めるときはゲームに対する知識が貧弱
    • 最初に書いたコードはあとからもっと良い書き方が見つかる
    • 最初の要求はあいまいで時間とともに具体的、詳細になる
    • 要求は時間とともに変化する
      • フィードバックの反映
      • 周りの環境の変化
  • 複雑な構造に対して最初から格闘していてもつくれない
    • 基本的な値の種類を見つける(例えば、HPクラスだけ最初に作ってみるとか)
    • 値と値を組み合わせたロジックを置く場所(第3のクラスを見つける)
  • 定番の答えはない, 開発してるゲームに合わせて探す

例えばRPGのようなゲームを作っているとどんな値の種類が見つかるか?

  • バトル型
    • 進行中の戦闘状態を表す状態を集めた型
    • ターン型オブジェクトの履歴を持つ
    • 戦闘結果を判定するロジックを持つ
  • バトルコンテキスト型
    • 戦闘開始時の状態を表現する型
    • 戦闘やターンの計算、判定に必要な基本情報を表現した値の集合
  • ターン型
    • 戦闘のターンごとの状況を表す値を集めた型
    • 前のターンの結果を計算して次のターンを返す
    • 次のターンから戦闘結果を判定する
  • ポイント型
    • HP, MP, SPなどの共通の定義
  • アイテム型
    • 食料、薬、巻物などのアイテム共通の定義

これらはあくまで例えばの話で、例えばアイテムに関しては特に全アイテム共通のアイテム型なんか持つ必要がなくて、 食料、薬、巻物別々に定義や所持枠を作るという方法もある。

ゲーム開発に取り入れる時の障害

どんな障害が起こるか

  • そうはいっても実際にソフトウェアを開発していると入出力の処理を重視しがちになる
    • UI命, 通信が〜, DBが〜
  • フレームワーク
  • 開発プロセス
    • 例えば見た目をすぐに修正して欲しいということがあったとして、 ビューモデルだけ変えるか, ビジネスモデルも変えるか・・・みたいなことが起きる

障害に対するいくつかのアプローチ

  • 少数精鋭のチームで徹底してDDDで開発してみる
    • 学習するための実験プロジェクトをやってみるなど
    • 効果は非常に高いけど難しい
  • 従来のやり方に少しずつ
    • 数値の計算ロジックを独自型にwrapして計算を行う
      • HPの単純な足し算、引き算のロジックをHP型にする、など
    • 判定結果をboolean型で表現することをやめ、独自に定義したenum型などを使って判定を行う
    • コレクションの操作を独自型にwrapして行う

実際にコードを書いてその結果を確認しながら議論し、決めていくことが一番大事!

質問やその他メモ

  • DDDでビジネスロジックを書いていたら固有名詞のクラスとかが生まれてきて気持ち悪い
    • ビジネスロジックでの計算のために使われているならいいと思いますよ的な
    • (けっこう難しいお話をされていたので多分こんな感じのことを言っていたと思うけどよく覚えていない。。)
  • 企画の人が完全に要件を定義していない。どうしよう?
    • 企画はビジネスロジックを完全に把握しているわけではなく、大体1割程度しか把握していない(よくて3割)
    • うまく聞き出せれば2, 3割くらいは聞き出せる
    • あとは開発側でよしなにしてしまおう
      • ただしコミュニケーションが不要なわけではない
      • ビジネスロジックを定義していく中で明らかに相談、共有しておいたほうがよいことがあると思えば迷わず相談、共有しておこう
  • 集約がむずかしくてよくわからない
    • そもそも集約はあまり作る必要がない
    • 複数のドメインモデルの間で変更の同期が必要なものがある場合にのみ作る
    • 入出力のときに必要だから集約を作ろうとしていない?
      • ビジネスモデルはあくまでビジネスロジックを計算する役割しか持たないので、入出力のときに必要なものはインフラ層でよしなにしましょう
  • エヴァンス本での集約のトランザクション整合性とはDBのトランザクションの話をしているわけではない
    • 複数のドメインモデルの間で変更の同期が必要なもののこと
    • 例えば商品の値段が上がれば請求書の値段もあがるみたいな(これはちょっと不自然なのでたとえとして悪いけど)

ソースコードからビルドしたPerlをplenvで管理する

独自にコンパイルされたperlや自分で少しソースコードをいじったperlもplenvで管理したいなーと思ったのでやってみました。
基本的にrbenvなどと同じようにできるようです。

手順

1. Perlソースコードを入手する

2. コンパイルする

取得してきたPerlソースコードの中に ./Configure というファイルがあります。
このファイルを実行することでMakefileが生成されます。

plenv管理下のperlは通常 $HOME/.plenv/versions/ 以下にあり、その中にあるディレクトリの一覧が plenv versions で表示されるようになっています。
なので、以下のようにplenv管理下のperlがあるディレクトリの中にインストールされるようにオプションを指定してMakefileを生成します。
(${version_name}plenv versions で表示される名前になるので適宜設定します。)

./Configure -Dprefix=$HOME/.plenv/versions/${version_name} -de -Dversiononly -Dusedevel -A'eval:scriptdir=$HOME/.plenv/versions/${version_name}/bin'

Makefileの生成が終わったらコンパイル、テスト、インストールを実行します。

make
make test
make install

3. 使う

インストールが終わったら plenv versions で先ほどインストールしたperlが表示されているはずです。
無事インストールが完了していれば plenv local などで設定して使うことができます。

参考にしたの

d.hatena.ne.jp

qiita.com

あとは Perl-Build のコードを見てみたり