はじめての設計

2020 T.Takeda

設計について

設計とは

  • 品質を上流で押さえる」ための行為
  • 上流工程の重要タスク
graph LR 要求 --> 設計 設計 --> 実装設計 実装設計 --> 実装
💡退屈な言い方をするなら、「要件を実装可能な状態にする作業」

設計の範囲

要求からビジネスモデルを定義づける設計と実現するために必要な実装設計は分けて考える

  • 本資料で単に「設計」といったときは前者を示す

設計のこつ

  • 制約の強いものから検討する
  • 図を活用してビジネスロジックを明確にする
  • 要求で現れない部分を考慮する
  • 実装に依存する部分を切り離す
💡設計ではビジネスの本質を押さえることを意識する
💡将来的に実装方法が変わったとしてもそのまま使える設計がよい設計

よい設計

  • シンプル
  • 流れが追いやすい
  • 役割が明確
  • MECE

誰にでも誤解なくわかる
= 誰でも理解できる状態

わるい設計

  • 無設計
  • 過度な抽象化
  • 役割が混在
  • 無駄/重複がある

書いた人にだけわかる
= 設計した人しか理解できない

💡技術的に優れていても、過度に高度な手法で実現するのは悪い設計

設計の目的

  • 品質を担保するため
  • 人に伝えるため = 誰にでもわかるようにしたい
  • なぜか?
    • 品質を高く一定にしたい
    • 開発を効率化したい
    • 人が入れ替わってもメンテナンスしたい

→ そのためにドキュメントとして残す

💡いにしえの古文書でも解読できたらありがたいことがある

極論として

  • (現時点の人類にとって)設計=ソースコードだとうれしいが、そうはなっていない
    • 設計を(まだ)直接、動作に落とし込めない
    • ビジネス設計と実装設計の両方が必要

さらに悲しいことに

  • 翻案の過程でプログラミング言語で設計通りに書けない
    • プログラミング言語で設計通りに書けていることを保証したい
    • テストを積み重ねても「バグがあること」は明らかになるが、ないことは証明できない

設計段階で不具合がないことを証明したい

証明するためには

“おまえたちの作っているものはプログラムではない。バグだ。”

過去に検証を専門にしている人に言われた言葉

  • 品質が高いことを証明するには検証をしっかりと行う

形式的検証

  • 数理モデルを用いて検証する
    • 有限オートマトン
    • Floyd-Hoare論理
    • ペトリネット
    • プログラム意味論
  • 論理的推論
    • Isabelle、Coqといった定理証明ツール
  • ここで重要になってくるのが「

品質を高めるためには

  • 図やフローで可視化する
  • 分岐をなくす
  • ループをしない
  • 再代入しない
  • 型を守る
  • 副作用を減らす
  • 数理モデルに基づく証明を行う
💡実装設計ではとにかくシンプルに実現すること

このプログラムは正しいですか?

fac :: Integer -> Integer
fac 0 = 1
fac n | n > 0 = n * fac (n-1)
  • 「階乗を求める関数」としては正しい(Testでわかること)
  • 何をしたいのかを間違えているとプログラムが正しくても無意味(Validationの必要性)

このプログラムは止まりますか?

collatz :: Int -> [Int]
collatz 1 = 1 : []
collatz n | n `mod` 2 == 0 = n : collatz (n `div` 2)
          | otherwise      = n : collatz (n * 3 + 1)
  • 少なくとも$2^{32}$以下の正の整数では停まる
  • 証明できなくても、実用上は成り立つケースもある

事前条件と事後条件

  • 事前にこうなっていなければならない
  • 事後にこうなっていなければならない
💡設計、実装設計ともに記述すべき仕様の一つ

8-Queens問題にみる検証

8-Queens問題

  • 8個のクイーンを配置するとき、互いに効き筋とならない置き方は何通りあるか
  • 事前条件と事後条件は?

8-Queens問題の満たすべき条件

  • n=8個配置されている
  • 縦横のマスの合計値は必ず1である
  • 斜めのマスの合計値は0か1である

8-Queens問題の検証

