MPがありません。

$liiu->mp == 0

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

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 のコードを見てみたり

UNIVERSAL::DOESについてのメモ

Perlのオブジェクトに $object->DOES('SomeRole') みたいな、ロールを消費したかどうかを判定できる(?)メソッドがあるのですが、それについていろいろ知見を得たのでまとめます。

  • Perl自体にロールを消費したかどうかを判定する機能はない
    • 特にDOESがオーバーライドされていない場合はUNIVERSAL::isaと等価 *1
    • クラスビルダー(Moose::Object)などで各々オーバーライドしてロールを消費したかの判定をするイメージ
    • 使うのならクラスビルダーごとにきちんと挙動を把握しておいたほうが良さそう
  • そもそもあまり使われていない
    • UNIVERSAL::canで判定したほうが確実 (変なDOESの実装だときちんと判定してくれない場合もありうるので・・・)
    • 大半のrole判定がほしい実装はMooseを使うのがベストプラクティスになってしまってそういう場面でのDOESの出番が無かった

これらに関してはid:karupanerura氏にいろいろ教えていただきました。
ありがとうございました。

YAPC::Okinawaにいってきました

けっこう遅くなりましたが、3/3〜3/5の間YAPC::Okinawa 2018 ONNASONに参加するため沖縄に行っていたのでその感想を書きます。
前後に予定が入っていたり、沖縄に行くのはなかなかしんどいのであまり参加に乗り気でなかったのですが、学生は宿泊代と交通費を負担していただけるということで参加することにしました。
学生交通費支援をしてくださった企業の皆様、ありがとうございました!

前夜祭

本祭の日は朝早く出発しなければなかったので前日から沖縄入りしついでに前夜祭に参加してきました。
バーでいろんな方とゆるーく食事をとりながらLTを聞くという感じでした。

本祭

見たトーク

萬國之津梁

charsbarさんによるトークで、氏が参加したPerl関係のイベントやそこでどんなことをしたかについて話されていました。

GraphQL をプロダクション導入した結果

GraphQLがどういうものなのか少し気になっていたので聞くことに。
とりあえずリクエストのパラメーターがちゃんと定義されていて型安全に扱える点がよさそうな感じでした。
GraphQLを使ったアプリの実装や運用についてかなり詳しく話されていたので、参考にして試してみようかと思います。

そろそろPerlでのHTTP/2について触れたい

HTTP2ってHTTP1.1と違って何ができるのか、どんなことが変わるのかといったことやPerlでHTTP2を利用するためのライブラリなどが紹介されていました。
(前半の内容は前にやっていて僕が見ていたかったトークの内容とかぶっていたそうですが、僕は前のトークを見ていなかったのでありがたかったです・・・)
HTTP2でコンテンツを配信するサーバーの実装は並行処理がふえたりと今までとかなり雰囲気が違ってきてきそう、
とても丁寧に解説されていたので、これを気にHTTP2を触ってみようかと思います。
しかしサーバープッシュはHTTP2も含めていろいろな方法が使えるようになっているようですが、どれを使うのがいいんでしょうかね。

High (Availability|Performance) WebSocket for Perl Real-Time Application

Webアプリのサーバープッシュの部分だけ別のサーバーでやるという発想で作られたWebSocketとHTTPの相互変換プロキシサーバkuiperbeltについてのトークでした。
これを使えばLLでもWebSocketを使ったアプリをだいぶいい感じにかけるんではないでしょうか
新しい概念から作っていてもうなんかすげえって感じでした。
ベストトーク賞もこれに投票しました。

2018年春のPerl

Perl5, Perl6の最新情報についてのトークでした。
5, 6月ごろにでるであろうPerl5.28での変更点とかPerl6がどんなふうになっているかは興味があったので見れてよかったです。
Perl5.28での変更点で気になったのはハッシュがkey/valueのスライスでdeleteできるようになることと正規表現文法が拡張されることでした。
他にもスマートマッチやgiven-when文などを実用的にするための変更が入る予定だったそうですが、一部のモジュールが壊れたので没になったそうです。。
ずっとexperimentalなままで早く外れないかなーと思っていただけに残念でした。
やはり後方互換性を維持しながら言語の開発を進めることって大変なんですね。
Perl6はぼちぼちって感じでした。

Inlineモジュールの世界

Inline系モジュールがどのように動いているのかがわかり面白かったです。
メリットやデメリットもまとめられていて、導入時の判断基準にできそうでした。
やはりランタイムエラーが起きた時にPerl側で捕捉できないというのがつらそうで、組み込む言語側でなんとかするしかないのかなあと思いました。

Perlコーディングテクニック2018

最近のPerlの便利な機能やモジュールの紹介をされているトークでした。
個人的に気になったのはModule::Functions, Importer, Module::Findあたりでしょうか。便利そうなのでどんどん使っていきたいと思います。
最近のPerl関連の情報ってあまり多くないのでありがたかったですね。

