アナログ金木犀

つれづれなるまままにつれづれする

Rxなインスタンスを返却するメソッドを実装する時の注意点

そういえばよくレビューで指摘してたなぁと思い出したのでメモ書き。

例えばこういうコード。

fun findContent(): Maybe<Content> {
    val connectivityManager = application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    return if (connectivityManager.activeNetworkInfo.isConnected) {
        apiRepository.find()
    } else {
        localRepository.find()
    }
}

Content を取得したい時に、オンラインだったら apiRepository を使って、オフラインだったら localRepository を使うというコード。

このコードにはいくつか問題があって、 as じゃなくて as? を使った方が安全とかは間違いなくそうなんだけど、それよりも オンラインかオフラインか の判定ロジックが Rxな世界に包まれていないところが問題だと思っている。

オンラインかオフラインか の場所で Throwable が投げられる可能性がある場合、 これを使っている側がしっかり onError を実装していたとしてもで落ちてしまう。それも、悪質なのは subscribe している箇所ではなく findContent を呼んでいる箇所で、だ。 いうまでもなく subscribe されなくてもこの判定ロジックは処理されてしまう

もし Throwable が投げられる可能性がないとしても、判定に時間がかかったりすることもあり、想定と違うものになりかねない。

なので、最終的に返すもの以外のロジックすべてをちゃんとRxな世界で包んであげましょう

自分の場合だと下記みたいに書くと思う。

fun findContent(): Maybe<Content> =
    isConnected().flatMapMaybe { isConnected ->
                    if (isConnected) {
                        apiRepository.find()
                    } else {
                        localRepository.find()
                    }
                }

private fun isConnected(): Single<Boolean> = Single.create { emitter ->
    val connectivityManager = application.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
    if (connectivityManager != null) {
        emitter.onSuccess(connectivityManager.activeNetworkInfo.isConnected)
    } else {
        emitter.onError(RuntimeException("cannot get connectivity"))
    }
}

Android Gradle Plugin3系におけるマルチモジュール時のconfigurationの解決について

こんにちは。釘宮です。

マルチモジュールなプロジェクトをやって行く上で、以前は下記のような記述をよく書いていました。

devCompile project(path: 'library', configuration: 'dev')

Android Gradle Plugin3系(gradle4系)から compile(など)ではなく implementation(など)を使っていきましょうともろもろ変わりました。

なので、単純に下記のように implementation と書き直してみます。

devImplementation project(path: 'library', configuration: 'dev')

これでビルドをして見ると、がっつりエラーが出てきます

解決方法などをまとめたので今回記事として見ました。

ここに書いていあることは 公式ページ に書いてあることの一部 + αでdす。

結論から言うと implementation project('library') で良い (条件付き)

app/build.gradle library/build.gradle
android {
  :
  flavorDimensions 'api'
  productFlavors {
    dev {
      dimension 'api'
    }
    stg {
      dimension 'api'
    }
    prod {
      dimension 'api'
    }
  }
  :
}
dependencies {
  :
  implementation project(":library")
  :
}
android {
  :
  flavorDimensions 'api'
  productFlavors {
    dev {
      dimension 'api'
    }
    stg {
      dimension 'api'
    }
    prod {
      dimension 'api'
    }
  }
  :
}

このようになっている場合、appではlibraryで同じ名前のflavorのものを選択してbuildされます。

いままでは

devCompile project(path: ':library', configuration: 'dev')
stgCompile project(path: ':library', configuration: 'stg')
prodCompile project(path: ':library', configuration: 'prod')

などと書いてましたが、下記一行で済むようになりました。便利ですね。

implementation project(":library")

ただこれに関しては app側にある api-dimension に所属する dev, stg, prod をlibrary側も同じものを持っているという条件を満たしている時に限ります。

そのため、library側が余計に dev1 というflavorを持っていても使われないだけでビルドは問題なく通ります。

以下から簡略化のため、 api-dimensionに所属する dev, stg, prod のflavorがある 状態を [api => (dev, stg, prod)] と書くようにしますね。

例1. appでは[api => (dev, stg, qa, prod)]、libraryでは[ api => (dev, stg, prod)]となっている時

