Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

ログラスについて

Slide 5

Slide 5 text

ログラスについて

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

理解できるとは?

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

ドメインモデリングの位置付け ビジネス的な一次情報 ● ユーザーヒアリング ● ユースケース ドメインモデリング ● ドメインモデル図 仕様・テストケース ● 仕様 ● テストケース ● 受入基準 ↑ ビジネスとシステムをつなぐ 最も重要なプロセス

Slide 17

Slide 17 text

ドメインモデリングのサンプル /** * コメント */ 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, ) ドメインモデル を元に実装する データの実例を併記しておくとわかりやすい

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

責務 例えば、ユースケース層では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) } }

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

テストに関するコメント ● 本質的なテストコード以外を小さくし、何をテストしているのかわかりやすく する @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で何をした結果 どうなるかを明確化

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

日本語変数の活用 ● 英語にすると馴染みがなくて可読性が低くなるような場合は日本語変数も アリ ○ テストコードなどでは有用 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) }

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

真似して欲しくないもの // 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)) } このレイヤの責務 でないものを明示

Slide 32

Slide 32 text

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