Perl6のエコシステム

最近のPerl6のエコシステムについてのお話でした。
最初はgithubで配布していましたが、最近はCPANでやるようになっているようです。
また、インストーラーもpandaからzefになっていて、いろいろ変わっているなーと感じました。
mi6というオーサリングツールもあって、簡単にモジュールを挙げられるようになっているそうです。
zefの実装がなかなかいい感じだということで(workaroudも多いそうですがw)参考にしていきたいと思います。

懇親会

懇親会に参加するのは初めてで、いろんな方とお話出来てよかったです。
Mouseの不具合(?)の話をskajiさんとお話できたのは良かったです、なかなか解決は難しそうですがp-r送ろうと思います。

その後

翌日は飛行機が出るまで少し時間があったので首里城に行っていました。
独特の雰囲気や眺めがとても素晴らしかったです。

DBIx::Schema::DSLで外部キー制約を定義するときにトリガを指定する方法

こんな感じででできます

foreign_key user_id => (
  user      => 'id',
  on_delete => 'CASCADE',
):

ドキュメントでは

foreign_key($columns :(Str|ArrayRef), $foreign_table :Str, $foreign_columns :(Str|ArrayRef) )

という風に明記されていませんが、追加の引数はそのままSQL::Translator::Schema::Table::add_constraintに渡されるので上記の方法で追加できます。
そもそもトリガはあまり使うべきではないのかもしれませんが...
他にも渡したいオプションがあればSQL::Translatorのドキュメントを読むとどんなことができるかわかります。

Rustをやってみた

この記事は OIT Advent Calendar 16日目の記事です。

予定変更してだいたい2ヵ月くらい前から卒研の合間に少しずつRustを勉強し始めているのでその感想を書こうかと思います。
まだそんなにがっつり使いこんでいないので参考程度に見てください。

Rustってどんな言語?

C並みの速度で実行することができてかつメモリ管理の安全性と並行処理に関する機能を強化したり、関数型言語由来の機能を取り入れた言語です。
メモリ安全性を保証するためにほかの言語と比べてかなり独特な仕様があります。
www.rust-lang.org

やろうと思ったきっかけ

前々から低レイヤーを触れる言語をできるようになっておきたいという思いがあって、
でもCを極めようとするのはしんどいしC++もがっつりやろうとすると闇が深そうな印象があったので最近出た低レイヤー向けの言語をやろうと考え、
独自のメモリ安全性を保証するための概念が気になったのと、現代的な機能がたくさんあったのでRustをすることにしました。

2ヵ月くらい使ってみた感想

現代的な機能や関数型由来の機能が多くてミスのすくないプログラムを書きやすい

まず変数はデフォルトでimmutableです。mutableな変数を使いたい場合はlet mutと宣言する必要があって、関数に渡すときとかもmutをつけないと値を変更できません。
慣れてないと面倒ですがわかりやすいです。
また、Maybeに相当するOption、Eitherに相当するResultというのがあって、クロージャも当然使えます。
型推論もありますね

メモリ安全性を保証するための機能はすばらしいと思った

Rustでは1つの値に対して変数は1つしか対応させられないようになっています。
そのためか、他のプログラミング言語では変数に値を代入するといいますが、Rustでは束縛するといいます。
そして変数は値に対する所有権をもち、変数がスコープから外れると所有権をもっている値のリソースを解放します。
何らかの値を束縛している変数を新しい変数に束縛しなおした場合は、新しい変数に所有権が移動して前の変数からは値を参照できなくなります。
この仕組みのおかげで、メモリの解放し忘れがなくなります。

テストが気軽に書ける

同じファイル内の関数をテストしたい場合は、テスト実行用の関数を書いて関数を宣言する前に#[test]と記述してコンパイルして
rustc --test ファイル名でテストが実行できます。

所有権になれるのがなかなかつらい

所有権という考え方自体はいいと思うのですが、配列やコレクションの要素にも所有権があって、コレクションを操作するときはそのあたりも考慮しなければいけなくてかなり難しいです。
例えばイテレータを取得するとき、普通のイテレータとコレクションの要素の参照のイテレータがあって、
普通のイテレータを取得すればもとのコレクションの要素の所有権がイテレータのほうに移ってしまってコレクションを束縛していた変数が使えなくなったりとか、
コレクションの要素の参照のイテレータを使うなら要素を使うときに参照外ししないといけなかったりとか、考えることが多いです。
さらにクロージャを使う際も所有権が絡みでいろいろ覚えることがあります。

それと話はそれますが配列の値を参照するときのインデックスもunsginedでないとだめで、あまり考えずに整数型を使ってプログラミングしていると
あとあとキャストしないといけなくて面倒だったりします。

標準入力から値を受け取るのがつらい

競プロの問題解くときとか結構つらいです。
以下C++との比較です。