app側に qa があるけど library側にそれがない場合です。

appの qa 時には library では stg を選択したいとします。

こう言う時は matchingFallbacks を使用します。

app/build.gradle library/build.gradle
android {
  :
  flavorDimensions 'api'
  productFlavors {
    dev {
      dimension 'api'
    }
    stg {
      dimension 'api'
    }
    qa {
      dimension 'api'
      matchingFallbacks = ["stg"]
    }
    prod {
      dimension 'api'
    }
  }
  :
}
dependencies {
  :
  implementation project(":library")
  :
}
android {
  :
  flavorDimensions 'api'
  productFlavors {
    dev {
      dimension 'api'
    }
    stg {
      dimension 'api'
    }
    prod {
      dimension 'api'
    }
  }
  :
}

依存先に対象flavorが見つからない場合に matchingFallbacks によって優先順位を指定できます。 上記の例では stg のみ指定しいます。 (なお、 flavor dimention が一致していないと、matchingFallbacks に指定しても動きません)。 加えてこのmatchingFallbacks は下記のようにbuildTypeでも同様の使い方ができます。

app/build.gradle library/build.gradle
android {
  :
  buildTypes {
    debug {
    }
    stg {
    }
    qa {
      matchingFallbacks = ["stg"]
    }
    release {
    }
  }
  :
}
dependencies {
  :
  implementation project(":library")
  :
}
android {
  :
  buildTypes {
    debug {
    }
    stg {
    }
    release {
    }
  }
  :
}

例2. library側に知らないdimensionがある時

そもそもdimensionとは

あえて触れてこなかったんですが flavorDimensions 'api' のところの話です。

Android Gradle Plugin3.0.0より前ではなかったものです。 いままでもありましたが、3系からは指定が必須になったようです。

いままでは productFlavors とはフラットなものでして、あるflavorと他のflavorを組み合わせるということはできませんでした。これを可能にするのが flavorDimension です。

言葉だと難しいので実際に設定して見ましょう。

productFlavors {
    buildTypes {
      debug
      release
    }
    flavorDimensions 'api', 'persistence'
    dev {
        dimension 'api'
    }
    stg {
        dimension 'api'
    }
    prod {
        dimension 'api'
    }
    local {
        dimension 'persistence'
    }
    cloud {
        dimension 'persistence'
    }
}

flavorDimension を指定しない場合、 buildVariant は (dev, stg, prod, local, cloud) × (debug, release) の 5×2 = 10通りのconfigurationがとなっていました。

これが、 現状は上記のようにdimensionを使い分けることで (dev, stg, prod) × (local, cloud) × (debug, release) の 3×2×2 = 12通りconfigurationが作られるようになったと言うことです。

何が便利かと言うと、観点ごとにコードを分けれることでしょうか。上記の場合だと api は (dev, stg, prod) のどの環境を使うか、永続化(persistence)は ローカルなのかクラウドなのかという例になっています。

ざっくりここまでがdimensionの説明です。

appは [api => (dev, stg, prod)]、 libraryが [api => (dev, stg, prod), persistence => (local, cloud)] の時

appの知らない persistence dimension がlibraryにある場合を考えます。

appで、例えば stg をbuildしたい時に、 library側では stg と言っても stgLocalstgCloud の組み合わせがあるのでどっちを使えば良いのかわからないと言う状況です。;

この場合は missingDimensionStrategy を使用します

missingDimensionStrategy (dimension名) (flavor名), (flavor名),...

と言うふうに 知らないdimensionがきた時に、どのflavorを優先的に採用するかということを決めることができます。

missingDimensionStrategy 'persistence' 'local' 'cloud'

この場合は persistence dimensionがきたら local, cloud の順で採用すると言うことになります。

これを踏まえると下記のようになりました。

