Slide 1

Slide 1 text

2025/05/28 社内勉強会 SOLID原則ではじめる よりよい設計の第一歩 1

Slide 2

Slide 2 text

2 ● オブジェクト指向でよりよい設計を目指す ● 基本方針としてSOLID原則の解説 ● 実装例を多めに紹介 What is it?

Slide 3

Slide 3 text

3 ● この資料を作り始めるタイミングでぴっ たりの本を見つけた ● 今回はほぼこの本の紹介です ○ 書籍の実装例はCrystal ○ 本発表ではJava 参考資料

Slide 4

Slide 4 text

4 よい設計とは?

Slide 5

Slide 5 text

● 要件を満たすシステムの構造 を計画・構築する ○ アーキテクチャ(インフラ/ソフトウェア) ○ クラス ○ メソッド ● 品質を担保する 5 ソフトウェア設計

Slide 6

Slide 6 text

6 ● 機能の修正が簡単 ● 機能の追加が簡単 ● 修正範囲が明確 ● 影響範囲が明確 ● テストがしやすい ● リリーススピードが早い ● バグの原因追求がしやすい よい設計の指標(一例) 「よい設計」は状況によって異なる 例えば保守しない案件では、 コードの読みやすさや バグの原因追求のしやすさは 優先度が低いかもしれない

Slide 7

Slide 7 text

7 ● 機能の修正が簡単 ● 機能の追加が簡単 ● 修正範囲が明確 ● 影響範囲が明確 ● テストがしやすい ● リリーススピードが早い ● バグの原因追求がしやすい 今回考える「よい設計」の指標

Slide 8

Slide 8 text

8 ● 「機能の追加が簡単になるように実装するぞ!」 ● 何から着手する? どうやって「よい設計」を実現するのか? ・・・

Slide 9

Slide 9 text

9 ● 「機能の追加が簡単になるように実装するぞ!」 ● 何から着手する? ● ゴールに向かうために、 もう少し具体的な指針が必要 ● そのひとつが「SOLID原則」 どうやって「よい設計」を実現するのか?

Slide 10

Slide 10 text

10 SOLID原則

Slide 11

Slide 11 text

11 ● オブジェクト指向で用いられる五つの原則の頭字語 ● ソフトウェア設計をより平易かつ柔軟にして保守しやすくするもの ● Robert C. Martin が提唱した SOLID原則とは? Robert C. Martin またの名をボブおじさん(Uncle Bob)

Slide 12

Slide 12 text

12 SOLID原則 略称 英語名称 日本語名称 SRP Single Responsibility Principle 単一責任の原則 OCP Open-Closed Principle 開放閉鎖の原則 LSP Liskov Substitution Principle リスコフの置換原則 ISP Interface Segregation Principle インターフェース分離の原則 DIP Dependency Inversion Principle 依存性逆転の原則

Slide 13

Slide 13 text

13 ● クラスや関数は、ただひとつの機能について責任を持つ ● どこまでをひとつの機能とするべきかは状況による ○ 例)ファイルの読み書きを分けるか? 単一責任の原則 ● Single Responsibility Principle ● Open-Closed Principle ● Liskov Substitution Principle ● Interface Segregation Principle ● Dependency Inversion Principle 変更するための理由が、一つのクラスに対して一つ以上あってはならない いい言葉!

Slide 14

Slide 14 text

14 ● 拡張(機能追加)するときに既存コードを修正するべきでない ● すごくざっくり言うと、ポリモーフィズムを駆使して if文を減らそうって話 開放閉鎖の原則 ● Single Responsibility Principle ● Open-Closed Principle ● Liskov Substitution Principle ● Interface Segregation Principle ● Dependency Inversion Principle ソフトウェアの実体(クラス、モジュール、関数など)は、 拡張に対して開かれているべきであり、修正に対して閉じていなければならない

Slide 15

Slide 15 text