int num1, num2;
cin >> num1 >> num2;
let mut buffuer = String::new();
io::stdin().read_line(&mut buffer).unuwrap();
let trimed = buffer.trim().to_string();
let nums: Vec<i32> = trimed.split(" ").map(|x| x.parse().unwrap()).collect();
let (num1, num2) = (nums[0], nums[1]);

外部ライブラリを使えばだいぶましになるそうです。

まとめ

Rustはいい言語だとは思うのですが、慣れるまで動くものを作るのにだいぶ時間がかかってしまいそうだと感じています。
慣れても所有権を意識しながらプログラムを書かないといけないので適当に書いているとコンパイルが通るまですごい時間がかかりそう。
そもそも今まで自由度の高い言語をよくさわっていた人間がRustみたいな制約が厳しい言語を急にやろうとするのは厳しいのかなという気もしますが、せっかくなので頑張って使えるようになりたいと思います

Plack::Middleware::Session まとめ

Plack::Middleware::Session と関連することについてのまとめ、ほぼ個人的なメモ
簡単にまとめるつもりだったけど詳しく書くとかなり長くなりそうな気がしてきた...

使い方

  use Plack::Builder;
  builder {
      enable Session => (
          state => Plack::Session::State::*->new,
          store => Plack::Session::Store::*->new,
      );
      $app;
  };

Middlewareを使うときにPlack::Session::Stateを継承したクラスのインスタンスPlack::Session::Storeを継承したクラスのインスタンスを渡す。
何も渡さない場合のデフォルトの引数は以下のようになる

state => Plack::Session::State::Cookie->new(session_key => 'plack_session'),
store => Plack::Session::Store::File->new(
    dir          => $ENV{TMPDIR} || '/tmp',
    serializer   => sub { Storable::lock_nstore( @_ ) },
    deserializer => sub { Storable::lock_retrieve( @_ ) },
),

stateの方はPlack::Session::State::Cookieが使われていて、バックエンドは普通のクッキーだけど、クッキーの有効期限だとかが指定されていないので、
試しに動かしているみたいな時以外はPlack::Session::State::Cookieのドキュメントをちゃんとみて指定したほうがいいと思う。

storeの方はPlack::Session::Store::Fileが使われていて、バックエンドにはStorableが使われている。セッションの保管場所はちゃんと指定しておくべき
Plack::Session::Storeのバックエンドは他に標準でDBI, Cacheのがあって、好みや用途で使いわけられるし、CPANにはRedisバックエンドのモジュールがあったり、なんなら自分でPlack::Session::Storeを継承した独自のモジュールを作って使うこともできる。

Mojoliciousで使う

有名だけど、Mojoliciousのセッション機能はセキュリティ的に貧弱というかそもそもセッションといえないような感じなので、
Mojoliciousで開発しているときでもPlack::Middleware::Sessionを使いたい場面が多い。
鉄板なのはMojoliciousアプリをPSGIアプリとして起動させてPlack::Middleware::Sessionを使う方法だけど、
これだとPSGIサーバーを使わなければならなくなるので、まだどのサーバーを使うか決めていない場合や開発時にMojolicious組み込みのサーバーを使って開発を進めていきたいときなんかは、
Mojolicious::Plugin::PlackMiddlewareというプラグインがあるのでそれを使ってPlack::Middleware::Sessionを使うといい。
(もしかしたらすべてのPlack::Middlewareは動かないかもしれないが少なくともSessionは使えた)

これを使ったログイン機能のMojolicousでの実装例

ORMにTengかAnikiを使っているとする。
本格的なコードでないのであしからず...(あとでちゃんと動くのをあげる)

sub auth {
    my $self = shift;
    $self->plack_session->get('user_id') ? 1 : 0;
}

sub login {
    my $self = shift;
    my $user_id = $self->param('id');
    my $user = $self->db->select('users', {id => $user_id});
    if ( $user->password eq $self->param('password') ) {
        $self->plack_session->set(change_id => 1);
        $self->plack_session->set(user_id => $user->id);
        $self->render(text => 'success');
    } else {
        $self->render(text => 'fail');
    }
}

sub logout {
    my $self = shift;
    $self->plack_session->expire;
}

Plack::Sessionを使うモジュールのテスト

Plack::Sessionのインスタンスを生成するときは要はキーpsgix.sessionにPlack::Session::Stateのインスタンスが格納されていて、キーpsgix.session.optionsにPlack::Session::Storeのインスタンスが格納されているハッシュリファレンスを渡せばよいので、
次のような感じでPlack::Sessionのインスタンスを作ってテストすればいい。

Plack::Session->new({
      'psgix.session'         => Plack::Session::State::Cookie->new(session_key => 'myapp_sid'),
      'psgix.session.options' => Plack::Session::Store::File->new(dir => './etc/sessions'),
})

まだわかってなさそうなところがあるので、間違ってたりしているところがあれば指摘していただけると嬉しいです。