Slide 1

Slide 1 text

1つのメソッドに、関心事が
 複数あると何が辛いのか?
 クリエーションライン株式会社
 アジャイルザムライ矢田
 スライドのリンクはX@yatakekeのポストから 


Slide 2

Slide 2 text

この発表の目的
 ターゲット
 ・モジュール設計の勘所が掴めていない開発者 
 ・ユニットテストの保守に苦労している開発者 
 
 アウトカム
 ・単一責任の原則を意識した設計が少しうまくなる 
 ・設計を意識することでテストの保守も楽になる 
 生成AIを使えばどうとでもなるのかもしれませんが、この発表のスコープには入っていません 
 2

Slide 3

Slide 3 text

今回の発表が生まれた背景
 ・自分の開発チームの若手開発者が実装に悩んでいたときのことを題材にして います。
 ・若手(新卒1年目程度)の開発者が書いたコードをもとに、本人の許可を
 経て発表用にアレンジしてあります。
 ・したがって、加工はしてありますが生々しさが少し残った微妙なコードになって いる気がします。
 3 コードのgistリンクは こちらから


Slide 4

Slide 4 text

自己紹介
 矢田進之介(やたしんのすけ)
 クリエーションライン株式会社
 スクラムディベロッパー
 自称アジャイルザムライ
 Python歴8年、Java歴2年
 JJUGは初参加 
 スクラムフェス三河実行委員長
 青いサムライである理由を知りたい方は、 こちらの動画をご覧ください
 4

Slide 5

Slide 5 text

今回のお題: 問題背景(gist)
 ● あなたはとある工場の生産管理システムの開発者です。
 ● 現在、あなたはそのシステムで顧客からの発注をもとに工場の製造品の生産計画 を作成する機能を作っています。
 ● 工場の生産ラインは平日のみ稼働しています。(一旦、祝日は考慮しません)また、 工場の生産キャパシティは考慮する必要はありません。
 ● あなたが実装する機能では、顧客からの注文(例、1-10日は150台納品)を稼働日 毎に均等に分割した計画を自動で作成します。
 ● 発注数量は10日ごとにまとめて送られます。(例、2月11日から20日で155台納品し てほしい)
 ● 端数は稼働日の初日から均等に割り振ってください
 5

Slide 6

Slide 6 text

今回のお題: データの例
 入力データ例 
 {
 "start_date": "2025-02-11", 
 "due_date": "2025-02-20", 
 "quantity": 155
 }
 出力データ例 
 [
 {"date": "2025-02-11", "quantity": 20}, 
 {"date": "2025-02-12", "quantity": 20}, 
 {"date": "2025-02-13", "quantity": 20}, 
 {"date": "2025-02-14", "quantity": 19}, 
 {"date": "2025-02-17", "quantity": 19}, 
 {"date": "2025-02-18", "quantity": 19}, 
 {"date": "2025-02-19", "quantity": 19}, 
 {"date": "2025-02-20", "quantity": 19} 
 ]
 6

Slide 7

Slide 7 text

入力データ例 
 {
 "start_date": "2025-02-11", 
 "due_date": "2025-02-20", 
 "quantity": 155
 }
 出力データ例 
 [
 {"date": "2025-02-11", "quantity": 20}, 
 {"date": "2025-02-12", "quantity": 20}, 
 {"date": "2025-02-13", "quantity": 20}, 
 {"date": "2025-02-14", "quantity": 19}, 
 {"date": "2025-02-17", "quantity": 19}, 
 {"date": "2025-02-18", "quantity": 19}, 
 {"date": "2025-02-19", "quantity": 19}, 
 {"date": "2025-02-20", "quantity": 19} 
 ]
 今回のお題: データの例
 7 端数は初日から割り当て 
 155/8 = 152…3


Slide 8

Slide 8 text