$$ \tiny{ \begin{equation} \begin{aligned} \{n=8\} \\ EightQueens(n) \{ \\ {\forall}_{i,j}.1{\leq}i{\leq}n{\land}1{\leq}j{\leq}n {\Rightarrow} (X(i,j)=0{\lor}X(i,j)=1){\land} \\ \textstyle\sum_{k=1}^n(\sum_{m=1}^nX(k,m))=n {\land} \\ {\forall}_{i}.1{\leq}i{\leq}n {\Rightarrow} \textstyle\sum_{k=1}^nX(i,k)=\sum_{k=1}^nX(k,i)=1 {\land} \\ {\forall}_{k}.3{\leq}k{\leq}{n+1} {\Rightarrow} \textstyle\sum_{i=1}^{k-1}X(i,k-i){\leq}1 {\land} \\ {\forall}_{k}.2{\leq}k{\leq}{n-1} {\Rightarrow} \textstyle\sum_{i=k}^nX(i,k+n-i){\leq}1 {\land} \\ {\forall}_{k}.1{\leq}k{\leq}{n-1} {\Rightarrow} \textstyle\sum_{i=1}^{n+1-k}X(i,i+k-1){\leq}1 {\land} \\ {\forall}_{k}.2{\leq}k{\leq}{n-1} {\Rightarrow} \textstyle\sum_{i=1}^{n+1-k}X(k+i-1,i){\leq}1 \} \end{aligned} \end{equation} } $$

引用: プログラム仕様記述論p.61を一部修正
1 1 1 1 1 1 1 1

$$ \tiny{ {\forall}_{i,j}.1{\leq}i{\leq}n{\land}1{\leq}j{\leq}n {\Rightarrow} (X(i,j)=0{\lor}X(i,j)=1) } $$

  • 任意の箇所X(i,j)にクイーンがあれば1、なければ0とする
1 1 1 1 1 1 1 1

$$ \tiny{ \textstyle\sum_{k=1}^n(\sum_{m=1}^nX(k,m))=n } $$

  • すべての合計値はnである

$$ \tiny{ {\forall}_{i}.1{\leq}i{\leq}n {\Rightarrow} \textstyle\sum_{k=1}^nX(i,k)=\sum_{k=1}^nX(k,i)=1 } $$

  • 縦横の領域について合計値は1である

$$ \tiny{ {\forall}_{k}.3{\leq}k{\leq}{n+1} {\Rightarrow} \textstyle\sum_{i=1}^{k-1}X(i,k-i){\leq}1 } $$

  • 対角線左上の領域について斜め方向の効き筋が条件を満たす

$$ \tiny{ {\forall}_{k}.2{\leq}k{\leq}{n-1} {\Rightarrow} \textstyle\sum_{i=k}^nX(i,k+n-i){\leq}1 } $$

  • 対角線右下の領域について斜め方向の効き筋が条件を満たす

$$ \tiny{ {\forall}_{k}.1{\leq}k{\leq}{n-1} {\Rightarrow} \textstyle\sum_{i=1}^{n+1-k}X(i,i+k-1){\leq}1 } $$

  • 対角線左下の領域について斜め方向の効き筋が条件を満たす

$$ \tiny{ {\forall}_{k}.2{\leq}k{\leq}{n-1} {\Rightarrow} \textstyle\sum_{i=1}^{n+1-k}X(k+i-1,i){\leq}1 } $$

  • 対角線右上の領域について斜め方向の効き筋が条件を満たす

8-Queens - Haskell実装

import Control.Monad (foldM)
import Data.List ((\\))

main :: IO ()
main = mapM_ print $ queens 8

queens :: Int -> [[Int]]
queens n = foldM f [] [1..n] where 
  f qs _ = [q:qs | q <- [1..n] \\ qs, q `notDiag` qs]
  q `notDiag` qs = and [abs (q - qi) /= i | (qi, i) <- qs `zip` [1..]]
引用元: rosettacode
  • バックトラック法
    • L.9 8個配置、配置済みを除くことで1個ずつ配置
    • L.10 配置済みに対して斜め方向に効き筋でない場合に残す

設計を検証する

  • 正しく停まるか?
  • 限界を超えないか?
  • 制約を守れるか?
  • 不正な状態にならないか?
