クソPR(Pull Request)をやっつけろ
本ブログ1年ぶりのポストは、私が所属するPTAのアドベントカレンダー6日目の記事です。 PTAについては、1日目の会長の記事をご覧ください。 前回は、同僚の@hyperdashによる、AppleScriptでなくすmicro-toilという記事でした。
私について
今年の前半はABEMAの広告プレースメントシステム、後半は営業支援システム(UU試算、在庫管理、申込・入稿など)の開発チームで、主にEMとPMを担当。
本記事について
今年はじめ頃に、レビューがちゃんと機能していないと感じて社内に展開した記事をちょっと加筆・修正したものです。私なりの、レビューってこういうものだよねっていう考えと、レビューを出す際に心得て欲しいことを記載しています。特に会社に入って初めてチームで、仕事でコードを書く新卒1年目のエンジニアなどに読んで欲しい内容です。
本題
↑スライド中に出てくるクソPRはこちら
補足・余談
- "Fear, and loathing In ~" は、本来後ろに地名(ラスベガスをやっつけろっていう映画が有名ですね)か人名(18禁)しかこないらしいのですが、細かいことは気にしない
- 異なる観点からのチェックのところの例は正直微妙だと思っている(言いたいのは、例えば他の業務システムを担当している人とかの視点でレビューしてもらうと考慮漏れとかが見つかるかもよ、ってことです)
- コミットログにwhyやhowの件で、howは例えば「コードジェネレータを使ってコードを生成」とか、「テンプレートからコピペ」とかを想定している(どうやってそのコードを作ったか)
- whyのところは、なぜその変更が必要なのか?ってことで、これがないとマジで積むことがある(変更していいのかどうか分からないため)
以上です。明日はMiyashita Yukiさんです!!
DDD(Domain Driven Development)でDDT(Dramatic Dream Team)を作る
本記事はドメイン駆動設計#1 Advent Calendar 2019の14日の記事です。 前回はryuseikurataさんの記事でした。
2019年もあとわずかになりました。今年は皇位継承や、多数の大物芸能人の婚約・結婚、個人的にも第一子が生まれるなど、とてもドラマティックな1年でした。
ちなみに、出産予定日だった3/29は会社を休んだのですが、その時、職場では会社を揺るがす大事件が起きていました。
/
— DDT ProWrestling (@ddtpro) 2019年3月29日
グループ企業にかこつけて #DDT路上プロレス 🙇
\
/
引っ越し祝いにかこつけて #DDT路上プロレス 🙇
\
/
真新しいオフィスにかこつけて #DDT路上プロレス 🙇
\
ぜひ歴史の目撃者になってください👀@AbemaTV で18:00から生中継!
視聴予約▷https://t.co/MD1Hi8xmYK#ddtpro pic.twitter.com/Br9798aQoE
職場ビルの倒壊は免れましたが、女子レスラーが同僚の机の上でロメロ・スペシャルを決めてて、机上のディスプレイが倒れそうになってましたw(セコンドの人が押さえて無事でした)
本題に戻ります。この記事は、DDDとチームビルディングというテーマで記載します。
軽く自己紹介
TD;LD
スクラムにDDDの開発手法やプロセスを導入するといいチームを作れます
前提:なぜチームで開発するのか?
「早く行きたければ一人で行け、遠くに行きたければ仲間と行け」という格言があります。チーム開発の狙いはまさにこれだと思ってます。スキルセットや成功(または失敗)体験の違うメンバーが知見を出し合うことでより高機能、高品質なシステムを作れます。しかしながら、昨今の世の中の変化の速度を鑑みると、より速く到達することも求められます。
チーム開発は並列処理と同じだと考えています。すなわち、個人作業の時間とメンバー間で同期をとる頻度と時間のバランスによって、開発速度が決まります。著者の経験上、この同期にかかる時間がボトルネックになることが多いです1。そこで、DDDの開発プロセスや成果物を導入することで同期にかかる時間を減らすことができます。
おさらい:DDDとは?
ユビキタス言語を用いたモデルを使って会話することで、円滑なコミュニケーションを実現すること が一番の目的だと考えています。それは、ビジ職とエンジニア職の間のコミュニケーションだけでなく、エンジニア同士のコミュニケーションの効率化も含まれます。
設計思想としてドメイン層を隔離するのも、ひいてはコミュニケーションの効率化のためだと考えています。究極にはドメイン層を隔離してビジネスロジックをまとめれば、ドメイン層のコードが仕様書の代わりになる(しかも動く!!)ので、仕様書の更新漏れから生じる確認作業とか、メンバー間での仕様の認識違いから解放されます。
以下、もう少し具体的にDDDがどのようにチームのコミュニケーション効率化に貢献するかを記載します。
同じ絵をインプットすることによる効率化
全く会話が噛み合わないと思ってた人との間に、共通の趣味や知人、似たような体験をしていることが分かると急に会話が弾む、という経験はないでしょうか。著者は開発案件に取り組むとき、まずは要件定義や画面イメージなどからドメインモデル図2というのを作って全員でレビューします。このプロセスには前述のような効果があると考えています。つまり、全く性質の違うメンバーが、まずは同じ絵を脳内にインプットすることでその後のコミュニケーションの質を上げられます。
設計と実装を分離できてレビューの質があがる
まれに「実装したから設計と実装どっちも見てくれ〜」、というプルリクエスト(以下PR)をもらうことがあり3、その度に「PRで設計のレビューするの無理じゃね?」と思ってます。設計のアウトプットと実装のアウトプットは違いますし、設計なら前述のモデル図やシーケンス図で議論した方がよっぽど効率的です。著者が実践するDDDのプロセスでは、まずはモデル図などの設計資料を作ってレビュー会を開きます。これにより設計レビューが効率化されます。また、設計についての共通認識ができれば実装後のレビュー観点を減らせるので、PRをマージするまでのリードタイムが削減できます。
モデル図があるとチケットを分離しやすく、フォローしやすい
例えば、XXXエンティティの実装をする、といったように、モデル図があればチケットを細かく発行することができます。チケットを細かく出せると、手の空いた隙間時間などに他のメンバーのフォローをしやすくなります(さらに、「この作業、どこまでやったっけ?」という確認のコミュニケーションも減らせます)
また、チケットが細かいと進捗やベロシティも把握しやすくなり、チームのふりかえりもより有意義になります。
まとめ
まとめるとチーム開発のボトルネックは仕様や設計、作業進捗を把握するための「同期のコスト」であり、一方でドメイン駆動設計の本質はユビキタス言語やドメインモデルを使ったコミュニケーションの効率化なので、スクラムと相性がよく、開発を加速できます。また、コミュニケーションが効率化すれば、ふりかえりの質も上がってより成果の出せるいいチームにしていくことができます。
ぜひお試しください。
エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)
- 作者:エリック・エヴァンス
- 出版社/メーカー: 翔泳社
- 発売日: 2011/04/09
- メディア: 大型本
Futureをネストしてハマった
あかんやつ
import scala.concurrent.{Await, Future} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.Duration object Main extends App { Await.ready(test(), Duration.Inf) def test(): Future[Unit] = { for { a <- Future { 1 } } yield { for { b <- Future { Thread.sleep(10000); a } } yield {} } } }
testの戻り値がFuture[Future[Unit]]になっているが、このバグをコンパイラが検知できず、内側のFutureの処理をまたずにプログラムが終了する。
def test(): Future[Unit] = { val a = for { ... a }
一度変数に詰めればコンパイルエラーになる。
Error:(17, 5) type mismatch; found : scala.concurrent.Future[scala.concurrent.Future[Unit]] required: scala.concurrent.Future[Unit] a
よって、こうした。
def test(): Future[Unit] = { val a = for { a <- Future { 1 } } yield { for { b <- Future { Thread.sleep(10000); a } } yield {} } a.flatten }
ドメイン駆動設計でトラッキングIDをどうするか
マイクロサービスっぽくサービスを分解していると、ログを串刺しでみたり分析したりするためにトラッキングIDを付与したいことがあるが、DDDの文脈からいうと、トラッキングIDはドメインの処理とは関係ないため、どうやって引回すか悩む。
例
↓のデータソースに渡す、TrackingIdをどうやって取得するか、という話です。
class SomeService { someEntityRepository: SomeEntityRepository someService(id: Id): Unit } class SomeEntity { id: Id someFunction(): SomeEvent } class SomeEvent { id: Id } interface SomeEntityRepository { findBy(id: Id): SomeEntity save(event: SomeEvent): Unit } class SomeEntityRepositoryImpl { entityDataSource: EntityDataSource eventDataSource: EventDataSource } class EntityDataSource { selectEntity(id: Id): (TrackingId, SomeEntity) postEvent(trackingId: TrackingId, event: SomeEvent): Unit } SomeService *-down-> SomeEntityRepository SomeService -down-> SomeEntity SomeEntity -down-> SomeEvent SomeEntityRepository <|-down- SomeEntityRepositoryImpl SomeEntityRepositoryImpl *-down-> EntityDataSource
object SomeService { def someService(id: Id): Unit { val entity = someRepository.findBy(id) val event = entity.someFunction someRepository.save(event) } }
案1:Entityに属性を追加して引回す
case class SomeEntity(id: Id, trackingId: TrackingId) { .... }
サービス層は変更ないですが、ドメイン層がTrackingIdに汚染される。ぶっちゃけTrackingIdだったらまぁって気がしてますが、EventDataSourceに引回す項目が増えてくると大変。
案2:サービス層で保存する
Repositoryを以下のようにして、サービス層で引回す
trait SomeEntityRepository { def findBy(id: Id): (SomeEntity, TrackingId) def save(event: SomeEvent, TrackingId): Unit }
Entityは汚染を防げるが、Repositoryは汚染される。EntityがクリーンならテストしやすいのでRepositoryが汚れてもいいかーという割り切りがありならあり。複数のエンティティを操作する場合に、TrackingIdが入れ替わらないように注意が必要。
案3:EntityDataSourceへの参照をキャッシュしてRepositoryで再度参照
これが一番良さそう。キャッシュの有効期限とか、キャッシュの排他制御が大変。 超雑に書くと↓、スレッドセーフではない。
object SomeEntityRepositoryImpl { val entityCache: TrieMap = TrieMap[Id, TrackingId] val entityDataSource: EntityDataSource = EntityDataSource val eventDataSource: EventDataSource = EventDataSource def findBy(id: Id): SomeEntity = { val res = entityDataSource.selectEntity(id) entityCache.put(res._1.id, res._2) res._2 } def save(event: SomeEvent): Unit = { val trackingId = entityCache.get(id)._2 eventDataSource.postEvent(event, trackingId) } }
SpringFrameworkなら、RequestScopeやJobScopeのオブジェクトが使えそう。
マイクロサービスつらいorz
過去のマイクロに振り切ろうとした(そして諦めた)経験を踏まえ、自分の考えるマイクロサービスのいいところとわるいところをまとめる。 多分、オライリーの本とかに同じようなことが書いてある気がする。
分ける対象
分ける対象は以下
以下それぞれ分けたときのメリット、デメリットをまとめる。
プロセスを分ける
Tomcatに複数サービスを相乗りさせているのを別々のサーブレットコンテナに、一つのバッチジョブで複数ステップ処理しているのを複数のジョブに分けるとか
メリット
- リリースしやすくなる
- 処理によってリソースを最適化できる
- スケーラビリティを確保できる(APIであればロードバランシング、ジョブであれば処理対象を分けて並列処理など)
- 部分的に再実行しやすい(ジョブを分けたパターン。フレームワークによっては失敗したステップから再実行できるものもある(例:SpringBatch))
デメリット
- 複数のサービスを同時にリリースしたい時にリリースのタイミングを合わせないといけない場合の手間
- 通信エラーのハンドリングのための作り込みが必要(データストア、HTTP通信など)
- プロセス間のデータのやりとりのための作り込みが必要(処理結果をJsonにシリアライズしてファイルに保存するなど。オブジェクト指向のプログラミングをしている場合は、型をシリアライズした先でどう表現すればいいか悩まされる)
- 前提となる処理のエラーや処理が遅延するケースの制御の作り込みが必要(前の処理が終わっていないのに後続が走り出すとマジでジーザスなことが多い)
- ACIDの実現のために作り込みが必要(RDBのトランザクション管理機能で実現してきたようなこと。一貫性についてはactivatorパターン - doilux’s tech blogというのを考えたこともある)
作り込みという文字を強調したのは、自分は作り込むことが死ぬほど嫌いだからです。作り込みが多ければ多いほど、バグが混入するリスクも多いし、運用保守の手間がかかるので。
ソースコードを分ける
一個のgitのレポジトリを、サービス毎に複数のレポジトリにわけるとか
メリット
デメリット
- ソースコードを追う時にあっちこっち参照しなきゃいけなくてめんどくさい
- ライブラリの管理が大変(脆弱性が見つかってバージョン上げる時とか)
- うまく共通部分を分離しないとコードが重複する
- 言語やフレームワークを変えられるがスキル(人間)がついていけないことが多い(教育コストがかかる。しかも開発人員だけではなく、運用保守人員にも)
- テストコードだけで完結しないテストが多くなる
データストアを分ける
DBMSをわけるとか
メリット
- 処理や管理するデータに応じてデータストアを選択できる(RDB、列指向DB、KVS...etc)
- 処理や管理するデータに応じて運用をカスタマイズできる(バックアップのポリシーなど)
- データへのアクセス権限を管理しやすい
デメリット
チームを分ける
システムの開発・運用保守するチームをサブモジュールごとにわける
メリット
- サービスに応じた開発プロセスを採用しやすい
- サービスに合わせた人材の採用・育成
- 共有コストが下がる(異なるチームは細部をしらなくていい)
デメリット
- コミュニケーションコストが上がる?下がる?(プロパー、委託社員の比率や会社の文化による)
結局どうなん
基本的には、分けると開発のしやすさと引き換えに運用コストや教育コストがかかると思っているので、コストに見合った価値を期待しているなら分けたほうがいいと思います。あと、最初から分けないほうがいいかと。後から分けるの大変じゃね?って意見もあると思うけど、運用コストが増えたわりに事業がスケールしないってこともあるので。
Lens便利
HoloLensの話ではない。scalaz.Lensの話。
これが
```
object Main extends App {
val a = A(B(C(D(1))))
println(a)
val copy = a.copy(a.b.copy(a.b.c.copy(a.b.c.d.copy(2))))
println(copy)
}
case class D(value: Int)
case class C(d: D)
case class B(c: C)
case class A(b: B)
```
こうなる
```
object Main extends App {
val a = A(B(C(D(1))))
println(a)
val AB = scalaz.Lens.lensu[A, B](
(a, b) => a.copy(b = b),
_.b
)
val BC = scalaz.Lens.lensu[B, C](
(b, c) => b.copy(c = c),
_.c
)
val CD = scalaz.Lens.lensu[C, D](
(c, d) => c.copy(d = d),
_.d
)
val DX = scalaz.Lens.lensu[D, Int](
(d, x) => d.copy(x = x),
_.x
)
val A2X = AB >=> BC >=> CD >=> DX
val copy2 = A2X.set(a, 2)
println(copy2)
}
case class D(x: Int)
case class C(d: D)
case class B(c: C)
case class A(b: B)
```
テストデータ作るのに便利
ScalaでprotoをJSONにする
そもそもなんでわざわざprotoをJSONにしているのかというと
- 重いバッチがあって、その中で一部の処理が重い
- 「一部の処理」を別バッチに切り出して並列処理をする
- 元のバッチのジョブと切り出したバッチのジョブの間でオブジェクトをやりとりしたい =>JSONにシリアライズしてデータストアに保存
- ↑のシリアライズのためのクラスを作る
- ↑をprotoにしておけば、もし将来、別のマイクロサービスとかに切り出した場合もすぐに対応できるじゃん
っていう経緯です。
依存ライブラリにscalaPBのscalapb-json4sを追加する。 scalapb.github.io
libraryDependencies += "com.thesamet.scalapb" %% "scalapb-json4s" % "0.7.0"
サンプル
message ProtoJsonTest { string a = 1; int64 b = 2; repeated string c = 3; repeated ProtoJsonTestSub sub = 4; } message ProtoJsonTestSub { string x = 1; }
object Proto2JsonTest extends App { val proto = ProtoJsonTest( "hoge", 1L, Seq("aaa", "bbbb"), Seq(ProtoJsonTestSub("ham"), ProtoJsonTestSub("egg")) ) val r: String = JsonFormat.toJsonString(proto) println(r) // {"a":"hoge","b":"1","c":["aaa","bbbb"],"sub":[{"x":"ham"},{"x":"egg"}]} val desProto = JsonFormat.fromJsonString[ProtoJsonTest](r) println(proto == desProto) // true }