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も同一パッケージ内で書く必要がある(まあ大した問題にはならない はず)
  • 継承ないことはそこまで気にならない

一部サンプルコード抜粋

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)

こちらの記事の続きになります。

moai510.hatenablog.com

今回は開発を進めるにあたってもう少しイメージを固める必要があったので、アーキテクチャ図や各コンポーネントの設計をざっくり行ってみました。

アーキテクチャ

ひとまずざっくりしたアーキテクチャ図はこんな感じのイメージで設計しました(前回のコンテキストマップのCore部分のみ)。細かい話は後半で書いてます。 各サービス間連携の部分等、より詳細の部分はまた後ほど載せていこうと思います。

f:id:moai510:20211012011820p:plain

APIゲートウェイ

バックエンドのマイクロサービスアプリケーションへのエントリとなるサービスになります。 ここではリクエストのルーティングやAPI合成、認証などを担当します。

前回の記事のドメインモデル図で、UserはAuthenticationコンテキストにも所属していましたが、Authentication.Userドメインの責務はここに関わる部分なので、あえて個別サービスを作るのではなくAPIゲートウェイの中でその役割を実装することにします。

また、認証周りは自前実装は避けたいところですし、Firebase Authenticationという優秀な認証基盤もあるのでこれを使って認証・認可を行います。

マイクロサービス

本来、開発当初にマイクロサービスアーキテクチャを選定することはあまり良い選択肢ではなく、最初はモノリスでスタートさせて組織の拡大とともにマイクロサービスに移行する方が適切なのが一般的です。 その方がドメインに対する知識も溜まっており、適切なサービス分割を行うことができます。

ただし、今回は勉強目的でもあるので最初からマイクロサービスを採用することにしています。知見のない状態でサービス分割を進めることになるので、ここでは明確な境界を引ける部分のみでサービス分割を行っていくことにします。 開発を進めていく中でもし粒度が大きい場合はその時に分割することにするとします(個人開発なのでそもそも分割する必要なんてないんですが、そこはまあ仮想的にです)。

サービス分割の指針

まず考えられるものとしては、コンテキストが異なるものは分割して考えられることがわかります。

さらにコンテキスト内にあるドメインモデルに境界を引いていきます。この時、一般的にはアグリゲート(集約)が明確な境界となります。アグリゲートの識別もドメイン知識がない状態では難しいのですが、アグリゲートは整合性の単位であり、以下のルールがあります。

  1. 外部からはアグリゲートルートだけを参照する
  2. アグリゲート間の参照では主キーを使わなければならない
  3. 1つのトランザクションで1つのアグリゲートを作成または更新する

これらを意識して開発を行うことを想定しつつ、現状でわかる範囲でサービス分割を行いました。

サービス分割

以上の指針の中で、ひとまずCoreコンテキストについて進めます。

まず少なくともUserは一つのアグリゲートととして考えることができそうなことがわかります。 また、本ツールの重要な概念であるInvestmentItem(投資案件)をアグリゲートルートとした投資案件サービスも考えます。UserはInvestmentItem(投資案件)をリストを保有していますが、idをリストとして持つことでルール2を満たせます。

また、InvestmentItemとCompanyおよびDealSourceも整合性の単位として一つではないと容易に想定できるため、分離して考えます。

「InvestmentItemとDueDiligence〇〇」および「UserとPerformance」は正直現時点で判断するには知識不足であるため、ひとまず同じアグリゲートに属すものとして考えてみようと思います。

ここまでをまとめると、以下の図の通りになります。実線はオブジェクト参照、点線は主キー参照です。

f:id:moai510:20211014223909p:plain

それでは大体開発に入るための指針が整ったので開発に入っていこうと思います。 次回はようやくコードを載せられると思います。

念願の湖の近くに引っ越すことになった件(浜松移住)

リモートワークが主流になり、都外の物件を見続けて半年程経ちました。。

ついに理想の立地 & 物件を見つけることができ、引越しすることになったのでそのことを書いておこうと思います。 もはや物件探しが趣味になっており、コンサル業できるんじゃないかと思っています(おまけで物件選びのコツも一応書いてます)。