15 ● サブクラスは親クラスの代わりとして使えるべき ● 主に継承に適用される原則 ● かなり理論的な原則で、もう少し細かい定義がある ● 今日はあまり扱いません リスコフの置換原則 ● Single Responsibility Principle ● Open-Closed Principle ● Liskov Substitution Principle ● Interface Segregation Principle ● Dependency Inversion Principle ある基底クラスへのポインタないし参照を扱っている関数群は、その派生クラスの オブジェクトの詳細を知らなくても扱えるようにしなければならない リスコフさん→

Slide 16

Slide 16 text

16 ● インターフェースはこの機能を保証します、という「約束」 ● 保証できない機能の実装を強制してはならない ● 小さいインターフェースを組み合わせる インターフェース分離の原則 ● Single Responsibility Principle ● Open-Closed Principle ● Liskov Substitution Principle ● Interface Segregation Principle ● Dependency Inversion Principle 汎用なインターフェースが一つあるよりも、 各クライアントに特化したインターフェースがたくさんあった方がよい

Slide 17

Slide 17 text

17 ● 依存とは「使うこと」 ○ 使う側が使われる側に依存する ● より安定したものに依存しましょう ○ 安定=変更が少ない ○ 下位より上位が安定 ○ 具象より抽象が安定 依存性逆転の原則 ● Single Responsibility Principle ● Open-Closed Principle ● Liskov Substitution Principle ● Interface Segregation Principle ● Dependency Inversion Principle 上位モジュールはいかなるものも下位モジュールから持ち込んではならない。 双方とも具象ではなく、抽象(インターフェースなど)に依存するべきである

Slide 18

Slide 18 text

18 各原則の関係 原則 他の原則との関係 SRP 根底となる原則。適切な責務分割で修正範囲を限定する。 OCP クラス設計で目指す形。各クラスの関係性を工夫し 変更容易性を上げる。 LSP OCPのためのテクニック。サブクラスを交換可能にする方法。 ISP OCPのためのテクニック。クラス間のインターフェース定義の方法。 DIP OCPのためのテクニック。クラス間の依存方向をコントロールする方法。 1. SRPでクラスの責務をひとつにし、修正が他のクラスに及ばないようにする 2. LSP/ISP/DIP を用いて、修正・追加が容易なOCPを目指す

Slide 19

Slide 19 text

19 単一責任、開放閉鎖は基礎的・ わかりやすいので人気 依存性逆転は難しくてかっこいいの で好まれる (私も投票した) 余談:好きな原則

Slide 20

Slide 20 text

20 実装例

Slide 21

Slide 21 text

21 ● 設定を表現するクラス ○ ApiConfig:APIの設定を 保持しているクラス ○ DbConfig:DBの設定を 保持しているクラス ○ RedisConfig:Redisの 設定を保持しているクラス 本発表で扱う問題 ● 設定クラスを利用するクラス ○ Api:外部APIを呼び出すクラス ○ Db:DBからデータ取得するクラス ○ Redis: Redisからデータ取得するクラス ● データを取得して処理するクラス ○ UserRepository: Api/Db/Redis などのクラスを 使ってデータを処理する ユーザーの情報を 外部APIやDBから取得するシステム

Slide 22

Slide 22 text

22 問題設定 ● API、DBそれぞれにjson形式の設定ファイルがある ● 設定ファイルの中身は、どちらもidとpasswordがある わるい設計 ● 設定ファイルのidとpasswordを返すConfigManager ● idとpasswordを取得したいのは同じなので、ConfigManagerとして共通 化 1. 単一責任の原則( SRP) api_config.json db_config.json 変更するための理由が、一つのクラスに 対して一つ以上あってはならない

Slide 23

Slide 23 text

23 1. 単一責任の原則 typeに渡す文字列で 処理を分岐 idとpasswordの 取得は共通 DBクラスは省略 Main.java

Slide 24

Slide 24 text

24 1. 単一責任の原則 Main.java

Slide 25

Slide 25 text

25 1. 単一責任の原則 APIクラスからConfigManagerク ラスへの依存 Main.java

