Upgrade to Pro — share decks privately, control downloads, hide ads and more …

リーダブルコード by DDD / Readable Code by DDD

リーダブルコード by DDD / Readable Code by DDD

リーダブルコード by DDD
モデリングを起点に可読性の高いコードを実現する

Yoshiki Iida

July 07, 2021
Tweet

More Decks by Yoshiki Iida

Other Decks in Programming

Transcript

  1. モデリングを起点に可読性の高いコードを実現する
    2021/07/07 #readablelt Yoshiki Iida
    リーダブルコード by DDD

    View Slide

  2. Yoshiki Iida (@ysk_118)
    エンジニアに始まり、スクラムマスター、プロダクトオーナー、マネージャー、執行
    役員を経験し、現場のチームビルディングから部署を超えた会社全体の改善な
    ど、アジャイルな組織づくりの推進を行ってきました。現在は株式会社ログラスに
    てソフトウェアエンジニアとしてプロダクト開発に携わっています。
    書籍「Scrum Boot Camp The Book 増補改訂版」コラムニスト。
    一般社団法人アジャイルチームを支える会 理事。
    $ whoami

    View Slide

  3. ログラスについて
          は、事業進捗を可視化することで
    柔軟で高精度な経営推進を実現する
    プランニング・クラウドサービスです。

    View Slide

  4. ログラスについて

    View Slide

  5. ログラスについて

    View Slide

  6. ● コードがリーダブルであることとDDDの関連
    ● 実践プラクティス
    Topic

    View Slide

  7. コードがリーダブルであるとは?

    View Slide

  8. コードがリーダブルであるとは?
    他の人が最短時間で理解できること

    View Slide

  9. 理解できるとは?

    View Slide

  10. 理解できるとは?
    ● 例えば、あるクラスが何をしているか理解できるとは?
    ○ 何をしているのかわかる
    ■ 状態/振る舞い
    ○ 依存関係がわかる
    ■ どこから呼ばれているか
    ■ 何を呼んでいるか
    ○ どんなビジネス的な意味を持っているかわかる

    View Slide

  11. 理解できるとは?
    ● 例えば、あるクラスが何をしているか理解できるとは?
    ○ 何をしているのかわかる
    ■ 状態/振る舞い
    ○ 依存関係がわかる
    ■ どこから呼ばれているか
    ■ 何を呼んでいるか
    ○ どんなビジネス的な意味を持っているかわかる
    ここまでは時間をかけて
    コードを読み込めば
    なんとかなるかも
    これはコードだけでは
    わからないかもしれない

    View Slide

  12. 理解できないとどうなるか?
    ● 実装の手戻り
    ● 実装の歪み
    ○ その後の変更可能性を著しく下げる
    ● 品質の低下
    ○ 不十分なテスト観点
    ● 低品質の再生産
    ○ コピペによる増殖

    View Slide

  13. システムを正しく作るためにはどちらも重要
    実装の理解しやすさ
    実装の目的の理解しやすさ

    View Slide

  14. システムを正しく作るためにはどちらも重要
    実装の理解しやすさ
    実装の目的の理解しやすさ
    → DDDというアプローチ

    View Slide

  15. ドメインモデリングから始まる実装
    実装の理解しやすさ
    実装の目的の理解しやすさ ドメインモデリング
    実装 & リファクタリング

    View Slide

  16. ドメインモデリングの位置付け
    ビジネス的な一次情報
    ● ユーザーヒアリング
    ● ユースケース
    ドメインモデリング
    ● ドメインモデル図
    仕様・テストケース
    ● 仕様
    ● テストケース
    ● 受入基準

    ビジネスとシステムをつなぐ
    最も重要なプロセス

    View Slide

  17. ドメインモデリングのサンプル
    /**
    * コメント
    */
    class Comment private constructor(
    val id: ID,
    val commentText: String,
    val isResolved: Boolean,
    val createdAt: DateTime,
    val updatedAt: DateTime,
    val createdUserId: ID,
    val commentType: CommentType,
    /** コメントの条件 */
    val condition: CommentCondition,
    ) {
    companion object {
    fun create(
    commentText: String,
    createdUserId: ID,
    commentType: CommentType,
    condition: CommentCondition,
    ): Comment {
    validationCommentTextLength(commentText)
    val createdAt = DateTime.now()
    return Comment(
    id = ID.gen(),
    commentText = commentText,
    isResolved = false,
    createdAt = createdAt,
    updatedAt = createdAt,
    createdUserId = createdUserId,
    condition = condition,
    commentType = commentType,
    )
    }
    }
    }
    /**
    * コメントの条件
    */
    data class CommentCondition(
    val departmentId: ID,
    val projectId: ID,
    val yearMonth: YearMonth,
    )
    ドメインモデル
    を元に実装する
    データの実例を併記しておくとわかりやすい

    View Slide

  18. 実装の目的の理解しやすさ
    ● ドメインモデル図があれば登場する概念を一目で把握できる
    ● ユースケースがあればそれらの概念がどう使われるのか理解できる
    ● そこから仕様を導き出すことができ、
    その仕様を検証するためのテストケースを作成することができる
    ここを最初に押さえていれば
    後からjoinする人も実装の目的は理解しやすい

    View Slide

  19. 余談: アンチパターン
    ● 実装の目的が失われてしまったシステムは変更難易度が桁違いになる
    ● 「なぜこの概念が必要なのか・・?」「この依存は必要なのか・・?」「この挙
    動でユースケースを満たしているのか・・?」
    ○ コードを考古学して答えが見つかれば良いが最終的にエスパーで妥
    協しなければいけないケースもありうる

    View Slide

  20. 実装の理解しやすさ
    ● 重要な要素
    ○ 責務
    ○ テスト
    ○ リファクタリング

    View Slide

  21. 責務
    責務とは、そのクラスが行うこと/そのクラスが表すもの
    ● 責務を1つに絞っていくことでリーダブルになること
    ○ クラスの関心ごとが小さくなり理解しやすくなる
    ○ テスト対象が明確になり、テストコード実装しやすくなる
    ○ テストがあるとリファクタリングしやすくなり、コードがさらに読みやすく
    なる

    View Slide

  22. 責務
    例えば、ユースケース層では1クラス1パブリックメソッドにしていくと見通しが良くなる
    class ProjectService(
    private val tenantSvc: TenantService,
    private val repo: ProjectRepository,
    ) {
    fun list(
    tenantId: ID,
    projectId: ID
    ): List {
    val projectList = ~~~
    return projectList
    }
    fun fix(
    tenantId: ID,
    projectId: ID
    ) {
    repo.fix(tenantId, projectId)
    }
    fun create(
    tenantId: ID,
    projectParam: ProjectParam,
    ): Project {
    return repo.create(tenantId, projectParam)
    }
    }
    class ListProjectUseCase(
    private val tenantSvc: TenantService,
    private val repo: ProjectRepository,
    ) {
    fun execute(
    tenantId: ID,
    projectId: ID
    ): List {
    val projectList = ~~~
    return projectList
    }
    }
    class CreateProjectUseCase(
    private val tenantSvc: TenantService,
    private val repo: ProjectRepository,
    ) {
    fun execute(
    tenantId: ID,
    projectParam: ProjectParam,
    ): Project {
    return repo.create(tenantId, projectParam)
    }
    }
    class FixProjectUseCase(
    private val tenantSvc: TenantService,
    private val repo: ProjectRepository,
    ) {
    fun execute(
    tenantId: ID,
    projectId: ID
    ) {
    repo.fix(tenantId, projectId)
    }
    }

    View Slide

  23. テスト
    リファクタリングしやすくなる。変更しやすくなる。テストは資産!
    ● テストに関するコメント
    ● テストデータの可視化
    ● 日本語変数を活用する

    View Slide

  24. テストに関するコメント
    ● 本質的なテストコード以外を小さくし、何をテストしているのかわかりやすく
    する
    @Test
    fun `インサートしたデータが削除できること`() {
    // given:
    val comment = createComment()
    dataCreators.comment.create(tenant.tenantId, listOf(comment))
    // 事前確認: 件数が一件取得される
    commentRepository.findById(tenant.tenantId, comment.id)
    .also { assertNotNull(it) }
    // when: ID指定して削除すると
    commentRepository.deleteById(tenant.tenantId, comment.id)
    // then: ID指定で取得できなくなる
    commentRepository.findById(tenant.tenantId, comment.id)
    .also { assertNull(it) }
    }
    データ準備はprivate
    method化
    when, thenで何をした結果
    どうなるかを明確化

    View Slide

  25. テストデータの可視化
    ● 大きなオブジェクトのテストデータはコメントなどで表現するのは限界がある
    ○ 割り切ってスプレッドシートリンクをテストコードに残す
    internal class ComparisonFlatTreeV3ViewTest {
    /**
    * テストデータの可視化 : https://docs.google.com/spreadsheets/d/1Gd_xxx---xx_XX/edit#gid=xx
    */
    @Test
    fun`対比表Viewの数値部分 _月毎`() {

    View Slide

  26. 日本語変数の活用
    ● 英語にすると馴染みがなくて可読性が低くなるような場合は日本語変数も
    アリ
    ○ テストコードなどでは有用
    val 当期純利益 = result.treeView.items.find { it.id == "XXXXXXXXXXXXX" }!!
    当期純利益.also { it ->
    val jan = it.data.find { it.periodKey == "2020-01" }!!
    assertEquals(expected = BigDecimal(10000), actual = jan.actual.sum)
    val feb = it.data.find { it.periodKey == "2020-02" }!!
    assertEquals(expected = BigDecimal(20000), actual = feb.actual.sum)
    val mar = it.data.find { it.periodKey == "2020-03" }!!
    assertEquals(expected = BigDecimal(30000), actual = mar.actual.sum)
    }

    View Slide

  27. リファクタリング
    絶え間なく行う
    ● 個人的によくやるもの
    ○ 責務を細かく分割する
    ○ プリミティブ型やタプルに対して名前をつけた型を用意する
    ○ SLAPでメソッドの粒度を揃える
    ○ ...

    View Slide

  28. その他
    ● 見た目の美しさ&統一はIDEとlinterに任せる
    ● 命名はガイドライン化し認知コスト・思考コストを下げる
    ● 参照実装のガイドライン化
    ○ 真似して欲しいものとしてまとめておく
    ● コメント
    ○ 真似して欲しくないものをしっかり書く

    View Slide

  29. 命名のガイドライン化
    命名がブレやすいものは認知コストにな
    るためガイドライン化。
    変更したければPullRequestベースで
    更新していく。

    View Slide

  30. 真似して欲しいもの
    オンボーディング資料に参照実装として
    積極的に真似して欲しい実装をまとめて
    ある

    View Slide

  31. 真似して欲しくないもの
    // TODO: TestXxxFactory実装に寄せる
    private fun mockDepartment(id: String) = Department.of(
    id = ID.from(id),
    tenantId = tenantId,
    code = Code.of(id),
    name = Name.of(id),
    externalSystemDepartmentCode = null
    )
    新たに汎用的な仕組みを作ったので
    古い書き方がコピペされないように
    機械的にTODOを埋め込む
    override fun insert(tenantId: ID, account: Account) {
    // TODO: バリデーションはドメイン層に委譲
    if (account.accountType.isAggregationType())
    throw BadRequestException("科目種別はSTANDARD_PL_AGGRには設定できません
    ")
    insert(listOf(account))
    }
    このレイヤの責務
    でないものを明示

    View Slide

  32. ● 変更しやすいシステムの特徴
    ○ リーダブルであること
    ■ 実装を理解しやすいこと
    ■ 実装の目的を理解しやすいこと
    ● 実践アプローチ
    ○ ドメインモデリングは実装の目的理解にダイレクトに有効
    ○ 責務を意識し、テスト・リファクタを回していくことで
    結果的にリーダブルになっていく
    ○ 参照実装などを充実させることでチーム開発がより効率化
    まとめ

    View Slide