引越し先としては、湖の近くでリモートワークできたら良いなと思っていて、静岡県浜松市佐鳴湖周辺がすごい良さそうということに気づき、周辺の物件に決めました。

f:id:moai510:20211009231035p:plain
引用:https://www.iiranavi.net/shop/shop.shtml?s=2079

浜松市は元々住んでいたこともあるのですが、以下のサイトを見て特に佐鳴湖周辺がすごく良さそうだなと思いました。

www.entetsuassist-dms.com

佐鳴湖周辺は自然を感じられる土地ですが、ちょっと移動すれば浜松駅や浜松市西部など、結構発展した街に行くこともできます。

浜松駅は新幹線も通っているので、仕事の都合や用事ができた時に東京にいくのも簡単なところも良い点です。

自分は体を動かすことも好きなので、佐鳴湖の外周をランニングするのも楽しみに感じてます。1週約6キロほどだそうです。 結構ランニングコースが整っていて、あの"山の神" 神野大地さんもラントレしてる?のもYoutubeで見ました。

f:id:moai510:20211009230746p:plain
引用:https://www.entetsuassist-dms.com/sanaru-park/facilities/

引越したらようやく広いお家になるため、今使ってる幅70cmにも満たないデスクは窓から投げ捨てて、 デスク環境整備を全力でやろうと思っているので、引越し完了したらデスク環境整備の参考になりそうな部分をブログに書こうかと思います。

おまけ:物件の選び方

ちなみに、物件探しを色々やってきた中で、個人的に気にしておいた方が良いと思ったことを書いておこうかと思います。

1. 搬入経路

意外と物件探しの時は考慮から漏れがちですが、物件が決まっていざ引越しという時に一番よく起こる問題じゃないでしょうか。 特に大きな家具を持っていて、お気に入りなのでどうしても使い続けたいというものがある場合は、最初からこの点を考慮に入れつつ物件を探した方が安全かと思います。

ベッドやソファなどは注意が必要で、せっかくお気に入り/高価なものを買ったのに入らないとすごい残念な感じになってしまいます。 ベッドであればダブルサイズは大抵の物件で入るようですが、それ以上の大きさになると搬入できないケースも結構あるそうです。

あと、仲介業者も大抵この辺は話してくれないので注意が必要です(というか悪い点は基本聞かないと教えてくれないです)。

2. 地域の補助金を確認する

引越し先が移住やリフォーム等の補助をやっていることもあるので、確認しておくのはおすすめです。 引越しは初期費用が結構ネックになってきますが、知っておくと初期費用分をかなり浮かせられる可能性があります。

最近有名なのはこちらの移住支援金・企業支援金の制度で、首都圏に5年以上住んでいる方が地方に引っ越す時に対象です(ちなみに自分はまだ該当しませんでした😭 )。 これは全国レベルでやっている制度ですが、地域によってはこれに加えて補助金が出ているところもあります。

3. 本当に完璧な物件が出ていたら時期を待たずに決めるべき

人それぞれ条件や理想があると思いますが、半年程見続けて、本当に全ての観点で理想通りにいく物件に出会えるのはかなり厳しいというのを感じてます。

かなり理想に近い物件もあったのですが、もう少し後で引っ越したいという理由で待っていたら取られてしまった、ということが2回ありました。 地方だからと油断していたのですが、良い物件の場合は、地方でも1週間とたたずに取られてしまうので、気をつけておくべきだと思います。

4. 仲介業者は完全な味方ではない

先ほども少し述べましたが、仲介業者から悪い点とかを率先して教えてくれることはまずありません。こちらから「これはどう?」とか聞く必要があります。 また、初期費用にも訳の分からない費用が入っていることもあるので、ちゃんと内容を聞いて、場合によってはNOを突きつけた方が良いケースもあると思います。

聞けば真摯に答えてくれるところもありましたが、平気で嘘ばかりつく仲介業者もいたので残念でした(インターネットでなんでも調べられる時代なので、正直すぐにわかっちゃいます)。