Slide 26

Slide 26 text

26 1. 修正が伝播する ○ 片方の責務の修正によって、もう片方にも修正が必要になる 2. 可読性とテスト容易性が低い ○ 可読性が悪く、テストが書きづらい わるい設計の問題点 変更するための理由が、一つのクラスに 対して一つ以上あってはならない

Slide 27

Slide 27 text

27 問題はConfigManager 変更理由としてあり得るもの ● api_config.jsonの形式が変わる ● db_config.jsonの形式が変わる これはSRPに違反している 修正が伝播する 変更するための理由が、一つのクラスに 対して一つ以上あってはならない

Slide 28

Slide 28 text

28 修正が伝播する api_config.jsonのkey名が 変更されたら、、、 ConfigManagerにも修正が 必要になって、、、 db_config.jsonに修正が 伝播する! 変更するための理由が、一つのクラスに 対して一つ以上あってはならない

Slide 29

Slide 29 text

29 可読性・ テスト容易性が低い 変更するための理由が、一つのクラスに 対して一つ以上あってはならない ● typeの値によってConfigManagerの意味が変わる ○ 処理を追わないと何が起こるかわからない ○ 可読性が低い状態 ● 機能追加で、typeの種類が増えるかも ○ あり得るパターンが増えると その動作を担保するテストも必要になる

Slide 30

Slide 30 text

30 1. プロパティが多い 2. クラスの責任を端的に言えない 3. プロパティで条件分岐する 4. テストが書きづらい 5. 修正が伝播する 単一責任の原則違反のにおい

Slide 31

Slide 31 text

31 1. プロパティが多い 2. クラスの責任を端的に言えない 3. プロパティで条件分岐する 4. テストが書きづらい 5. 修正が伝播する 単一責任の原則違反のにおい プロパティが多い場合、次の可能性が 高い ● 2つ以上の責任を表現しようとして いる ● プロパティを媒介に修正が伝播す る

Slide 32

Slide 32 text

32 1. プロパティが多い 2. クラスの責任を端的に言えない 3. プロパティで条件分岐する 4. テストが書きづらい 5. 修正が伝播する 単一責任の原則違反のにおい 幅広い意味を持つ名前のクラスは、様々な 責任を負わされる 「設定を管理する(ConfigManager)」はそ の典型例 もう一段階具体的に説明できないか 考えるべし

Slide 33

Slide 33 text

33 1. プロパティが多い 2. クラスの責任を端的に言えない 3. プロパティで条件分岐する 4. テストが書きづらい 5. 修正が伝播する 単一責任の原則違反のにおい flagやtypeなどのプロパティを持ち、 条件分岐しているもの プロパティによって「やりたいこと」が異な る →つまり責任が異なる 単一責任の原則にしたがってクラスを分 離する

Slide 34

Slide 34 text

34 1. プロパティが多い 2. クラスの責任を端的に言えない 3. プロパティで条件分岐する 4. テストが書きづらい 5. 修正が伝播する 単一責任の原則違反のにおい 複数の責任があると、テストケースが膨 大になる メソッドもシンプルになっているはずな ので、テスト自体も複雑にならない

Slide 35

Slide 35 text

35 1. プロパティが多い 2. クラスの責任を端的に言えない 3. プロパティで条件分岐する 4. テストが書きづらい 5. 修正が伝播する 単一責任の原則違反のにおい 複数の責任が一つのクラスに集まって いる 異なる責任の間でプロパティやメソッド を共有できてしまう ある責任に対する修正が、別の責任を 扱う箇所にも伝播する

Slide 36

Slide 36 text

36 責任にあわせてクラスを分割する よい設計への修正 設定ファイルを扱うクラスから条件分岐が消えた 設定を個別に変更でき、修正が伝播しない

Slide 37

Slide 37 text

37 責任にあわせてクラスを分割する よい設計への修正 依存関係も整 理された

Slide 38

Slide 38 text