💡設計の正しさがプログラムの正しさにつながる

設計のための道具

設計を表現する方法

  • 状態遷移図/状態遷移表
  • UML図(ダイアグラム図)
    • シーケンス図
    • クラス図
    • オブジェクト図
    • ユースケース図
  • Design Structure Matrix
  • 制約リスト
  • MS Officeドキュメント

DSM(Design Structure Matrix)

DSM は要素間の依存を明らかにし、順序関係、フィードバック、構造をモデル化して整理できる

  • プロセス改善、プロジェクト計画、組織設計等で使われる

DSM例

屋根 基礎
屋根 - x x x
- x x
- x
基礎 -
x -
x x x -
  • 依存関係があるものにチェックする
    • 例)窓を作るためには壁が必要
基礎 屋根
基礎 -
x -
x -
x x -
x x x -
屋根 x x x -
  • 並べ替えて左下に集めることで順序依存が決まる
    • 床、柱は基礎以降いつ作ってもいいことがわかる
    • 柱がないと壁は作れない
    • 床と柱、屋根と窓は平行可能
基礎 屋根
基礎 -
x -
x -
x x - x
x x x -
屋根 x x x -
  • 例)窓の事情により壁のやり直しが起こりえるなら取り除くべきリスク

DMM(Domain Mapping Matrix)

基礎 屋根
源三 x
源九郎 x x
カンナ x
八波 x
刃渡 x
  • 例)担当を割り当て人員配置に問題はないか確認する

人員配置例

MDM(Multiple-Domain Matrix)

基礎 屋根 源三 源九郎 カンナ 八波 刃渡
基礎 -
x -
x -
x x -
x x x -
屋根 x x x -
源三 x - x x x
源九郎 x x -
カンナ x - x x
八波 x x - x
刃渡 x x x -
  • 例)チーム構造を合わせて表現

ネクロノミコン

  • 完璧なドキュメントは実在しない
    • 設計は図や表を駆使して可視化する
    • 時間が経っても変わらない事柄を書く
    • 使用頻度、更新頻度に合わせて管理する
💡「いつか見るかも」という参照しないドキュメントは作らない
💡経緯などあまりにも時系列が離れる場合は、そのときに判断すればよいことが多い

実践しよう

やりがちな設計

  • いきなりER図やクラス図を書く
  • プログラムを言語化しただけの設計図
  • 手続き処理で考えている
  • システム的な制約だけを意識している(32bits最大値など)

→ だったら、もう実装した方がよい

💡「設計する意味がわからない」「時間の無駄」という人はだいたい設計がこのタイプ

まずビジネス要求を把握する

  • ビジネスの表現で設計する
  • ビジネス制約を明らかにする
  • 画面に必要な要素を洗い出す
  • エンジニアにだけわかるものは設計書ではない

ユーザーの「動作」を追ってみる

例)期末にタレマネで人事考課を開始する

1. 人事部が事前に期のイベントを設計/開始する
  - ○○期人事考課
  - 対象者、評価フロー、承認者
2. イベント中に複数のタスクがある
  - 自己評価、一次面談、最終評価
3. タスクは条件を満たすまで開始できない
  - 前段のタスクの完了、期日など
4. タスクは差し戻しができる
  - 自身による引き戻しはできない
  - 一度開始したタスクは削除できない
5. 被評価者は後続のタスクの進捗のみが分かる
6. 被評価者は後続のタスクの中身は参照できない
7. 最終評価者が開示許可したものを被評価者は参照できる
8. 全体の人事考課が終わると人事はイベントを終了させる

悪い例

  • 手続き型の思考で設計に入ると、
1. イベントを開始ボタンで開始する
2. イベント内の最初のタスクが自動的に開始される
3. 最初のタスクが入力可能な状態になる
4. タスクに入力すると次のタスクが入力可能になる
5. どこまでタスクが進んだか進捗が分かる
6. 最後のタスクが完了したらイベントの終了ボタンを押す
💡実現方法が固定化されたり、実装方法が入り込む場合は特に注意

よい設計

  • 実現方法によらない本質が書かれていること
    • ビジネスのモデリング
    • システム化しなくてもビジネスとして遂行できる
