GolangでDDDのサンプルコード書いてみた
DDD(ドメイン駆動開発)についての所感
まず初めにDDDについて個人的な所感を書いてみます。
ネット上で調べているとDDDを実践すべきかについては賛否両論があるのを見かけます。もちろん全ての現場で取り入れる必要はないとは思いますが、個人的な意見としてはわざわざ否定するものではないと思っています。
というのも、ドメイン駆動といってますが、ドメインという用語自体は抽象な概念で、「こうあるべきだ!」という強い制約を課しているわけではありません。つまり、「ドメインにしたがえば綺麗に開発できるぞ!」と言っているわけではなく、単純に「開発しやすいモデリングがされたもの = ドメイン」なのです。自分なりに言い換えると「ドメイン = 適切にモジュール化されたもの」と捉えています。
※オブジェクト指向は車のタイヤ・ライトとか"物"ごとにクラス分割すると良いという指針を示している部分もありましたが、ドメイン駆動はそう言った意味合いではないと思います。
つまり、DDDが言いたいことは、"綺麗に分割して開発しよう" & "DDDで述べている概念を取り入れると分割しやすいよ"ということにとどまると思います。厳密にドメインモデリングの仕方や実装方法にまで言及しているわけではありません(だからこそドメインって何ってよく言われるんじゃないかと思います)。DDDは賛否あっても、「綺麗なコード書きたい、モジュール化したい」に特に反対はないですよね。
開発を進めて知見がたまらないとドメインモデリングが難しいのは、そもそも「適切なモジュール分割」が分からないからであり、開発を進めると、このコードはよく触るとか、一緒に触ることが多いとかが見えてきて、ドメインが見えてくるんだと思います。 ドメインエキスパートと対話し、最初からモデリングを考えたりすることもあると思いますが、そのプロダクトの開発経験を積むことが最も大事なんじゃないかと考えてます。
具体的なコーディング手法とは切り離して考えるとこのように、DDDはガチガチのフレームワーク的なものではなく、心構え的なものとして考えられると思います。 もちろん具体的な実装方法も提案されており、そこまでやる必要あるのか?という部分もあるとは思いますが、そこは適宜取捨選択すれば良いともいますし。
※ただし、後でサービス分割まで考えている場合は真面目にやっておかないと行けない部分は多いかも
DDDのサンプルコード書いてみた
こちらのリポジトリにサンプルコードを置いています。ちなみにまだコーディング途中の部分もあります汗
https://github.com/tonouchi510/golang-ddd-layout
参考書籍
ちなみに、以下の書籍を参考にしており、こちらの書籍のC#で書かれたサンプルをGolangに書き直しています(&一部追記)。
ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本 | 成瀬 允宣 |本 | 通販 | Amazon
ちなみにこの本はDDDの入門書としてかなり良本だと思います。DDDについて体系的に学べて、実践方法もコードと共に書いてあります。 DDDで他人に勧める本としては間違いなくこの本を最初に勧めると思います。
GOでDDDする上でのTips
箇条書きになりますが、特筆すべきと思った点を書いておきます。適宜追記していくかもしれません。
- Golangではレシーバーや引数は、パフォーマンスの都合上ポインタにする(コピー分の節約)慣習があるが、DDDやる上では基本使い分ける
- DDD的には参照渡しと値渡しは明確に分けたい(脳死ポインタ指定はよくない)
- 綺麗なコードを優先しつつ、大きなstructだけパフォーマンス面の都合からポインタにする、という方針が良さそう
- クラスは基本privateで作成する
- 外部パッケージからオブジェクトを生成する時にNewメソッドを課すため
- => 生成ルールを必ず通ることになるため、受け取り側でvalidationがいらない
- 注意:goの場合パッケージレベルでのアクセス制限しかないため、同一パッケージ内では依然としてNew以外でも作れてしまうが、そこは紳士協定を結んでおくしかなさそう
- 注意:testも同一パッケージ内で書く必要がある(まあ大した問題にはならない はず)
- 外部パッケージからオブジェクトを生成する時にNewメソッドを課すため
- 継承ないことはそこまで気にならない
一部サンプルコード抜粋
value objectの例
type userName string func NewUserName(value string) (userName, error) { if value == "" { return "", fmt.Errorf("ValueError: userNameが空です。") } if len(value) < 3 { return "", fmt.Errorf("ValueError: ユーザ名は3文字以上です。") } name := userName(value) return name, nil }
entityの例
type user struct { id UserId name UserName } func NewUser(id UserId, name UserName) (*User, error) { user := User{ id: id, name: name, } return &user, nil }
個人開発シリーズ3:設計(v0.1)
こちらの記事の続きになります。
今回は開発を進めるにあたってもう少しイメージを固める必要があったので、アーキテクチャ図や各コンポーネントの設計をざっくり行ってみました。
アーキテクチャ図
ひとまずざっくりしたアーキテクチャ図はこんな感じのイメージで設計しました(前回のコンテキストマップのCore部分のみ)。細かい話は後半で書いてます。 各サービス間連携の部分等、より詳細の部分はまた後ほど載せていこうと思います。
APIゲートウェイ
バックエンドのマイクロサービスアプリケーションへのエントリとなるサービスになります。 ここではリクエストのルーティングやAPI合成、認証などを担当します。
前回の記事のドメインモデル図で、UserはAuthenticationコンテキストにも所属していましたが、Authentication.Userドメインの責務はここに関わる部分なので、あえて個別サービスを作るのではなくAPIゲートウェイの中でその役割を実装することにします。
また、認証周りは自前実装は避けたいところですし、Firebase Authenticationという優秀な認証基盤もあるのでこれを使って認証・認可を行います。
マイクロサービス
本来、開発当初にマイクロサービスアーキテクチャを選定することはあまり良い選択肢ではなく、最初はモノリスでスタートさせて組織の拡大とともにマイクロサービスに移行する方が適切なのが一般的です。 その方がドメインに対する知識も溜まっており、適切なサービス分割を行うことができます。
ただし、今回は勉強目的でもあるので最初からマイクロサービスを採用することにしています。知見のない状態でサービス分割を進めることになるので、ここでは明確な境界を引ける部分のみでサービス分割を行っていくことにします。 開発を進めていく中でもし粒度が大きい場合はその時に分割することにするとします(個人開発なのでそもそも分割する必要なんてないんですが、そこはまあ仮想的にです)。
サービス分割の指針
まず考えられるものとしては、コンテキストが異なるものは分割して考えられることがわかります。
さらにコンテキスト内にあるドメインモデルに境界を引いていきます。この時、一般的にはアグリゲート(集約)が明確な境界となります。アグリゲートの識別もドメイン知識がない状態では難しいのですが、アグリゲートは整合性の単位であり、以下のルールがあります。
- 外部からはアグリゲートルートだけを参照する
- アグリゲート間の参照では主キーを使わなければならない
- 1つのトランザクションで1つのアグリゲートを作成または更新する
これらを意識して開発を行うことを想定しつつ、現状でわかる範囲でサービス分割を行いました。
サービス分割
以上の指針の中で、ひとまずCoreコンテキストについて進めます。
まず少なくともUserは一つのアグリゲートととして考えることができそうなことがわかります。 また、本ツールの重要な概念であるInvestmentItem(投資案件)をアグリゲートルートとした投資案件サービスも考えます。UserはInvestmentItem(投資案件)をリストを保有していますが、idをリストとして持つことでルール2を満たせます。
また、InvestmentItemとCompanyおよびDealSourceも整合性の単位として一つではないと容易に想定できるため、分離して考えます。
「InvestmentItemとDueDiligence〇〇」および「UserとPerformance」は正直現時点で判断するには知識不足であるため、ひとまず同じアグリゲートに属すものとして考えてみようと思います。
ここまでをまとめると、以下の図の通りになります。実線はオブジェクト参照、点線は主キー参照です。
それでは大体開発に入るための指針が整ったので開発に入っていこうと思います。 次回はようやくコードを載せられると思います。
念願の湖の近くに引っ越すことになった件(浜松移住)
リモートワークが主流になり、都外の物件を見続けて半年程経ちました。。
ついに理想の立地 & 物件を見つけることができ、引越しすることになったのでそのことを書いておこうと思います。 もはや物件探しが趣味になっており、コンサル業できるんじゃないかと思っています(おまけで物件選びのコツも一応書いてます)。
引越し先としては、湖の近くでリモートワークできたら良いなと思っていて、静岡県浜松市の佐鳴湖周辺がすごい良さそうということに気づき、周辺の物件に決めました。
浜松市は元々住んでいたこともあるのですが、以下のサイトを見て特に佐鳴湖周辺がすごく良さそうだなと思いました。
佐鳴湖周辺は自然を感じられる土地ですが、ちょっと移動すれば浜松駅や浜松市西部など、結構発展した街に行くこともできます。
浜松駅は新幹線も通っているので、仕事の都合や用事ができた時に東京にいくのも簡単なところも良い点です。
自分は体を動かすことも好きなので、佐鳴湖の外周をランニングするのも楽しみに感じてます。1週約6キロほどだそうです。 結構ランニングコースが整っていて、あの"山の神" 神野大地さんもラントレしてる?のもYoutubeで見ました。
引越したらようやく広いお家になるため、今使ってる幅70cmにも満たないデスクは窓から投げ捨てて、 デスク環境整備を全力でやろうと思っているので、引越し完了したらデスク環境整備の参考になりそうな部分をブログに書こうかと思います。
おまけ:物件の選び方
ちなみに、物件探しを色々やってきた中で、個人的に気にしておいた方が良いと思ったことを書いておこうかと思います。
1. 搬入経路
意外と物件探しの時は考慮から漏れがちですが、物件が決まっていざ引越しという時に一番よく起こる問題じゃないでしょうか。 特に大きな家具を持っていて、お気に入りなのでどうしても使い続けたいというものがある場合は、最初からこの点を考慮に入れつつ物件を探した方が安全かと思います。
ベッドやソファなどは注意が必要で、せっかくお気に入り/高価なものを買ったのに入らないとすごい残念な感じになってしまいます。 ベッドであればダブルサイズは大抵の物件で入るようですが、それ以上の大きさになると搬入できないケースも結構あるそうです。
あと、仲介業者も大抵この辺は話してくれないので注意が必要です(というか悪い点は基本聞かないと教えてくれないです)。
2. 地域の補助金を確認する
引越し先が移住やリフォーム等の補助をやっていることもあるので、確認しておくのはおすすめです。 引越しは初期費用が結構ネックになってきますが、知っておくと初期費用分をかなり浮かせられる可能性があります。
最近有名なのはこちらの移住支援金・企業支援金の制度で、首都圏に5年以上住んでいる方が地方に引っ越す時に対象です(ちなみに自分はまだ該当しませんでした😭 )。 これは全国レベルでやっている制度ですが、地域によってはこれに加えて補助金が出ているところもあります。
3. 本当に完璧な物件が出ていたら時期を待たずに決めるべき
人それぞれ条件や理想があると思いますが、半年程見続けて、本当に全ての観点で理想通りにいく物件に出会えるのはかなり厳しいというのを感じてます。
かなり理想に近い物件もあったのですが、もう少し後で引っ越したいという理由で待っていたら取られてしまった、ということが2回ありました。 地方だからと油断していたのですが、良い物件の場合は、地方でも1週間とたたずに取られてしまうので、気をつけておくべきだと思います。
4. 仲介業者は完全な味方ではない
先ほども少し述べましたが、仲介業者から悪い点とかを率先して教えてくれることはまずありません。こちらから「これはどう?」とか聞く必要があります。 また、初期費用にも訳の分からない費用が入っていることもあるので、ちゃんと内容を聞いて、場合によってはNOを突きつけた方が良いケースもあると思います。
聞けば真摯に答えてくれるところもありましたが、平気で嘘ばかりつく仲介業者もいたので残念でした(インターネットでなんでも調べられる時代なので、正直すぐにわかっちゃいます)。
(引越しは人生において何度かあるイベントですし、ちゃんとした対応をしてもらえれば次もそこを利用しようとなるのでその方が長期的には良いと思うのですが...)
最後はちょっと愚痴みたいになってしまいましたが、こんなところで締めようと思います。物件探しの参考になれば幸いです。
「焼肉ジャンボ はなれ」に行ってきた
今回はご飯メモ。良い焼肉屋を見つけてしまったのでブログに書いて記録しておきます。
食べログ4.37というすごい数値がついています。
割と有名だったり、高い焼肉屋とかにも何度か行く機会/連れて行ってもらえる機会はあったんですが、4.37という数値の場所に行くのは初めてでした。 (ちなみに今回は自分が後輩達を連れてご馳走したという善行をしてます)
高級焼肉店はどのお店もお肉が柔らかくて美味しかったんですが、大抵高そうな雰囲気のお店になるんですが、ここは比較的入りやすい&カジュアルな雰囲気のお店という感じがしました。
ご飯の方ですが...
無能なので今回写真はこれしか撮ってませんでした😇
この写真にある牛ご飯や、その他ご飯もの、かつなど調理に時間のかかるものは事前予約が必要なのが注意です。
※ちなみに予約サイトも忘れたという無能ぶり
最近雑になってきた...
TensorFlowの実装プラクティス
最近忙しくてネタがないので、Qiitaに書いてたこちらの記事を移行してきました。 TensorFlowの実装プラクティス - Qiita
※いろんな媒体に分散して書いちゃってるものを徐々にこちらに一本化していこうかとおもいます
はじめに
TensorFlowを使っていると、たくさんのライブラリや様々な実装の仕方があることがわかると思います。これはtf2が出てきても依然として状況は変わってないように思えます。自由度が高い反面、公開リポジトリのコードを読む際にも、実装の仕方が様々で読み解くのが大変になってしまっていると思います。
今回は、TensorFlowで実験コードを書く際に、個人的に良いと思う実装パターンを書いていこうと思います。結論を言うと、tf.keras.Model
のエコシステムを存分に利用して実装しよう、という主張です。
※ツッコミも大歓迎です。
tf2での実装方法
ここでは主にトレーニングループの実装に焦点をおきます。
トレーニングループの実装
- tf.keras.Model.fit
- custom training loops
- 複雑なトレーニングループを組む必要がある手法の時の選択肢
- GANやSimCLRなど。論文実装はこちらがよく使われている印象
- https://www.tensorflow.org/guide/keras/writing_a_training_loop_from_scratch
- 複雑なトレーニングループを組む必要がある手法の時の選択肢
- tensorflow estimator
- 実験の様々な処理をカプセル化する
- 自由度が高いが、tf.kerasと比べて複雑で可読性が悪くなる印象
- どんどん使われなくなってきていると思うが、ネット上では見かける実装方法
- https://www.tensorflow.org/guide/estimator
主に分けてこれらの実装がよくみられます。特に「こう実装するべき!」という主張はないため、これらが混在している印象です。 tensorflowの初学者は特に実装の方法がたくさんあって苦戦を強いられる状況になってしまっていると思います。 自分自身、TensorFlowでコードを書く時の推奨の実装パターンのようなものがあったら嬉しいなと思っていたので、今回は個人的に良いと思う実装パターンを書いていきたいと思います。
tf.keras.Model
に寄せた実装
ここが今回のメイン部分になります。まずは、tf.keras.Model
に寄せて実装すると何が嬉しいか書いていきます。
いい点
- 便利なコールバックを簡単に使える
- LRスケジュールとか、TensorBoardのログ記録とかも優秀
- compileが優秀
model.save
する際にcompileで設定した情報も保存してくれる- compileしていれば学習途中のoptimizerの状態保存までしてくれる => 実験の再現性
- TensorBoardコールバックなどでも、ここで設定したロスやメトリクスを自動で記録してくれる
逆に、これらの部分は、custom training loops
など、他の方法で実装する際には自前で実装しなければなりません。
tf.keras.Model.fit
を使わない場合
tf.keras.Model
のエコシステムが勝手にやってくれる部分を自前で実装していく必要があります。
tensorboard
例えば、tensorboard用のmetricsやログを取りたかったら、summary_writerを必要な分定義し、トレーニングループ内などでログ記録用のコードを色々と書かなければならなくなり、見栄えが悪くなります。
train_summary_writer = tf.summary.create_file_writer(train_log_dir) test_summary_writer = tf.summary.create_file_writer(test_log_dir) for epoch in range(EPOCHS): for (x_train, y_train) in train_dataset: train_step(model, optimizer, x_train, y_train) with train_summary_writer.as_default(): tf.summary.scalar('loss', train_loss.result(), step=epoch) tf.summary.scalar('accuracy', train_accuracy.result(), step=epoch) for (x_test, y_test) in test_dataset: test_step(model, x_test, y_test) with test_summary_writer.as_default(): tf.summary.scalar('loss', test_loss.result(), step=epoch) tf.summary.scalar('accuracy', test_accuracy.result(), step=epoch) template = 'Epoch {}, Loss: {}, Accuracy: {}, Test Loss: {}, Test Accuracy: {}' print (template.format(epoch+1, train_loss.result(), train_accuracy.result()*100, test_loss.result(), test_accuracy.result()*100)) # Reset metrics every epoch train_loss.reset_states() test_loss.reset_states() train_accuracy.reset_states() test_accuracy.reset_states()
tf.keras.Model.fit
のコールバックであればその設定をするだけでOKです。
tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=1)
optimizer
また、トレーニングは途中で停止・異常終了された時に適切に復旧できるように実装されている必要もあります(トレーニングインスタンスのプリエンプトが主な理由)。 checkpointを保存している例は見ますが、optimizerの状態保存などはあまり考慮されてないケースをよく見かます。これもトレーニングの継続には大事な部分でありますが、自前で実装するのは少し面倒な部分です。
これも、tf.keras.Model.compile
でコンパイルしていれば、(公式で実装済みのoptimizerであれば)model.save
時にデフォルトで保存してくれるようになっています。model.save
のinclude_optimizer
引数で制御することも可能です。
これらのように、tf.keras.Model
に寄せて実装することで、自分で書かなければいけない部分を大幅に減らし、コードの可読性を保つことができます。
ここではひとまずこれらの例を上げましたが、他にも便利な部分や、今後追加されていく機能も多いと思います。
カスタマイズ性
色々と便利な点を書いていきましたが、カスタマイズ性の観点が肝になってくると思います。
そもそも複雑なトレーニングループを書きたいからcustom training roops
で実装しているという方が多いかと思いますが、tf.keras.Model.fit
でトレーニングを実行する場合にも、実はcustom training roops
と同様にトレーニングループを書くことは可能です。
tf.keras.Model.train_stepのオーバーライド
上記公式ドキュメントにも書かれていることなので、知っている人は知ってると思います。当たり前な話ですが、fit
内部で使ってる関数をオーバーライドするということです。
手順
以下のようにtf.keras.Model
のサブクラスを作成します。
class MyModel(tf.keras.Model): """Example in overridden `tf.keras.Model.train_step` Arguments: data: A tuple of the form `(x,)`, `(x, y)`, or `(x, y, sample_weight)`. Returns: The unpacked tuple, with `None`s for `y` and `sample_weight` if they are not provided. """ def train_step(self, data): # If `sample_weight` is not provided, all samples will be weighted # equally. x, y, sample_weight = tf.keras.utils.unpack_x_y_sample_weight(data) with tf.GradientTape() as tape: y_pred = self(x, training=True) loss = self.compiled_loss(y, y_pred) gradients = tape.gradient(loss, self.trainable_variables) self.optimizer.apply_gradients(zip(gradients, self.trainable_variables)) self.compiled_metrics.update_state(y, y_pred, sample_weight) return {m.name: m.result() for m in self.metrics}
参照元。一部改変。
モデルを作成する時に以下のようにカスタムのモデルクラスでラップすればOKです。
def build_model(input_shape: List, num_classes: int): """トレーニングに使用するモデルを作成する. Args: input_shape {List} -- 入力データのshape. num_classes {int} -- クラス数. """ # 例 inputs = tf.keras.Input(shape=input_shape) outputs = tf.keras.layers.Dense(num_classes, activation="softmax")(inputs) model = MyModel(inputs, outputs) # カスタムモデルクラスをしようしない場合は以下 #model = tf.keras.Model(inputs, outputs) return model
たったこれだけでtf.keras.Model.fit
を使ってトレーニングの実行が可能になります。実際、custom training roops
の方法で実装した関数をほぼそのままtf.keras.Model.train_step
に移植するだけで良いと思います。
その他、損失関数やOptimizerなども当然カスタムも可能です。tf.keras.Model.compile
で指定すれば、self.optimizer
やself.compiled_loss
などでトレーニングループ内からアクセスできます。
def custom_loss_func(y: Tensor, y_pred: Tensor) -> Tensor: """カスタムの損失関数を実装する. Args: y {Tensor} -- 例えば教師ラベル y_pred {Tensor} -- 例えばモデルの予測値 Returns: Tensor -- 損失の計算結果 """ loss = y - y_pred return loss model.compile(loss=custom_loss_func, optimizer=custom_optimizer, metrics=[custom_metrics])
tf.keras.Model.fitに寄せた実装にするもう一つの利点は、コードの共通化が可能なところです。ここまで書いてきたカスタマイズできる部分は必要に応じてカスタマイズし、残りは毎回同様のコードを使いまわせることになります(例えば以下)。
def main(argv): if len(argv) > 1: raise app.UsageError('Too many command-line arguments.') # tf.distribute.Strategyを使うかどうか if FLAGS.use_tpu: # Setup tpu strategy cluster = tf.distribute.cluster_resolver.TPUClusterResolver() tf.config.experimental_connect_to_cluster(cluster) tf.tpu.experimental.initialize_tpu_system(cluster) distribute_strategy = tf.distribute.TPUStrategy(cluster) with distribute_strategy.scope(): model = build_model(FLAGS.input_shape, num_classes=FLAGS.num_classes) optimizer = tf.keras.optimizers.Adam(learning_rate=FLAGS.learning_rate) elif FLAGS.use_gpu: # Setup mirrored strategy distribute_strategy = tf.distribute.MirroredStrategy() with distribute_strategy.scope(): model = build_model(FLAGS.input_shape, num_classes=FLAGS.num_classes) optimizer = tf.keras.optimizers.Adam(learning_rate=FLAGS.learning_rate) else: model = build_model(FLAGS.input_shape, num_classes=FLAGS.num_classes) optimizer = tf.keras.optimizers.Adam(learning_rate=FLAGS.learning_rate) model.compile(loss=custom_loss_func, optimizer=optimizer, metrics=["accuracy"]) model.summary() tboard_callback = tf.keras.callbacks.TensorBoard(log_dir=f"{FLAGS.job_dir}/logs", histogram_freq=1) callbacks = [tboard_callback] train_ds = get_dataset(FLAGS.dataset, FLAGS.global_batch_size, "train") valid_ds = get_dataset(FLAGS.dataset, FLAGS.global_batch_size, "valid") for epoch in range(FLAGS.epochs): model.fit(train_ds, validation_data=valid_ds, callbacks=callbacks, initial_epoch=epoch, epochs=epoch+1) model.save(f"{FLAGS.job_dir}/checkpoints/{epoch+1}", include_optimizer=True) model.save(f"{FLAGS.job_dir}/saved_model", include_optimizer=False)
分散学習の場合も、基本的には、ほぼ変わらない実装で機能します。
サンプルリポジトリ
雛形のコードとサンプル実装を以下のリポジトリに載せています。
https://github.com/tonouchi510/tensorflow-design
(今はSimCLRの実装例しか載せてませんが)複雑な手法の実装も可能であることがわかると思います。
なお、宣伝的になってしまいますが、SimCLRのこの実装に関しては、技術書典10でmixi tech note #5
の2章でも掲載予定です。より詳しい情報や、興味がある方は読んでいただけると幸いです。
=> SimCLRの実装としてはMinimalな実装で扱いやすく、分散学習にも対応しているという点で、ある程度需要があるんじゃないかというのもあってこの題材をテーマにしてます。
まとめ
このように、tf.keras.Model.fit
はcompile
で指定できるトレーニング手法しか使えないわけではなく、かなり拡張性が確保されています。tf.keras.Model
のエコシステムを理解すれば、その恩恵を受けつつ、かなり自由度高くトレーニングループを書くことが可能です。
また、自分でコードを書く部分は最小限に抑えられるので、可読性や拡張性の観点で優れているのではないかと思っています。
公式ドキュメントでは、やり方は小さく書かれていますが、あまりこういう実装がいいという主張はなかったように思うので、ここで紹介させていただきました。
現状ではまだ様々な実装のされ方がしていて読むのが辛い状況ですが、これに限らず実装の仕方がもう少し統一されるようになってくれれば良いなぁと思っています。
個人開発シリーズ2:ドメインモデリング
こちらの記事の続きになります。
今回は前回学んだ知識をもとに、ドメインモデリングを行います。 いきなり大きな規模で作ると終わりが見えず、個人開発としては厳しいので、まず解決したい問題を絞ってモデリングを行なっていきたいと思います。
目標はシリーズ0で書いた通り、「個人投資家でもVCなどと同じ水準の投資活動が行えるようなツールを作ること」でした。そのために必要なものとして、ドメインについて学んだ結果、以下の二点が思いつきました。
前者は最も重要な素養であり、VCの能力に直結する部分ですが、個人投資家において解決は簡単ではないため、先に後者の実装を進めようと思います(といってもここでは一旦VCと同様な業務フロー管理を可能にする部分のみ)。つまり今回のモデリングでは後者を対象に行います。
とりあえず重要な用語・概念を挙げてみる
スモールスタートでいきたいので、ひとまずこんな感じでしょうか。
名称 | 説明 |
---|---|
ユーザ | 個人投資家として投資活動を行う主体。本ツールで支援する対象。 |
業界 | 企業を業種や取り扱い商品等により分類する概念。 |
企業 | ユーザにとって以下5つの投資活動を行う対象。 |
Deal Sourcing | 投資先を発掘する業務。 |
Due Diligence | 発掘した投資先候補を詳しく検討するフェーズ。 |
Exection | Due Diligenceで決定した投資先に投資額と期間等を定め、投資の実行をするフェーズ。 |
Monitoring | 投資先の監視・観察を行うフェーズ。 |
Exit | 利益を確定させるために売却を行うフェーズ。 |
運用実績 | ユーザの上記投資活動の実績・成績。 |
まず先に難しかった点:動詞どうしよう()
※深夜2時を回りました
上記用語でも書いたVC業務フローのモデリングが今回の最も重要な点になっていると思います。
ただし、上で挙げた業務フローに関する概念(Due Diligence等)は動詞的な意味合いを持つ単語なんですが、「あれ動詞ってどうしよう??」となり、ドメインモデルとして定義することに難しさを感じました(一般的にドメインモデル=名詞
だと思いますし、動詞だと他との関係性の定義も難しい)。
とはいえ、本ツールを実装する上でこの部分のモデリングは最も重要なものであるため、蔑ろにするわけにもいかないという状況でした(動詞だとしてもドメインモデルに落とし込んでみようと考えを巡らせたりはしましたが、これはすぐに破綻しました。。)
ではどのようにして解決したかというと、新たに適切な名詞を見つけることで解決につながりました。元々下図の通り、ユーザの投資対象として企業が直接関係を持つ形で定義してしまっていたのですが、
下図のように、「投資案件」という名詞をドメインモデルとして捉え、ユーザと直接関係を持たせる対象としました。
これにより、「Deal Sourcingは新しい"投資案件"の発掘・追加すること」、「Due Diligenceは投資案件を検討・分析すること」、「Exectionは投資案件に対して投資を実行すること」など、VC業務フローの全てがこの名詞に紐付き、これらの動詞が全てこのドメインモデルを操作する行動であると捉えることができました。この気づきを得たきっかけはユースケース図を書いたことなので、よくドメインモデリングでユースケース図書くのが大事と言われてますが、それを体感できたので良かったです。
今回のように動詞どうしよう問題(ドメインの重要な概念が動詞になるケース)って、モデリングの仕方が悪いのか、一般によくぶち当たる問題なのかは分かりませんが、今回のように適切な名詞を発見するのが一つの解決策になりそうということは発見でした。
また、この辺りのモデリングをしながら気づいたのですが、Deal SourcingやDue Diligenceの具体的な部分のモデリングについても、名詞化やその動詞の成果物を対象に考えると意外と関係を捉えられるようになることにも気づきました。最後の節で具体的なドメインモデル図を示しています。
コンテキストマップ
だいぶドメインモデルも端折って簡略化してますが、とりあえずこんなイメージです。よくあるシステム上のユーザ概念と境界を引くためのコンテキストに加え、将来的に企業向けとしてスタートアップと個人投資家を紐付ける機能とかもあると面白そうと思い、そのためのコンテキストを追加してたりします(今のところ実装対象ではないですが、拡張機能として考慮に入れておくという意味で)。コンテキストが変わるとドメインモデルの意味合いや関係性がかなり変わる想定ですので、今のうちに明確化しておきます。
ドメインモデル
ひとまずざっくりとなモデリング図を書いています。ここまで述べたように、重要な概念である投資案件(InvestmentItem)が中心にきて、それぞれの関係をざっくり示しています。
※実装進めたり進捗が出たらもう少しちゃんとしたものに差し替えるかも
また、「Deal Sourcing => Deal Source
と案件発掘の元となるものをモデリングすることに」、「Due Diligence => Due DiligenceArtifacts
と分析結果の成果物をモデリングするように」考えることで、↑のように関係を捉えることができるようになりました。
後でその辺の開発を進めるときのために、今のうちに含めています。
このように、動詞だとしてもその意味を理解し、適切な名詞に置き換えることで、ドメインの関係が捉えやすくなるということが発見でした(DDDばんばんやってる方からしたら当たり前かもしれませんが)。
他にも、同じUser、Companyというモデルでも、コンテキストが異なると意味合いが異なるので、今のうちからそれをモデル図に表現しています。
それでは開発の一歩として、今回モデリングした部分をコードに落とし込んでいこうと思います。それではまた来週(...来月かも)。
Advanced Android in Kotlin (created by google developers training team) 備忘録
はじめに
これは、以下のKotlin/Androidの基礎チュートリアルについて書いた記事の続きです。
moai510.hatenablog.com ※こちらも過去ブログの移行ですが
↑の記事ではGoogle Developers Training teamが作成したKotlin基礎のチュートリアルについての紹介とメモを書いてましたが、今回はそのチュートリアルの続編的な内容のCourceをやったのでその紹介と備忘録として書きます。
Advanced Android in Kotlin
基礎コースと同じく、Google Developers Training teamが作成した発展的内容に関するCourseです。
Advanced Android in Kotlin: Welcome to the course | Android Developers
Lessonは全部で6つあり、以下の構成になっています。
- Lesson 1: Notifications
- Lesson 2: Advanced Graphics
- Lesson 3: Animation
- Lesson 4: Geo
- Lesson 5: Testing and Dependency Injection
- Lesson 6: Login
タイトル通りの内容なので特に説明は省きますが、だいたい必要になりそうな機能がカバーされてるのですごく参考になりそうです。
今回は、特にLesson5のテストについて取り上げます。
Lesson5: Testing
このコースでは、簡単なTODOリストのアプリを例にテストの方法を学べます。
基本知識
Android Projectは基本的に以下の3つのSource Set(フォルダー)で構成される。
チュートリアルのアプリの例 com.example.android.architecture.blueprints.todoapp (main) com.example.android.architecture.blueprints.todoapp (androidTest) com.example.android.architecture.blueprints.todoapp (test)
- main: アプリケーションのコードを含む
- androidTest: instrumented testとして知られるテストを含む
- test: ローカルテストを含む
ここでいうinstrumented testとlocal testの違いは実行方法にある。
local test (test source set)
ローカル開発マシンのJVMで実行され、エミュレーターまたは物理デバイスを必要としない。
高速で実行されるが、実環境での動作ではないので、忠実度は低い。
local testのコード例
class ExampleUnitTest { // Each test is annotated with @Test (this is a Junit annotation) @Test fun addition_isCorrect() { assertEquals(4, 2 + 2) assertEquals(3, 1 + 1) // This should fail } }
instrumented test (androidTest source set)
実際のAndroidデバイスまたはエミュレートされたAndroidデバイスで実行されるため、実環境での動作を確認できるが、非常に遅い。
instrumented test のコード例
- Android固有のコードを含む部分のテストを行う
@RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("com.example.android.architecture.blueprints.reactive", appContext.packageName) } }
テストの実行方法
Android Studio使ってればボタンぽちぽちでいける。手順は割愛(サイト参照)。
テストのコーディング
Android Studioの機能に、testスタブを生成してくれるという便利機能がある。テスト作成の手順は以下の通り。
- 目的の関数にカーソルを合わせてメニューを開き、[Generate...]でスタブ作成
- ダイアログは基本的にそのままでOK
- 作成先ディレクトリは、android固有のコードを含むかどうかでtest/androidTestを選択する
- 雛形が作成される
- テストを記述する
Android Classのテスト
ViewModelのテスト
Android特有のクラスではあるが、ViewModelのコードはAndroidフレームワークやOSに依存するべきではないため、local testとして書く。 Application ContextやActivityなどが必要になる場合は、AndroidX Test Librariesを使うとシミュレートできる。
// Test用のViewModelの生成 val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
AndroidX Testライブラリはlocal testでもinstrument testでも同じようにテストできるので切り分ける必要はない。
LiveDataのテスト
LiveDataのテストでは以下が必要 - InstantTaskExecutorRuleを使用する - LiveDataを観測する
InstantTaskExecutorRuleは、バックグラウンドジョブをシングルスレッドで動くようにするルールで、LiveDataのテストをする際に必要(同期したいので)。 LiveDataを観測するためには、observeForeverメソッドを使用し、LifeCycleOwnerを必要とせずに監視できるようにする。
// コードの例 @Test fun addNewTask_setsNewTaskEvent() { // Given a fresh ViewModel val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()) // Create observer - no need for it to do anything! val observer = Observer<Event<Unit>> {} try { // Observe the LiveData forever tasksViewModel.newTaskEvent.observeForever(observer) // When adding a new task tasksViewModel.addNewTask() // Then the new task event is triggered val value = tasksViewModel.newTaskEvent.value assertThat(value?.getContentIfNotHandled(), (not(nullValue()))) } finally { // Whatever happens, don't forget to remove the observer! tasksViewModel.newTaskEvent.removeObserver(observer) } }
実際には上だと毎回書くのが面倒なので、便利な拡張関数も示してくれている。
import androidx.annotation.VisibleForTesting import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException @VisibleForTesting(otherwise = VisibleForTesting.NONE) fun <T> LiveData<T>.getOrAwaitValue( time: Long = 2, timeUnit: TimeUnit = TimeUnit.SECONDS, afterObserve: () -> Unit = {} ): T { var data: T? = null val latch = CountDownLatch(1) val observer = object : Observer<T> { override fun onChanged(o: T?) { data = o latch.countDown() this@getOrAwaitValue.removeObserver(this) } } this.observeForever(observer) try { afterObserve.invoke() // Don't wait indefinitely if the LiveData is not set. if (!latch.await(time, timeUnit)) { throw TimeoutException("LiveData value was never set.") } } finally { this.removeObserver(observer) } @Suppress("UNCHECKED_CAST") return data as T }
このようにgetOrAwaitValueと呼ばれるKotlin拡張関数を作成して置いておくことで、以下のように簡潔にLiveDataのAssertionが書ける。
val value = tasksViewModel.newTaskEvent.getOrAwaitValue() assertThat(value.getContentIfNotHandled(), (not(nullValue())))
完全なコード例
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.not import org.hamcrest.Matchers.nullValue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class TasksViewModelTest { @get:Rule var instantExecutorRule = InstantTaskExecutorRule() @Test fun addNewTask_setsNewTaskEvent() { // Given a fresh ViewModel val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()) // When adding a new task tasksViewModel.addNewTask() // Then the new task event is triggered val value = tasksViewModel.newTaskEvent.getOrAwaitValue() assertThat(value.getContentIfNotHandled(), not(nullValue())) } }
まとめ
ここまで書いてきた基礎知識に加え、実際のテストのコーディングの詳細や便利なライブラリ群、TDDについても紹介している。 綺麗に書く方法まで紹介してくれているので、実際にテストを書く場面になったらまた適宜参照して行きたい。
テストのライブラリ
- JUnit4:
- Hamcrest: 可読性の高いAssertionが書けるようにするライブラリ
- AndroidX Test Library: ActivityやContextなどAndroidクラスが必要なlocal testを実行するために必要なライブラリ
- AndroidX Architecture Components Core Test Library