app/build.gradle library/build.gradle
android {
  :
  defaultConfig {
    :
    missingDimensionStrategy 'persistence', 'local', 'cloud'
    :
  }
  flavorDimensions 'api'
  productFlavors {
      dev {
          dimension 'api'
          missingDimensionStrategy 'persistence', 'local'
      }
      stg {
          dimension 'api'
          missingDimensionStrategy 'persistence', 'cloud'
      }
      prod {
          dimension 'api'
          missingDimensionStrategy 'persistence', 'cloud'
      }
  }
  :
}
dependencies {
  :
  implementation project(":library")
  :
}
android {
  :
  flavorDimensions 'api', 'persistence'
  productFlavors {
      dev {
          dimension 'api'
      }
      stg {
          dimension 'api'
      }
      prod {
          dimension 'api'
      }
      local {
          dimension 'persistence'
      }
      cloud {
          dimension 'persistence'
      }
  }
  :
}

上記のように各Flavorごとに missingDimensionStrategy を設定することができます。

まとめ

これからは configuration の解決は dependencies のところではなく flavor名、 matchingFallbacksmissingDimensionStrategy で頑張りましょう。

キャリアパスどうするねん問題

釘宮です。こんにちは。

この記事は SHIROBAKO Advent Calendar 2017 の 二日目の記事です。昨日のkonifarの 武蔵野アニメーション総務 興津由佳の左眼について という記事からスタートしました。 ベスト・オブ・興津 はあれ一択ですよね。さすが、わかってらっしゃる。

さて。

キャリアパス悩みますよね。

なのでSHIROBAKOを見てみました。

なぜかというと、SHIROBAKOの主題を一言で表すと(浅はかかもですが)、 みゃーもりのキャリアパスどうするねん問題 に尽きるからです。1話から最後の方までのほとんど全ての要素がみゃーもりの「私って何をしていきたいんだっけ?」のヒントや刺激となっていて、最終話でみゃーもりが結論をだすいうのがSHIROBAKOの大きな流れなんです。

改めてSHIROBAKOを見ることで何かしら自分もヒントを見つけられないかなと感じて見直した次第です。

「飛ぶ意味ってなんなのかな?」

〈20話 - がんばりマスタング〉での会議での出来事でした。

この時みゃーもりの務める武蔵野アニメーションでは第三飛行少女隊というアニメを制作しております。

最終回が近づくにつれ、あるトラウマ的な事件をきっかけに主人公のパイロットであるアリアは飛ぶことをやめてしまいます。

しかし監督はじめ制作スタッフ一同はアニメの最終回では、なんとかそのトラウマを乗り越えてなんとか飛んでもらいたい。だけど原作ではまだアリアはそれを克服していない上、作者に連絡も取れない。その中でみゃーもりがぽつりと

宮森  「飛ぶ意味ってなんなのかな?」

と漏らし、そこから話が広がっていきます。

宮森  「飛ぶ意味ってなんなのかな?」

舞茸  「それは我々にとってなんでアニメを作るのか? みたいなことじゃないの?」

宮森  「あ、そっか!それだったらわかります!」

舞茸  「え、宮森さんはなんで作ってんの?」

宮森  「いや、質問の意味はわかるってだけで。作る意味とかは全然。」

:

宮森  「監督はなんでアニメ作ってるんですか?」

監督  「なんとなく?でも、やめられないんだよね。なんでかな」

舞茸  「僕だって、意味とかたいそうなことを考えてないよ。明日の締め切りだけこなしてたら何十年も経っちゃっただけでさ」

なべP  「なんでこんな仕事についちゃったかなぁ」

葛城  「僕は刺激かな。みる人に刺激を与えたいし、作ることで自分も刺激を受けたい」

この議題は、会議が終わった後も各所で広がっていきます。

りー  「自分にはやっぱり物語が必要なんだなって思うんす」

矢野  「面白いじゃないじゃないですか?」

安藤  「好きだから、以外にないっすね」

山田  「自分がここにいるっていうのを確認するため」

円   「次の作品をつくりたいから、今の作品を作ってるのかな」

佐藤  「(一丸となって完成を目指す会社の雰囲気が)好きだからかもしれません」

ここで終わると思いきや、次話 〈21話 - クオリティを人質にすんな〉のアバンにて各キャラクターの アニメを作り続ける理由 が一気に流れます。

木佐  「んー自己表現ってやつ?」

落合  「最終的におれがみたいものをつくりたいんだよ」

富ヶ谷 「ペクサーみたいな会社を日本に作りたいんだよ。いつか独立してやる」