入力データ例 
 {
 "start_date": "2025-02-11", 
 "due_date": "2025-02-20", 
 "quantity": 155
 }
 出力データ例 
 [
 {"date": "2025-02-11", "quantity": 20}, 
 {"date": "2025-02-12", "quantity": 20}, 
 {"date": "2025-02-13", "quantity": 20}, 
 {"date": "2025-02-14", "quantity": 19}, 
 {"date": "2025-02-17", "quantity": 19}, 
 {"date": "2025-02-18", "quantity": 19}, 
 {"date": "2025-02-19", "quantity": 19}, 
 {"date": "2025-02-20", "quantity": 19} 
 ]
 今回のお題: データの例
 8 端数は初日から割り当て 
 155/8 = 152…3
 15,16日は土日のため 
 スキップ
 要約
 
 ・10日ごとに数百台の注文がくる 
 ・それを工場の作業日ごとに分割しなさい 
 ・ただし、作業日は平日 


Slide 9

Slide 9 text

みなさんなら 
 どう実装しますか? 
 TDDのToDoリスト/UML図/シーケンス図/メソッドの羅列など軽く書いてみてください 


Slide 10

Slide 10 text

実際に若手が書いていたコードの紹介(gist)
 public class TenDaysOrderCalculator { 
 public List dailySplitBy(TenDaysOrder tenDaysOrder) { 
 // 次ページ参照 
 }
 public Integer countWeekDays(LocalDate targetDate) { // 次々ページ参照 } 
 private boolean isWeekDay(LocalDate date) {
 return !(date.getDayOfWeek() == DayOfWeek.SATURDAY 
 || date.getDayOfWeek() == DayOfWeek.SUNDAY); 
 }
 }
 10

Slide 11

Slide 11 text

countWeekDays
 public Integer countWeekDays(LocalDate targetDate) {
 var count = 0;
 var date = targetDate;
 for (int i = 0; i < 10; i++) {
 if (targetDate.getMonth() != date.getMonth()) { break; }
 if (isWeekDay(date)) { count += 1; }
 date = date.plusDays(1);
 }
 return count;
 }
 11

Slide 12

Slide 12 text

dailySplitBy(gist)
 上記にgistのリンクを公開しているので そちらでみた方が良いです
 
 
 
 
 ※ちなみに31日のケースが考慮漏れしている のでわざと残してあります 
 12

Slide 13

Slide 13 text

分析してみると
 ・mainのメソッド(dailySplitBy)の行数は30行程度。
 ・メソッド分割されているが依存関係が生じていて変更コストが高い。
 dailySplitBy(TenDaysOrder) 
 CountWeekDays(LocalDate) 
 isWeekDay(LocalDate) 
 isWeekDay(LocalDate) 
 このメソッドの役割が複数
 13

Slide 14

Slide 14 text

単一責任の原則(Single Responsibility Principle)とは
 “モジュールを変更する理由はたったひとつだけあるべきである”
 
 14 Clean Architecture (Robert C. Martin) P81


Slide 15

Slide 15 text

このとき起きていた辛さ ~ユニットテスト編~
 dailySplitBy(TenDaysOrder) 
 CountWeekDays(LocalDate) 
 ・1月11日旬の数量が109だった場合_ 
  13日の日次が19になりそれ以外が18になる 
 
 ・2月21日旬の数量が5だった場合_ 
  28日の日次が0になりそれ以外が1になる 
 ・2025年1月11日旬の場合_平日の数が6になる 
 
 ・2026年2月21日旬の場合_平日の数が5になる 
 15

Slide 16

Slide 16 text

このとき起きていた辛さ ~ユニットテスト編~
 ・1月11日旬の数量が109だった場合_ 
  13日の日次が19になりそれ以外が18になる 
 
 ・2月21日旬の数量が5だった場合_ 
  28日の日次が0になりそれ以外が1になる 
 ・2025年1月11日旬の場合_平日の数が6になる 
 
 ・2026年2月21日旬の場合_平日の数が5になる 
 稼働日の算出と数量の割り当て の2つの目的があるので、 
 テストケースに 
 組み合わせ爆発が起きている 
 
 →ケース洗い出しが複雑化 
 →簡易的で似たようなケースを 使いがち 
 16

Slide 17

Slide 17 text

今後起こりうる辛さ ~仕様変更編~
 1週間後に以下の仕様変更が入りました(と、しましょう)
 → 「平日を稼働日として扱うのではなくて、カレンダーデータをDBに保持するのでその 情報を稼働日にしてほしいです。」
 17 dailySplitBy(TenDaysOrder) 
 CountWeekDays(LocalDate) 
 isWeekDay(LocalDate) 
 isWeekDay(LocalDate) 
 変更はここだけで済みそうだが… 


Slide 18

Slide 18 text

今後起こりうる辛さ ~仕様変更編(ユニットテスト)~
 1週間後に以下の仕様変更が入りました(と、しましょう)
 → 「平日を稼働日として扱うのではなくて、カレンダーデータをDBに保持するのでその 情報を稼働日にしてほしいです。」
 dailySplitBy(TenDaysOrder) 
 CountWeekDays(LocalDate) 
 ・1月11日旬の数量が109だった場合_ 
  13日の日次が19になりそれ以外が18になる 
 
 ・2月21日旬の数量が5だった場合_ 
  28日の日次が0になりそれ以外が1になる 
 ・2025年1月11日旬の場合_平日の数が6になる 
 
 ・2026年2月21日旬の場合_平日の数が5になる 
 18

Slide 19

Slide 19 text

今後起こりうる辛さ ~仕様変更編(ユニットテスト)~
 1週間後に以下の仕様変更が入りました(と、しましょう)
 → 「平日を稼働日として扱うのではなくて、カレンダーデータをDBに保持するのでその 情報を稼働日にしてほしいです。」
 dailySplitBy(TenDaysOrder) 
 CountWeekDays(LocalDate) 
 ・1月11日旬の数量が109だった場合_ 
  13日の日次が19になりそれ以外が18になる 
 
 ・2月21日旬の数量が5だった場合_ 
  28日の日次が0になりそれ以外が1になる 
 ・2025年1月11日旬の場合_平日の数が6になる 
 
 ・2026年2月21日旬の場合_平日の数が5になる 
 修正範囲は全部
 19 関心事が混在するとテストの修正が辛くなることもある 


Slide 20

Slide 20 text

米久保さんの講演
 20 https://docswell.com/s/tyonekubo/ZP21V1-unit-testing-basic#p21

Slide 21

Slide 21 text

一番伝えたいこと
 ・モジュール設計とテスト設計は切り離せない関係
 ・モジュール設計の質はテスト設計に影響する
 21

Slide 22

Slide 22 text

なぜこうなってしまったのか
 方針をみるとissueには以下のようなToDoが書かれていた
 
  [ ] 平日かどうかを判定する
 
  [ ] 指定された10日間の平日の数を計算する
 
  [ ] 指定した10日間と数量を渡すと日バラシにする
 
 22

Slide 23

Slide 23 text

なぜこうなってしまったのか
 このTodoがそのままメソッドに反映されていた
 
  [ ] 平日かどうかを判定する
   → isWeekDay
  [ ] 指定された10日間の平日の数を計算する
   → countWeekDay
  [ ] 指定した10日間と数量を渡すと日バラシにする
   → dailySplitBy
 23

Slide 24

Slide 24 text

なぜこうなってしまったのか
 このTodoがそのままメソッドに反映されていた
 
  [ ] 平日かどうかを判定する
   → isWeekDay
  [ ] 指定された10日間の平日の数を計算する
   → countWeekDay
  [ ] 指定した10日間と数量を渡すと日バラシにする
   → dailySplitBy
 やりたいのは ここ
 24

Slide 25

Slide 25 text

なぜこうなってしまったのか
 このTodoがそのままメソッドに反映されていた
 
  [ ] 平日かどうかを判定する
   → isWeekDay
  [ ] 指定された10日間の平日の数を計算する
   → countWeekDay
  [ ] 指定した10日間と数量を渡すと日バラシにする
   → dailySplitBy
 やりたいのは ここ
 重要そうでテストが書きやすそうな 
 メソッドで切り分けていた
 25 考えやすそうと変更しやすいは違う 


Slide 26

Slide 26 text

より良い設計のために問題設定をふりかえる
 ● あなたはとある工場の生産管理システムの開発者です。
 ● 現在、あなたはそのシステムで顧客からの発注をもとに工場の製造品の生産計画 を作成する機能を作っています。
 ● 工場の生産ラインは平日のみ稼働しています。(一旦、祝日は考慮しません)また、 工場の生産キャパシティは考慮する必要はありません。
 ● あなたが実装する機能では、顧客からの注文(例、1-10日は150台納品)を稼働日 毎に均等に分割した計画を自動で作成します。
 ● 発注数量は10日ごとにまとめて送られます。(例、2月11日から20日で155台納品し てほしい)
 ● 端数は稼働日の初日から均等に割り振ってください
 26

Slide 27

Slide 27 text

問題設定を構造化してみる
 生産計画を作成する
 ・入力は顧客からの注文データ 
  ・平日と休日に関係ない10日毎の台数 
 ・稼働日毎に均等に分割した生産計画を出力する 
  ・工場が稼働する日の計算 
   ・工場の生産ラインは平日のみ稼働している
   ・祝日は考慮しなくても良い
  ・稼働日ごとの生産量の計算 
   ・合計台数を稼働日から均等に割り振る
   ・端数は初日から均等に割り振る
 2月11日から20日で155台 
 11,12,13,14, , ,17,18,19,20日 
 20,20,20,19, , ,19,19,19,19個 
 27

Slide 28

Slide 28 text

問題設定を構造化してみる
 生産計画を作成する
 ・入力は顧客からの注文データ 
  ・平日と休日に関係ない10日毎の台数 
 ・稼働日毎に均等に分割した生産計画を出力する 
  ・工場が稼働する日の計算 
   ・工場の生産ラインは平日のみ稼働している
   ・祝日は考慮しなくても良い
  ・稼働日ごとの生産量の計算 
   ・合計台数を稼働日から均等に割り振る
   ・端数は初日から均等に割り振る
 2月11日から20日で155台 
 11,12,13,14, , ,17,18,19,20日 
 20,20,20,19, , ,19,19,19,19個 
 ポイントはこのインターフェース
 28

Slide 29

Slide 29 text

要件の流動性を考える
 ・もしも祝日を考慮するようになったら
 ・もしもDBにある独自のカレンダーを稼働日として扱うようになったら
 ・もしもT社カレンダーを使うようになったら
 ・もしも海外のカレンダーを使うようになったら
 稼働日毎に注文を割り当てることを考慮すると、
 範囲内に稼働が何日あるかではなく、何日が稼働日である で切り分ける
 29

Slide 30

Slide 30 text

役割が分離できた設計
 interface WorkingDaysCalculator { 
 public List calculate(
 OrderDate orderDate 
 );
 }
 interface OrderQuantityAllocator { 
 public List allocate(
 List workingDays, 
 OrderQuantity quantity 
 );
 }
 与えられた日にちから
 稼働日を割り出す
 稼働日のリストに対して数量を
 均等に割り当てる
 30

Slide 31

Slide 31 text

どんなテストケースが実装できそうか
 ・与えられた日にちから稼働日を割り出す
   ・2025年1月11日であれば13-17日と20日のリストを返す
   ・2025年2月21日であれば21日と24-28日のリストを返す
   ・2024年2月21日であれば21-22日と25-29日のリストを返す
 ・稼働日のリストに対して数量を均等に割り当てる
   ・稼働日が2日で4個の場合2個ずつ割り当てる
   ・稼働日が5日で11個の場合初日のみ3個で残りは2個ずつ割り当てる
   ・稼働日が5日で4個の場合最終日のみ0個で残りは1個ずつ割り当てる
 31

Slide 32

Slide 32 text

どんなテストケースが実装できそうか
 ・与えられた日にちから稼働日を割り出す
   ・2025年1月11日であれば13-17日と20日のリストを返す
   ・2025年2月21日であれば21日と24-28日のリストを返す
   ・2024年2月21日であれば21-22日と25-29日のリストを返す
 ・稼働日のリストに対して数量を均等に割り当てる
   ・稼働日が2日で4個の場合2個ずつ割り当てる
   ・稼働日が5日で11個の場合初日のみ3個で残りは2個ずつ割り当てる
   ・稼働日が5日で4個の場合最終日のみ0個で残りは1個ずつ割り当てる
 32 仮に稼働日計算の仕様変更が入っても 
 このテストだけ修正すればよい 


Slide 33

Slide 33 text

最後に良い設計を考えるためのポイント紹介
 ・ユースケースから仕様変更を考えながら変更を最小限に抑えられそうなインターフェー スを考える
 
 ・具体的な英語に直したときに動詞が複数存在していないか
  → dailySplitByは本当であればcalculate workdays and allocate orders to them 
 
 
 33 まだまだいっぱいあるとは思いますが 


Slide 34

Slide 34 text

まとめ
 ・メソッドの切り出し ≠ 関心事の分離
 ・メソッドレベルでもうまく分離できないと辛い点
  → テストケースで組み合わせ爆発が起きて、ケースを考えるのが複雑になる
  → 仕様変更が入ったときにテストコードの 修正コストが大きくなるときもある
 ・流動性を考慮して適切なインターフェースを設計するとコードとテストの可読性が向上 する
 34

Slide 35

Slide 35 text

End
 35

Slide 36

Slide 36 text

時間があったら余談
 36

Slide 37

Slide 37 text

そのとき自分が書いたコード
 interface TenDaysOrderCalculator {
 public List calculateWorkingDays(
 OrderDate orderDate
 );
 public List> allocateQuantity(
 List workingDays,
 OrderQuantity quantity
 );
 }
 37

Slide 38

Slide 38 text

単一責任の原則から外れているけど大丈夫なのか
 自分の考え
 → このレベルで分離できていれば簡単な修正でなりたい姿に辿り着けるのでOK
 
 もしも仕様変更が入ったら最初にやるリファクタリング
 ① クラスの複製(もとのクラスをclassA, 複製クラスをclassBとする) 
   classBの名前をOrderQuantityAllocatorにする 
 ② classAをWorkingDaysCalculatorにrenameする(IDEの機能を使う) 
 ③ classAのallocateWorkinを使っている部分をclassB参照にする 
 工事中
 38

Slide 39

Slide 39 text

どう実装されるかではなく、何を実行するか
 オブジェクトが実行することに着目することで、早すぎるタイミングでの実装の詳細につ いて考慮する必要もなくなります。つまり、実装の詳細を隠蔽できるようになるのです。 そして、将来的に発生する修正が容易に行えるようなソフトウェアを作り出すことも、やろ うと思えばできるようになるのです。
 by オブジェクト指向のこころ P105
 ここではざっくり言うとメソッドレベルで
 大枠を考えようくらいに捉えてください
 流動的要素のカプセル化と
 呼ばれたりしますが
 39

Slide 40

Slide 40 text

関心の分離とは
 定義: プログラムを複数の「関心(関心事)」に分け、それぞれを独立して扱えるようにす る設計原則。
 関心とは: UI、ビジネスロジック、データアクセス、ログ処理、バリデーションなど、異なる 種類の目的や役割のこと。
 
 今回の発表では、メソッドレベルでもプログラムの目的が複数ある実装を
 「関心の分離ができていない」プログラムとして定義します。
 40