(引越しは人生において何度かあるイベントですし、ちゃんとした対応をしてもらえれば次もそこを利用しようとなるのでその方が長期的には良いと思うのですが...)

最後はちょっと愚痴みたいになってしまいましたが、こんなところで締めようと思います。物件探しの参考になれば幸いです。

「焼肉ジャンボ はなれ」に行ってきた

今回はご飯メモ。良い焼肉屋を見つけてしまったのでブログに書いて記録しておきます。

tabelog.com

食べログ4.37というすごい数値がついています。

割と有名だったり、高い焼肉屋とかにも何度か行く機会/連れて行ってもらえる機会はあったんですが、4.37という数値の場所に行くのは初めてでした。 (ちなみに今回は自分が後輩達を連れてご馳走したという善行をしてます)

高級焼肉店はどのお店もお肉が柔らかくて美味しかったんですが、大抵高そうな雰囲気のお店になるんですが、ここは比較的入りやすい&カジュアルな雰囲気のお店という感じがしました。

ご飯の方ですが...

無能なので今回写真はこれしか撮ってませんでした😇

f:id:moai510:20211007230549p:plain
牛ご飯

この写真にある牛ご飯や、その他ご飯もの、かつなど調理に時間のかかるものは事前予約が必要なのが注意です。

※ちなみに予約サイトも忘れたという無能ぶり

最近雑になってきた...

TensorFlowの実装プラクティス

最近忙しくてネタがないので、Qiitaに書いてたこちらの記事を移行してきました。 TensorFlowの実装プラクティス - Qiita

※いろんな媒体に分散して書いちゃってるものを徐々にこちらに一本化していこうかとおもいます

はじめに

TensorFlowを使っていると、たくさんのライブラリや様々な実装の仕方があることがわかると思います。これはtf2が出てきても依然として状況は変わってないように思えます。自由度が高い反面、公開リポジトリのコードを読む際にも、実装の仕方が様々で読み解くのが大変になってしまっていると思います。

今回は、TensorFlowで実験コードを書く際に、個人的に良いと思う実装パターンを書いていこうと思います。結論を言うと、tf.keras.Modelのエコシステムを存分に利用して実装しよう、という主張です。

※ツッコミも大歓迎です。

tf2での実装方法

ここでは主にトレーニングループの実装に焦点をおきます。

レーニングループの実装

主に分けてこれらの実装がよくみられます。特に「こう実装するべき!」という主張はないため、これらが混在している印象です。 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.saveinclude_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.optimizerself.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.fitcompileで指定できるトレーニング手法しか使えないわけではなく、かなり拡張性が確保されています。tf.keras.Modelのエコシステムを理解すれば、その恩恵を受けつつ、かなり自由度高くトレーニングループを書くことが可能です。 また、自分でコードを書く部分は最小限に抑えられるので、可読性や拡張性の観点で優れているのではないかと思っています。

公式ドキュメントでは、やり方は小さく書かれていますが、あまりこういう実装がいいという主張はなかったように思うので、ここで紹介させていただきました。

現状ではまだ様々な実装のされ方がしていて読むのが辛い状況ですが、これに限らず実装の仕方がもう少し統一されるようになってくれれば良いなぁと思っています。

個人開発シリーズ2:ドメインモデリング

こちらの記事の続きになります。

moai510.hatenablog.com

今回は前回学んだ知識をもとに、ドメインモデリングを行います。 いきなり大きな規模で作ると終わりが見えず、個人開発としては厳しいので、まず解決したい問題を絞ってモデリングを行なっていきたいと思います。

目標はシリーズ0で書いた通り、個人投資家でもVCなどと同じ水準の投資活動が行えるようなツールを作ること」でした。そのために必要なものとして、ドメインについて学んだ結果、以下の二点が思いつきました。

前者は最も重要な素養であり、VCの能力に直結する部分ですが、個人投資家において解決は簡単ではないため、先に後者の実装を進めようと思います(といってもここでは一旦VCと同様な業務フロー管理を可能にする部分のみ)。つまり今回のモデリングでは後者を対象に行います。

とりあえず重要な用語・概念を挙げてみる