💡「50年後、別の実現手段が現れたときにそのまま使えること」を意識する

モデリング

  • ユーザーの動作が見えてから、モデリングする
💡ここで初めて、状態遷移図、ユースケース定義、シーケンス図が必要、といった話になる

ビジネス要素を可視化してみる

  • まず気づいたものを書き出す
イベント情報
  タイトル
  概要
  期間
  タスクフロー
タスク情報
タスクのつながり情報
組織情報
社員情報
進捗状況
💡この時点でシステム側の「専門用語が出てきてないか?」は注意する
💡ビジネス上の専門用語は注釈を置く

制約を書き出す

  • ただの値にも制約がある
    • 例えば、イベントの期間や評価値
イベントの期間は日付としての情報を持つ
有効な開始日か検証することができる
終了日が開始日より以前になることはない

評価値は使える固定の値を持つ
評価値には順序がある
これ以外の評価値は許容しない
💡これを意識しないとロジックを分散させてベタに書く傾向になる

状態遷移を確認する

  • 事前条件、事後条件は?
  • イベントの状態遷移とタスクの状態管理がある?
  • 正常系のユースケース
  • エラー系のユースケース
  • 脱出手段を用意
  • 起こりえない状態を定義

異常系は特に注意

  • 脱出手段がないと、ある状態から動けなくなるリスクがある
    • 被評価者や承認者が退職していたら?
    • 代理承認は許可?
    • 一部タスクが該当しない場合にスキップは?

状態遷移表

From/To イベント開始前 イベント中 タスク1 タスク2 タスクN-1 タスクN イベント終了 イベント完了
イベント開始前 - 開始ボタン - - - - - - -
イベント中 - - 自動開始 - - - - - -
タスク1 - - - 入力 - - - 終了ボタン -
タスク2 - - 差し戻し - 入力 - - 終了ボタン -
- - - 差し戻し - 入力 - 終了ボタン -
タスクN-1 - - - - 差し戻し - 入力 終了ボタン -
タスクN - - - - - 差し戻し - 終了ボタン -
イベント終了 - - - - - - - - 完了ボタン
イベント完了 - - - - - - - - -

状態遷移も一つのオブジェクト

  • 状態の一覧を持つ
  • 渡された操作に応じて状態を遷移させる
  • 現在の状態を知っている
💡if文で管理せず、オブジェクトで管理する

集合を持つオブジェクト

  • 最終評価者の一覧を持つ
  • 社員が最終評価者かどうか知っている
  • 被評価者に対する評価者の情報を持つ
  • 俗に言うコレクション(配列や集合)

必要そうなオブジェクト

  • イベントオブジェクト
  • タスクオブジェクト
  • 集計オブジェクト
  • 権限オブジェクト
  • 社員オブジェクト
  • etc…

イベントオブジェクト

  • イベントに必要な情報をもつ
  • 特定期間でイベントを実施する
  • タスクの順序やフロー
  • タスクの進行状態を管理する
  • 未完了者にリマインド
  • 差し戻しで一つ前のタスクに戻る

タスクオブジェクト

  • タスクに必要な情報をもつ
  • タスクの種類
    • 自己評価タスク
    • N次評価タスク
    • 評価面談タスク
  • 代理評価者
  • スキップ可否

集計オブジェクト

  • 全体進捗や評価結果の集計ロジックがありそう

権限オブジェクト

  • 評価者になれるのは?
  • 組織との関連は?

評価者オブジェクト

  • 被評価者→高次評価者、最終評価者の関連情報

状態オブジェクト

  • 入力前、入力済=承認前、承認済み、(差し戻し)
  • 例外の状態遷移

要求分析と設計を行き来する 

  • オブジェクトの解像度を上げることで、要求からは見えないが、事実上必要な情報が明らかになる
  • 組織や権限は要求には現れづらいが密接に関係するため、要求分析と設計を行き来して曖昧さを排除していく

値オブジェクト

  • 期日
  • 評価値
  • 評価者
  • 被評価者
  • 組織
  • フロー履歴
💡主に自分自身の値に関する制約を持つ