38 問題設定 ● UserRepositoryは、データを取得してユーザー一覧を返す ● データの取得元にはAPIとDBがあり、切り替えられるようにする わるい設計 ● UserRepositoryに、APIクラスとDBクラスのプロパティを持たせる ● どちらを取得元にするかは、コンストラクタで設定する 2. 開放閉鎖の原則( OCP) ソフトウェアの実体は、拡張に対して開か れているべきであり、修正に対して閉じて いなければならない

Slide 39

Slide 39 text

2. 開放閉鎖の原則( OCP) 39 2種類の コンストラクタ APIクラスは省略 処理を分岐 Main.java

Slide 40

Slide 40 text

2. 開放閉鎖の原則( OCP) 40 UserRepositoryから API、DBへの依存 Main.java

Slide 41

Slide 41 text

41 1. 修正の影響を受ける範囲が広い 2. 追加の影響を受ける範囲が広い わるい設計の問題点 ソフトウェアの実体は、拡張に対して開か れているべきであり、修正に対して閉じて いなければならない

Slide 42

Slide 42 text

42 試しに、Db.selectAllUser()をDb.getUserList()に変更してみる 修正の影響を受ける 範囲が広い ソフトウェアの実体は、拡張に対して開か れているべきであり、修正に対して閉じて いなければならない UserRepositoryにも修正が伝 播してしまった!

Slide 43

Slide 43 text

43 API、DBの他に、Redisからもデータを取得したくなり、クラスを追加 追加の影響を受ける 範囲が広い ソフトウェアの実体は、拡張に対して開か れているべきであり、修正に対して閉じて いなければならない UserRepositoryにも修正が必要 =修正が閉じていない

Slide 44

Slide 44 text

44 インターフェースを導入 よい設計への修正 UserRepositoryから 条件分岐が消えた

Slide 45

Slide 45 text

45 インターフェースを導入 よい設計への修正 各データ取得元は インターフェースを実装 UserRepositoryは インターフェースに依存

Slide 46

Slide 46 text

データ取得元の修正や追加の影響が、UserRepositoryに及ばない 46 よい設計のメリット DBクラスの 関数名を変更しても、 DbAdapterで吸収できる Redisクラスを 追加する場合、 DataStoreInterface を実装していればOK

Slide 47

Slide 47 text

47 OCPを実現するテクニックの一つ 書籍での紹介もOCPのおさらいなので、今回は割愛 3. リスコフの置換原則 (LSP) ある基底クラスへのポインタを持つ関数 群は、その派生クラスの詳細を知らなくて も扱えるべき

Slide 48

Slide 48 text

48 問題設定 ● APIを実行するApiクラス、DBから データを取得するDbクラス ● それぞれの接続情報を ApiConfig、DbConfigに持つ わるい設計 ● 各Configクラスは ConfigInterfaceを実装 4. インターフェース 分離の原則( ISP) 一つの汎用なインターフェースより、 特化したインターフェースがたくさんあっ た方がよい

Slide 49

Slide 49 text

49 4. インターフェース 分離の原則( ISP) 一つの汎用なインターフェースより、 特化したインターフェースがたくさんあっ た方がよい どちらもConfigInterfaceを使用 ApiはgetId()を利用、DbはgetUser()を利用 getPassword()は共通 Main.java

Slide 50

Slide 50 text

50 4. インターフェース 分離の原則( ISP) 一つの汎用なインターフェースより、 特化したインターフェースがたくさんあっ た方がよい APIの設定ファイル DBの設定ファイル 設定を扱う共通の インターフェース 使わないメソッド

Slide 51

Slide 51 text

51 fatなインターフェースを媒体にして修正が伝播してしまう わるい設計の問題点 一つの汎用なインターフェースより、 特化したインターフェースがたくさんあっ た方がよい ①DbConfigのgetUser()を getUserName()に変更してみる ②DbのgetUser()がgetUserName()に変わる ②ConfigInterfaceのgetUser()が getUserName()に変わる ③ApiConfigのgetUser()が getUserName()に変わる!