スモールスタートでいきたいので、ひとまずこんな感じでしょうか。

名称 説明
ユーザ 個人投資家として投資活動を行う主体。本ツールで支援する対象。
業界 企業を業種や取り扱い商品等により分類する概念。
企業 ユーザにとって以下5つの投資活動を行う対象。
Deal Sourcing 投資先を発掘する業務。
Due Diligence 発掘した投資先候補を詳しく検討するフェーズ。
Exection Due Diligenceで決定した投資先に投資額と期間等を定め、投資の実行をするフェーズ。
Monitoring 投資先の監視・観察を行うフェーズ。
Exit 利益を確定させるために売却を行うフェーズ。
運用実績 ユーザの上記投資活動の実績・成績。

まず先に難しかった点:動詞どうしよう()

※深夜2時を回りました

上記用語でも書いたVC業務フローのモデリングが今回の最も重要な点になっていると思います。 ただし、上で挙げた業務フローに関する概念(Due Diligence等)は動詞的な意味合いを持つ単語なんですが、「あれ動詞ってどうしよう??」となり、ドメインモデルとして定義することに難しさを感じました(一般的にドメインモデル=名詞だと思いますし、動詞だと他との関係性の定義も難しい)。

とはいえ、本ツールを実装する上でこの部分のモデリングは最も重要なものであるため、蔑ろにするわけにもいかないという状況でした(動詞だとしてもドメインモデルに落とし込んでみようと考えを巡らせたりはしましたが、これはすぐに破綻しました。。)

ではどのようにして解決したかというと、新たに適切な名詞を見つけることで解決につながりました。元々下図の通り、ユーザの投資対象として企業が直接関係を持つ形で定義してしまっていたのですが、

f:id:moai510:20210912023225p:plain:w450

下図のように、「投資案件」という名詞をドメインモデルとして捉え、ユーザと直接関係を持たせる対象としました。

f:id:moai510:20210912023411p:plain:w300

これにより、「Deal Sourcingは新しい"投資案件"の発掘・追加すること」、「Due Diligenceは投資案件を検討・分析すること」、「Exectionは投資案件に対して投資を実行すること」など、VC業務フローの全てがこの名詞に紐付き、これらの動詞が全てこのドメインモデルを操作する行動であると捉えることができました。この気づきを得たきっかけはユースケース図を書いたことなので、よくドメインモデリングユースケース図書くのが大事と言われてますが、それを体感できたので良かったです。

f:id:moai510:20210912032433p:plain

今回のように動詞どうしよう問題(ドメインの重要な概念が動詞になるケース)って、モデリングの仕方が悪いのか、一般によくぶち当たる問題なのかは分かりませんが、今回のように適切な名詞を発見するのが一つの解決策になりそうということは発見でした。

また、この辺りのモデリングをしながら気づいたのですが、Deal SourcingやDue Diligenceの具体的な部分のモデリングについても、名詞化やその動詞の成果物を対象に考えると意外と関係を捉えられるようになることにも気づきました。最後の節で具体的なドメインモデル図を示しています。

コンテキストマップ

だいぶドメインモデルも端折って簡略化してますが、とりあえずこんなイメージです。よくあるシステム上のユーザ概念と境界を引くためのコンテキストに加え、将来的に企業向けとしてスタートアップと個人投資家を紐付ける機能とかもあると面白そうと思い、そのためのコンテキストを追加してたりします(今のところ実装対象ではないですが、拡張機能として考慮に入れておくという意味で)。コンテキストが変わるとドメインモデルの意味合いや関係性がかなり変わる想定ですので、今のうちに明確化しておきます。

f:id:moai510:20211013005107p:plain

ドメインモデル

ひとまずざっくりとなモデリング図を書いています。ここまで述べたように、重要な概念である投資案件(InvestmentItem)が中心にきて、それぞれの関係をざっくり示しています。

※実装進めたり進捗が出たらもう少しちゃんとしたものに差し替えるかも

f:id:moai510:20211013010252p:plain

また、「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でも同じようにテストできるので切り分ける必要はない。

f:id:tonotech:20200131000323p:plain

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