荒川  「こどものころから塗り絵とかすきだったんだよね」

堂本  「好きなこと続けてるだけ」

河野  「だんだん面白くなってやめられなくなっちゃったんだよな」

大倉  「10才の少年がずぅっと俺の心のなかに住んでんダヨォ(酔いながら)」

久乃木 「さっがっ!」

f:id:kgmyshin:20171201141230j:plain

出典: http://ponpokonwes.blog.jp/archives/24023907.html

こんなにあるのかってくらいたくさんありますね。

ベテランか新人によっての偏りはあんまりなさそうでしたが、パターン分けはできそうです。

  1. なんとなく (なんで続けてるかはわからない)
  2. 好きだから
  3. 自己実現

(何気に久乃木の性が渋かっこいい)

これをエンジニアである自分自身に落とし込んでみると、 今までは〈なんとなく〉〈好きだから〉続けてられていたように思います 。それが、いつのまにか徐々にそれだけではモチベーションが保てなくなってきているようになってしまった。そしてそれゆえに「キャリアパス悩む」問題に直面しているのかなぁと思うわけです。

「安原さんはなんでアニメーターになろうと思ったの?」

〈8話 - 責めてるんじゃないからね〉にて我らが杉江大先生と僕の絵麻の会話にてドンピシャな会話をしています。

杉江  「安原さんはなんでアニメーターになろうと思ったの?」

絵麻  「絵を描くのが好きだからです」

絵麻  「それだけじゃいけませんか」

杉江  「好きな事をして食べていけるのは幸せなことだ。だけどそのうち描くだけじゃ物足りなくなってくる

杉江  「何か新しい目標が必要になってくるんだ」

絵麻  「目標……」

杉江  「世界中の子供たちが笑顔になってくれたら……僕はそう思いながら描いている」

絵麻  「私はそんな大それたこと全然思えません」

f:id:kgmyshin:20171201143514j:plain

出典: http://kagano443.blog.fc2.com/?no=792

擦れた絵麻も可愛いですがそこは飲みの席で語るとして、自分もまさに 物足りなくなってきてるのかなぁ と。

そういえば先の「なんでアニメを作り続けているのか?」の回答に、ちゃんと新しい目標を持ってる回答がいくつかありました。

逆に〈好き〉だけで長年やれてる人もいるので、やっぱり 物足りなくなるのかどうかは人による 、そして 僕は物足りなくなってしまった側の人なんだな と思いました。

「目先のことばかり考えてる時期はもう終わりだよ」

SHIROBAKO主人公みゃーもりは、初めから悩んでるのもあって 新しい目標を見つけないといけない側 だと考えられます。それを後回し後回しとしてきた結果とうとう最終回にて、、

ロロ  「これからどうしたいのか決まった?このままアニメを作りたいのか。作りたいとしたらなぜなのか?」

宮森  「んと、これからゆっくり考える」

ロロ  「あまったれんなー!」

ロロ  「目先のことばかり考えてる時期はもう終わりだよ

ロロ  「そろそろ少し高いところから遠くを見る時がきたんだよ」

f:id:kgmyshin:20171201153717j:plain

出典: http://anicobin.ldblog.jp/archives/43988621.html

そして宮森は結論を出すわけですが、気になる方はぜひ1話から見てみてくださいませ!!!!!!

また自分の話に戻すのですが 目先のことばかり考えてる時期はもう終わりだよ というセリフにはぐさっと心臓を刺されました。。。

ということで自分も後回しにせずにじっくり考えて見ました。

いきなり過程をすっ飛ばしますと、自分はやっぱり ものづくり をしたいんだなぁと。それもただ作っていたいわけでなく、売り上げがどうこうとかじゃなくて、 ひたすらユーザーへの価値を第一に考えて、ほぼほぼそれのみKGI・KPIにおいて磨いてたら、売り上げもついてきたみたいなものづくりをしてサービスを育てたい。それをいくつも。 というのが本当にやっていきたいことだなぁと。

もちろん、ただの夢、理想かもしれません。でも、 これを実現したいがゆえにエンジニアしてるんだなぁと思うわけです。僕の場合は。

以下、所信表明です。

