昔ながらのプログラム設計論(スマホアプリ系)
0.いきさつ
先日、社内で新人に説明するために、基本的なプログラムのモジュール設計法について大急ぎでメモにまとめた。もしかしたら他にも誰かの役に立つかもしれない。せっかくなのでブログのネタにしてしまおう、ということでブログに掲載してみることにした。
ここに書いたことは全然目新しくはなくて、昔からある考え方であるが、現在でも有効なものである。非常に基礎的な考え方なので、オブジェクト指向設計でも、構造化設計でも、(場合によっては関数型でも?、)有効な考え方である。プログラム設計を分割統治法で考えようとするとき、この考え方をマスターしておくと、品質の高いプログラムを実装するのがたやすくなるはずである。
意外に、最近はこうしたことをハッキリ説明してくれている文献があまり見当たらない。特に新人が最初に参考にするような書籍やネット記事などでも触れられていることが少ないのだが、何故だろう? 非常に大事なプログラマの『感覚』なのではないかと思うのだが。
1. プログラムのモジュール(クラス・メソッド)分割方針について
プログラムを設計していく場合、クラスなりメソッドなり、何かのカタマリ(=ここではモジュールと呼びます)に分割していくわけですが、そこには経験的に上手くいく指針があります。それらを以下に説明します。
1.1. 常に『このモジュール(クラス・メソッド)の責務(Responsibility)は何か?』を明確にして分割する
- 『責務』という日本語だと分かりにくいですが、Responsibilityという英語だと、プログラマの直感に近いニュアンスです。
- Responsibilityは、'Response + ablility' という構成の語であり、『(ある要求に)反応・応答する能力を持っている』という意味です。
- クラスやメソッドを設計するときのプログラマの意識は、Responsibility=『外部からの要求を受けて、決められた仕事を実施して結果を返すユニット』にあります。
- このとき、そのモジュールの責務=『どの範囲の要求に応答し、どこまで仕事をさせることにするか』を明確に決めておくことが大事です。
- 責務を曖昧にしていると、どっちつかずで位置づけが分かりにくく、デバッグがしにくかったり、将来の機能拡張や引継ぎのときに混乱を生じる原因になったりします。
1.2. 一つのモジュールには、明確にシンプルに説明できる一つの責務だけを持たせる
- 一つのクラスや一つのメソッドにたくさんの機能を盛り込むと、ややこしくなってバグの元になります。
- 例えば、『なにかを計算して、その結果を表示する、エラーならエラー表示を行う』という機能を一つのメソッドに詰め込むのは、原則として良くない設計です。
- 以下の4つの単機能の下位メソッドを作り、それらをまとめて制御する上位モジュールを作るのが基本です。
- 計算する
- 計算結果を判定する
- 結果を表示する
- エラーを表示する
1.3. モジュール間の上下関係・主従関係を明確にする
- 上位(主位)モジュールは:
- いろいろな下位モジュールを呼び出します。
- 全体の状況(状態)を把握しています。
- 状態を持ち、その更新に責任を持ちます。
- スレッドやタイマーを管理し、システムのさまざまな処理を起動するタイミングを統括します。
- メモリやDB,リソースの初期化・確保・削除・解放に責任を持ちます。
- 別名:Activeモジュールといいます。
- 下位(従位)モジュールは:
- 上位モジュールから呼び出されたら、単純な一つの責務を実行します。
- いつ呼び出されるかは知りません。
- なぜ呼び出されるかも知りません。
- 上位モジュールの存在は知りません。
- 他の下位モジュールの存在も知りません。
- リソースの確保や解放はしません。
- 『副作用』や『状態を持つ・状態を更新する』のはナシにするのが理想です。副作用があったとしても最小限かつ明確なものにします。
- 別名:Passiveモジュールと呼びます
図:上位モジュールは、下位モジュール呼び出す
図:下位モジュールは、上位モジュールを呼び出さない。下位モジュール同士の直接のやりとりは、原則やらない。
図:下位モジュールは単純・単機能な責務にする。上位モジュールは状態を管理し、全体を統括する。
2. オブザーバ(Delegate,Listener)パターンについて
オブザーバパターンでは、プログラムコード上は、下位が上位を呼び出す形になる場合がある。ただし、呼び出し先のオブザーバ(delegate, listener)はプロトコル(Swift)やインターフェイス(Java)として抽象化されており、具体的な特定のクラスを差していないので、上記ような意味で上位クラスを呼び出すのとは異なっている。
3. iOSアプリでの上下関係の例
- 最上位モジュール:AppDelegate
- 上位モジュール:各画面の ViewController
- VC内のメソッドの上下関係
- 上位メソッド
- 状態を把握・状態遷移を行う
- クラスのプロパティを変更しても良い。
- 例:
- viewDidLoad()
- viewWillAppear()
- viewDidAppear()
- viewWillDisappear()
- UIボタンやセンサーのイベントハンドラ
- ただし、ハンドラに長々と制御コードを書くのは好ましくない。制御を集約した別クラスを作って、それを呼び出す形にする、など。
- ・・・
- ユニットテストしにくい
- 下位メソッド
- 状態を持たない(状態値を変更しない、参照しない)
- クラスのプロパティを変更しない。
- 入力=引数のみに依存し、結果は全て return 値だけに反映する。
- 例:
- 自分で定義する各種処理ルーチン
- ユニットテストがやりやすい
4. どこで深刻なバグが起きやすいか
- 上記説明における、下位モジュールでは、あまり深刻なバグは生まれません。
- ほとんどの深刻なバグは上位モジュールの状態管理や条件判定、あるいはスレッドやタイマーの制御、メモリやリソースの管理に起因します。
- したがって、デバッグしやすくするには以下の原則を守ることが大事です。
- スレッドやタイマーの制御方針を明確に、一貫したものに。
- 状態の管理や状態遷移の方法を明確に、一貫したものに。できるだけ一箇所・ワンパターンに集約する。
- 安易にフラグ制御を増やさない。
- メモリ・リソースの確保・解放は同一レイヤー(階層)に揃える。
以上