Slide 52

Slide 52 text

責任ごとにインターフェースを分離する DB側の修正がAPI側に伝播しない 無理な共通化は悲劇の始まりです 見極めが重要(難しいけど、、) よい設計への修正 一つの汎用なインターフェースより、 特化したインターフェースがたくさんあっ た方がよい 52 分離と共通化って真逆でしょ? 共通化したほうがいいって 聞いたけど

Slide 53

Slide 53 text

53 問題設定 ● UserRepositoryは、APIでデータを取得してユーザー一覧を返す わるい設計 ● UserRepositoryに、APIクラスのプロパティを持たせる 5. 依存性逆転の 原則(DIP) 上位モジュールは下位モジュールから 何も持ち込んではならない 具象ではなく、抽象に依存するべき

Slide 54

Slide 54 text

5. 依存性逆転の 原則(DIP) 上位モジュールは下位モジュールから 何も持ち込んではならない 具象ではなく、抽象に依存するべき 54 Main.java

Slide 55

Slide 55 text

● DIPは依存の方向をコントロールする手法 ● 右のクラス図だけをみても、 設計の良し悪しを議論できない ● 議論のために、次の前提を導入する UserRepositoryはApiに依存しているので、Apiの変更はUserRepositoryに 伝播する 逆に、UserRepositoryの変更はApiに伝播しない わるい設計の問題点 上位モジュールは下位モジュールから 何も持ち込んではならない 具象ではなく、抽象に依存するべき 55 ● Apiの修正が頻繁に行われたり、他のクラスと頻繁に交換される

Slide 56

Slide 56 text

Apiが変更されたとしても、UserRepositoryは変更したくない 基本方針 ● 利用する側(UserRepository)の都合でインターフェースを定義 ● 利用される側(Api)はそのインターフェースを実装 よい設計への修正 上位モジュールは下位モジュールから 何も持ち込んではならない 具象ではなく、抽象に依存するべき 56

Slide 57

Slide 57 text

上位、下位のモジュールを考えてみる より抽象度の高いUserRepositoryを上位 より詳細な処理を行うApiを下位 上位モジュールで下位モジュールが利用されている =下位モジュールを持ち込んでいる状態 より詳細な処理を扱う下位モジュール(具象)に依存している よい設計への修正 上位モジュールは下位モジュールから 何も持ち込んではならない 具象ではなく、抽象に依存するべき 57

Slide 58

Slide 58 text

UserRepository(上位)の都合でインターフェースを定義し、 Api(下位)がそれを実装する Apiの変更が UserRepositoryに伝播しない よい設計への修正 上位モジュールは下位モジュールから 何も持ち込んではならない 具象ではなく、抽象に依存するべき 58 上位→下位の依存だった設計が 下位→上位の依存になった =依存性が逆転した 抽象(上位・インターフェース)に依存 している

Slide 59

Slide 59 text

よい設計への修正 上位モジュールは下位モジュールから 何も持ち込んではならない 具象ではなく、抽象に依存するべき 59 UserRepositoryとApiは インターフェースに依存 下位が上位に依存

Slide 60

Slide 60 text

● よい設計の指標は様々で、今回はクラス設計について、機能追加や修正 の容易さについて議論した ● SOLID原則は、よい設計の指標を実現するための考え方の一つ ● SRPでクラスの責務をひとつにし、修正が他のクラスに及ばないようにす る ● LSP/ISP/DIP を用いて、修正・追加が容易なOCPを目指す 60 まとめ

Slide 61

Slide 61 text

61 ● DRY原則 Don't Repeat Yourself ○ すべての知識はシステム内において、単一、かつ明確な、そして信頼 できる表現になっていなければならない ● KISSの原則 Keep It Simple, Stupid ○ 単純さを追求し、複雑さを避けること ● YAGNI原則 You Ain't Gonna Need It ○ 本当に必要になるまで作らない方がよい おまけ:その他の設計原則