会社でそういうことをやるのが難しいし、いきなり起業だ!!って気は全くなく、こじんまりと少人数でそういう開発をやっていけたらと考えています。

まずは12月中にアイデアだしからこつこつやって1月からは早速開発を。

デザイン力だけ自分にはないので、上記のようなものづくりに共感できる人いたらこそっと言ってもらえるとすごくありがたいです!!!!

結論をまとめるとキャリアパスとしては、 自分が作りたい環境で作ってそのやり方でサービスが育つことを確認したい。(そしてそれを何回も繰り返したい。再現性あるものにしたい。) となります。

ただ、完全に会社で働く意味性が完全に抜けてるのでここはまた考えます。

「たどり着きたい場所がはっきりすると、やるべきことが見えてくるんだなぁ」

f:id:kgmyshin:20171201160627j:plain

やってくぞ!!!

まとめ

  • キャリアパスに悩むなら SHIROBAKO を見てみよう。至言がそこにたくさんある。
  • ユーザーファーストなこじんまりとした開発をやってくぞ
  • デザイナーさん、やってきましょう?

それでも見つからないあなたへ

SHIROBAKOの舞台である、ずかちゃんのバイト先の 「柗亭」。 この居酒屋は実在しており、月1ペースで言っているのですが、そこのトイレ飾られている 生きるのが楽になる まいにち蛭子さん(日めくり) , (日めくり)まいにち、修造! も結構な至言に溢れているので、柗亭に行くかカレンダーを買うのをオススメします。

Rxを使えば非同期か同期かを意識しなくてよくなるというのはどういうことか

この件、多少説明した方がいいのではと思ったので記事にしました。

一応Androidの話です。題材はなんでもよかったんですがリポジトリパターンを題材としてみました。

あらかじめ言っておくと、コードの細かいツッコミは置いておいてもらえると。伝えたいのは 非同期か同期かを意識しなくていいとはどういうことか です。

Rxのなかった世界のころ

非同期がゆえに...

こういうリポジトリのinterfaceを実装したいと考えます。

interface CourseRepository {
  fun resolveById(id: CourseId): Course

  fun resolveAll(): List<Course>
}

でも実際は CourseAPIで取得するので実装は下記のようにならざるを得ないわけです。 (blockingGetすればええやんって思う人もいるかもしれませんが、Androidではmain threadでhttp通信したらCrashします)

class CourseRepositoryImpl {
  
  val service = Executors.newSingleThreadExecutor()
  
  fun resolveById(id: CourseId): Future<Course> =
    service.submit(Callable {
          return HTTP通信してResponseをCourseに変換したもの
    })
  

  fun resoveAll(): Future<List<Course>> =
    service.submit(Callable {
          return HTTP通信してResponseをList<Course>に変換したもの
    })  
  
}

そのため、泣く泣く 外部に提供するinterfaceを

interface CourseRepository {
  fun resolveById(id: CourseId): Future<Course>

  fun resolveAll(): Future<List<Course>>
}

のように Future で包んであげなければなりません。

キャッシュしてみる

HTTP通信したものをキャッシュする実装を考えます。

class CourseRepositoryImpl : CourseRepository {
  
  val service = Executors.newSingleThreadExecutor()
  val cache = Cache() // なんらかのCacheクラス
  
  fun resolveById(id: CourseId): Future<Course> =
    service.submit(Callable {
          if (cache[id] == null) {
            val course = HTTP通信してResponseをCourseに変換したもの
            cache[id] = course
            return course
          } else {
            return cache[id]
          }
    })
   :
}

Future型で返さないといけないのもあって、キャッシュを使うときも非同期用のスレッドで実行されます。無駄ですがしょうがないです。

ほかのRepositoryを実装してみる

次に下記のRepositoryを実装しようと考えます。

interface LevelRepository {
  fun resolveById(id: LevelId): Level

  fun resolveAll(): List<Level>
}

Level関連のデータは同期で取得できるのでこのままのinterfaceで良しとします。

改めて先の CourseRepository と見比べて見ましょう。

interface CourseRepository {
  fun resolveById(id: CourseId): Future<Course>

  fun resolveAll(): Future<List<Course>>
}