これらが集まって設計書になる

  • ドキュメントたくさんできそうですよね

設計を繰り返す

  • これらのオブジェクト、フロー、状態遷移などを 繰り返しビジネスの言葉で表現して おかしいところを修正する
  • 抜け漏れ がないか繰り返しブラッシュアップして積み上げる
  • 当たり前だが、
    • 一回でできない
    • 一人でできない
    • 開発者だけでできない
  • 寝て起きたら変わる
    • 顧客の言うことも変わる

ここまでできたら、実装設計に入る

  • 実装を想像する
  • アーキテクチャ図を書く
  • 画面デザインを起こす
  • システム制約を追加する
評価値の値域は?使える文字は?
評価値の順序関係はどう設定する?
任意の評価値は許容するか?
型は文字列か数値か?
数値も文字列で扱うか?
文字数/桁数制限は?
編集・閲覧権限は?
タスクの状態との関係は?
入力された評価値の検証方法は?

実装を反映する

  • ビジネス上制約はないはずだが、実現のために必要な事柄をフィードバックする
評価値の値を文字列で持つ
評価値の種類は最大10パターンまで
一つの評価値は全角16文字まで
評価値の順序を画面上から設定できる
イベント開始時に設定した評価値はイベント完了まで変更できない
イベント完了後には編集できない
💡こうした制約からどのようなデザインパターンにすればよいか検討する

レビューする

  • 当たり前だが、設計をレビューする
    • 自己、他者両方
  • 必要なことが書いてあること
    • いらないことは書いていない(完璧な設計書の35%はいらないこと)
    • きれいに書くことが目的ではない(共通してわかること)

設計からテストも導かれる

  • ユーザーストーリー
  • ユースケース(正常系、異常系)
  • エラー処理
  • 状態
  • 制約
  • パターン

デザインパターンを適切に使う

  • 値オブジェクトは完全コンストラクタにする
    • 値は不変
  • インスタンス生成はファクトリーに任せる
    • どこでもインスタンス生成をしない
    • 不要になったインスタンスを回収する
  • 今回のタスク例はStateかStrategyパターンがはまるかも
    • タスクが満たさなければいけない機能、I/Fは何か
  • 真に一意のもののみシングルトンにする
    • そんなにない

スクリプト言語の壁

  • オブジェクトを表現できるか?
  • 型を守れるか?
  • Immutableにできるか?
  • パフォーマンスは出るか?
💡品質の担保が難しい言語を選択するときは注意
💡開発者が言語に精通してミスしない修練が必要

Micro Service Architecture

機能単位でモノリシックか

「一部の機能を独立させて別のサービスにしたい」ができるか

  • APIが独立していても裏側でロジックが結合していることはよくある
    • 共通化すべきものは結合していてもよい
    • ソースコードを複製して分ければよいということではない

独立性を担保する

  • 「内部的には別のサービス」と捉えて開発する
  • I/Fを定義して疎結合になっているか?
  • ビルドやデプロイを独立にできるか?
  • 別のチームでも開発できるか?

アーキテクチャ

「こうなっているとうれしい」から考える

  • ソースコードが分かれている
  • テストケースが分かれている
  • DBが分かれている
  • APIで相互に疎結合
  • 機能単位で環境更新できる
  • 機能単位の開発が相互に依存しない

ドキュメント

こういうものが必要そうから考える

  • これらは必要なドキュメント
    • 内部向けのI/F仕様書
    • 各種情報を返すAPI
    • サービスごとのリソース管理
    • サービスごとの制約管理

疎結合だからこその問題も起こる

  • 例えばDBが独立すると外部参照キー制約が使えない
  • コードの乱立
  • チーム単位で品質に差がでる

monolithic

  • サービス全体でモノリシックはメンテナンス性が低くなる
  • 機能単位でモノリシックは悪くはない
  • 不必要にマイクロサービス化する必要もない
  • 結合している単一システムの方が一定規模までは合理的

まとめ

  • 設計は品質のために行う
  • ビジネスのモデルを表現する
    • 実現方法が変わっても変わらないこと
  • 実装設計はその後に行う
    • システム制約
    • アーキテクチャ起因の制約
💡よりよい設計ライフを!