実装を隠すために interface きってるのに内部実装が非同期なんだなというのが丸わかりです。

そしてわかるだけならともかく resolveById とか似たような名前なのに使い方は全く違います。LevelRepository.resolveById では呼び出せばそのまま値が返ってきます。それに対して CourseRepository.resolveById は Thread & Handler 使うなりして非同期で取ってきたCourseなどをMainThreadでつかうようにする処理が必要になってしまします。

返り値の型を見ればわかるのですが、おなじ resolveById でまったく使い方が異なるので命名でもしっかり分けるようにします。今回は非同期の時は request というprefixをつける運用にします。

interface CourseRepository {
  fun requestResolveById(id: CourseId): Future<Course>

  fun requestResolveAll(): Future<List<Course>>
}

元の理想形からは遠くなりました。ただそれでも、名前だけでmain threadに戻さないといけないんだなってのはわかるので最高ではないですが以前よりはbetterでしょう。

betterな選択肢を選んできたつもりですが、以上では解決できないしょうがない課題がいくつか残りました。

  1. interfaceを使う側が非同期か同期かで(interfaceの内部実装の都合によって)いちいち処理を変えないといけない
  2. 無駄に非同期処理にせざるを得ない箇所がでてくる

いっそLevelRepositoryの各メソッドも全てFutureで包めばいいやという解決策もありですが、その場合はより課題 ( 2 ) の方が深刻になってきます。

Rxを導入する

Rxを導入します。この場合は全て Single や Maybe などの Rxの型で包む運用とします。

interface CourseRepository {
  fun resolveById(id: CourseId): Maybe<Course>

  fun resolveAll(): Single<List<Course>>
}
interface LevelRepository {
  fun resolveById(id: LevelId): Maybe<Level>

  fun resolveAll(): Single<Level>
}

これによって先の課題2つをまるっと解決できます。

interfaceを使う側が非同期か同期かでいちいち処理を変えないといけない問題について。

まず、interfaceを使う側は XXXRepository.resoveById(id).observeOn(uiScheduler).subscribe() とすれば、interfaceの内部実装が非同期だろうが同期だろうが常に処理をすれば済みます。もちろんinterfaceの内部実装が非同期か同期かを知るすべはありませんし、意識する必要もありません。

次に無駄に非同期処理にせざるを得ない箇所がでてくる問題について。

これについても subscibeOn によって処理スレッドを実装時に指定できるのでオールおっけーです。

例えば先ほどのキャッシュありの実装は下記のようになります。

class CourseRepositoryImpl : CourseRepository {
  
  fun resolveById(id: CourseId): Maybe<Course> =
    if (cache[id] == null) {
      return Single.create(HTTP通信してResponseをList<Course>に変換する処理)
        .doOnSuccess(キャッシュに保存)
        .toMaybe
        .observeOn(ioScheduler)    // 非同期用のスケジューラを設定
    } else {
      return Maybe.just(cache[id]) // 非同期用のスケジューラは設定しなくて良い
    }
    :
}

処理によって選択することができるので無駄に非同期処理みたいなことはなくなります。

まとめ

  • Rx(など)を使わなかったら下記の課題がある
    • interfaceの各メソッドの内部処理によって使う側が非同期か同期かを考えて実装しなくていけなくて辛い
    • 必要のない非同期処理がが入ってしまうことがある
  • Rxを使うことで処理によって、使う側が非同期か同期を知らずに実装できる (一部運用は必要)

kotlinのcoroutineが出てきて、ExecutorやThread Handlerを使うのが面倒という課題があってRxを使ってた人と、この記事に書いたような課題も感じてRxを使ってた人でギャップがあるなぁと感じた昨今です。

coroutineは確かに素晴らしいですが、上記のような課題を解決(するためのもの)/(できるもの)ではないので、自分の場合は Rx を coroutine に置き換えるというのが表現として厳しいなぁと思った次第であります。

coroutineがダメだといういう気はなく、Rxのここが良いんだよというのが伝われば幸いです。

追記

申し訳ありません。kagurazakaさんの方法で確かにできそうです!

ただ、このツイートのメンションで話してますが、 coroutineでは非同期前提のものを同期でも扱えるのに対し、Rxは非同期同期関係なく扱えるみたいな違いはあるのかなと。 そして今回のケースは自分的にはやはり後者の方が用途としては妥当なのかなと思いました。まぁでもkurokawaさんのいうように気持ち良さとかのレベルの話であるのは違いないです。

ツイッターでうまく議論できない子なので、できれば今度対面でいろんな方と話したいなと思った次第です!ぜひお話しさせてくださいませ!

ドメインオブジェクトをクライアントも持つべきなのか考えてみた

きっかけ

尊敬するエンジニアの一人 @nobuoka さんのつぶやきがきっかけ。

常々自分も同様のことを考えていて、基本的には賛成。

しかし実際の開発においては 「(複雑な場合 は持たないとつらいなぁ)」 という実感があり、話してみるとnobuoka先生も同様の実感をもっているもよう。

今回はその 複雑な場合 とは実際どんな場合なのかを考えてまとめてた。

注意

この手の議論は燃えやすいので、しっかり前置きしておきます。

ここに書いてある考えが絶対的に正解とは自分自身考えてないです。こう言う風に考えてますと言う段階です。なにか引っかかること、疑問や多少議論してみたいなどあれば @kgmyshin までメンション投げてもらえれば、応えられるときは応えます!

ただし、SNSで議論することをあまり好んでないので、がっつり議論したい方はぜひ弊社に入社をば!一緒に楽しく議論しながら開発していきましょう!!

そもそもの前提となる自分の考え

たとえば、コンテキストマップが下記のようになっているサービスを考える。

f:id:kgmyshin:20171016172248p:plain

このサービスをクライアントをつくるとなった時コンテキストマップはどうなるか。

システムが全く違うので下記のように一つのコンテキストと捉える場合もあると思う。

f:id:kgmyshin:20171016172653p:plain

ただ、クライアント自身が意味のあるコンテキストを持っている訳ではないので、一つのコンテキストとして扱うのではなく必要なコンテキストを写した像のように考えた方が筋が良いのではないかと考えている。

f:id:kgmyshin:20171016174020p:plain

クライアント自身が意味のあるコンテキストを持っている訳ではないからだ。

各コンテキストを拡張して一つのシステムにおいたものがクライアント だと考えている。

ドメインモデル持つべきか?

ここからが本題。どんな場合に持つべきなのか整理して行く。

持つべき場合

サーバーの構成を知らない場合

f:id:kgmyshin:20171016175648p:plain

サーバーの構成やコンテキスト関係がどうなっているわからない場合はクライアントでもドメインモデルを持つべきである。

そもそもサーバー側の構成をクライアントが知る必要はないという意見も正であると思う ので、この理由ひとつだけで 持つべき としてもいいかもしれない。

コンテキストの統合がクライアント内で行われる場合

f:id:kgmyshin:20171016180258p:plain

コンテキストの統合をクライアント内で行う場合がよくある。この場合はドメインモデルをクライアントで作らざるを得ない。

また複数にコンテキストをまたがる場合、コンテキストの統合はエンハンスして行く中で頻繁に発生しうるので初めから各コンテキストでドメインモデルを持っておくべきかもしれない。

オフラインでも動くことを保証される場合

f:id:kgmyshin:20171016180642p:plain

オフライン時、上記のようにサーバー側がなくても動くことを求められているのでドメインモデルはクライアント側でも持たなければならない。

持つべきでない場合

逆に持つべきでない場合に触れておく。

クライアントでは一つのコンテキストしか扱わない

f:id:kgmyshin:20171016182409p:plain

この場合は基本的に単純なクライアントとなるのでドメインオブジェクトを作る必要性はないでしょう。

単純なRSSリーダーなどがここに当てはまると思う。

まとめると...

いろいろ書いたが、 複数のコンテキストにまたがるアプリを作る場合は コンテキストの統合が後々発生した場合に対応するために、 クライアントにもドメインオブジェクトを持つべき 、という判断基準が個人的には妥当だと考えている。 (もちろん例外はあるが)

つまるところ、上に書いた〈複雑な場合〉はもう少し具体的に言うと〈複数のコンテキストにまたがるアプリを作る場合〉だったということになる。