【Tips】ソフトウェア設計

関数の返り値は極力小さく

これは今のためではなく、将来的にもコードをきれいに保つために非常に重要です。特に多数の人が利用する基盤要素のクラスでは。

  • 関数の返り値が多くの要素にアクセス可能なラッパークラスであればあるほど、意図しない利用がされます。
    Javadocのあるなしに関係なく、アクセスしやすい位置にあるので利用してしまうのが時間に追われているエンジニアの宿命です。監督者がいなくなり、複数人が手を付け始めた結果生まれるのは、その返り値に応じて様々に挙動を変える謎の何か。返り値に求められる役割が複数に渡り、変更を加えていいのかすらいちいち確認しないといけない
  • 異なる責任を負いたくない

対策としては、

  • Interfaceで返す
  • 必要な値のみ返す
  • フィールドの時点でそもそも大きなラッパークラスを持たない

全てにInterfaceで返すのは現実的ではないですが、テストも書きやすくなるので極力実施すべきでしょう。最後のがチーム開発では重要でしょう。便利なフィールドをクラスで持っている時点で利用したくなってしまうので、保持するフィールドの形は最小限にしましょう。

Argumentは極力小さく

これは上記と同様の理由がまずあります。便利なArgumentほど将来的にそのまま利用されてしまうので、関数の責務が大きくなりすぎます。その時点で分割できると考えるべきです。

また、引数として大きなArgmentが渡されるということは、その関数の中身を見るまでプロセスを想像できません。ただのデータの中継役として大きなラッパークラスを渡すのはやめましょう。もしかしたら中で何かすごく大きな処理をしているのかもしれないし、ラッパークラスの中のたった一つの値だけ利用しているのか、何も分からないのです。

フラグに違う意味を持たせるな

なぜか似通った状態、たまたまその時点では同様の意味となる状態を示すフラグあったときに、それをそのまま利用するのはやめましょう。後で修正する人が泣きます。

厳密に定義し、新規にフラグを作成しましょう。厳密に定義した結果、既存のフラグと関連があるのであれば、

  • 共通部分をまとめる
  • まとめない場合はどの状態まで共通なのかドキュメント
  • 関連部についてドキュメント

意味が違うのであればいくらフラグがあっても問題ではありません。最悪なのは上記したように将来違う状態もありえるのに、そのフラグを無理に利用してしまうこと

例えば、高速道路というフラグが必要な時に、たまたま60km/h以上走行すべき道路というフラグがあったから利用するのはNGです。今は60km/h以上で走行するのが高速道路かもしれませんが、将来的には100km/hになるかもしれません。(というかソフトウェア上は必ずと言っていいほど変わります)

この概念は単一か複数か

Viewerというシステムがあったとき、これは1つだけなのか、2つ以上あるのか、というのは基本的ですが常に意識しましょう。ViewerAとViewerBが別なのは理解していたけど、ViewerA自体も複数あり得るんだよ、というのは後で判明すると面倒くさいです。

何が言いたいかというと、

”システムがViewerAである場合、xxしない”

の場合、ViewerA1とViewerA2は同様の条件で大丈夫か?ということです。例えばViewerAがViewerA自身の動作結果の変更通知を受け取る場合、自身の通知は他部分で処理している可能性があるので省きたいとします。この場合に上のように単純にViewerAという括りで省いてしまうと、ViewerA2はViewerA1の動作による変更を受け取りたいのに受け取れないという可能性が生じます。

システムがシングルかマルチかというのは常に意識して損はないですね。

複数のStateで振る舞いが決定するときのクラス設計

初期化時に決定できるようなStateの場合、Stateごとにクラスを分けることをまず思いつきます。ただStateが複数種類になったらどうするのでしょう?StateAの状態が3つ、StateBの状態が2つある場合、合計6つのクラスを作成しますか?

そのStateの持つ処理量の大きさによりますが、合計6つはかえって見通しが悪くなります。また、StateAで別れている箇所には、共通したStateBの処理を記載する必要があり重複コードが生まれてしまうのでどこかに切り出す必要があります。以下のようなコードにはしたくないということです。

public abstract class AbstractClass {
    abstract void methodA();
    abstract void methodB();
}

public class StateA1B1 extends AbstractClass{
    @Override
    void methodA() {
        doA1();
    }

    @Override
    void methodB() {
        doB1();
    }
}

 public class StateA2B1 extends AbstractClass{
    @Override
    void methodA() {
        doA2();
    }

    @Override
    void methodB() {
        doB1();  //ここが重複
    }
} 

これらをシンプルに書くためには実現方法はどうあれ、委譲するのがシンプルでいいのでしょう。

  • StateAHandlerのような処理クラスを作成して、それに委譲する
  • annonymous classとして毎回指定する
  • lambdaで処理を指定する

特にシンプルな処理であれば、インスタンス化時にラムダで処理を指定するのがいいと思います。共通処理としてまとめる必要があるのであれば、関数型Interfaceをstaticメンバとして定義します。

public class ClassImpl<T,R> extends AbstractClass<T,R>{
    Function<T, R> operatorA;
    Consumer<T> consumerB;

    public ClassImpl(Function<T, R> operatorA, Consumer<T> consumerB) {
        this.operatorA = operatorA;
        this.consumerB = consumerB;
    }

    @Override
    R methodA(T a) {
        return operatorA.apply(a);
    }

    @Override
    void methodB(T b) {
        consumerB.accept(b);
    }

    public static Function<Integer, Object> cOperatorA1 = (a) -> {/* do something */ return null;};
    public static Function<Integer, Object> cOperatorA2 = (a) -> {return new Object();};
    public static Function<Integer, Object> cOperatorA3 = (a) -> {return new Object();};
    public static Consumer<Integer> cOperatorB1 = (a) -> {/* do something */;};
    public static Consumer<Integer> cOperatorB2 = (a) -> {/* do something */;};
}

//使うとき
     void test(){
        new ClassImpl<Integer, Object>(
                cOperatorA1, cOperatorB1
        );
    } 

開発時間短縮には?

今までユニットテストはソフトウェアの品質向上とRegressionの目的で作成することが重要だと感じていましたが,それだけではないですね.ユニットテストを記載することで,統合テスト前に不具合を発見できるということです.

開発上時間を要するのは
ユニットテスト<結合テスト<統合テスト
の順番な訳です.統合テストなんかは必要なソフトウェアを立ち上げる必要があるので,ここでテストすればいいやと思って何回も修正+ビルド+立ち上げを繰り返していると無限に時間が消費されてしまいます.

ユニットテストでテストできる部分はしてしまった方が結果的に開発が早くなることが多いのです.時間がないからユニットテストは省略する,というよりも,時間がないからユニットテストから作成する,が体験上良さそうです.

View層はInput/Outputに集中

上記のユニットテストを書きましょう,の話に関係しますが,ユニットテストを書きやすくするための鉄則としてユーザーInput/Outputを行うView層にそれ以外の役割を持たせないように気を付けます.View層は基本的に統合テストでの評価が必要になるため,View層に表示以外の責任を持たせてしまうと,その責任部のユニットテストが書けなくなります.

View層が基本的に統合テストでの評価が必要,となるのはそもそもView層をインスタンス化できないこととユーザーInput/Outputが必要だからです.

そのため,View層は表示責任にとどめ,すぐにModel/Presenter/ViewModel層に委譲します.以下のような形です.

Button button = new JButton(){
    @Override
    public void onClick(){
        viewModel.onClick();
    }
}

viewModel自体はインスタンス化もしやすく,View層に依存しないのでロジック部のユニットテストが記載できます.onClick()の動作が期待するviewModelの状態遷移もテストできます.(onClickは本当はもっと正確な役割を示す名称がいいです)

最近仕事上でDialogクラスに全てを記載するコードをよく見かけるのでこれは本当に良くない例ですね.テストしにくいし勘弁してほしい.

バグの修正方法は一つじゃない

バグの修正方法は一つではありません.

  • 根本原因を潰し,バグが発生しないように修正する
  • バグが発生したとしても,アプリケーションに影響しないように修正する
  • バグが発生したとしても,影響が軽微なため,Known Issueとして顧客に通達する.バグ発生を抑制できる・バグ発生した時の影響を軽微にできる手法(Workaround)があれば,併せて提供する

概ね上記の3つの対策を取ることが可能です.バグの深刻度とバグ修正工数,顧客への通達工数を天秤にかけて決定します.

バグ修正工数が1日程度なら実施した方が早いでしょうが,それ以上にかかる場合は相談の元,他2つの対策を検討すべきです.2日なら直せるじゃん,という話もありますが,バグは何十個も生まれるもので必須でない箇所は対策しない,というのも立派な対策の一つです.

TODO,FIXMEコメントにはタグをつける

ある作業に対して,複数のTODO・FIXMEコメントを付与する場合には,タグをつけましょう

TODO:tag1
//説明

のような形式です.これは後から検索しやすくするためです.タグ付けがないと無限にあるFIXME,TODOが表示されるためクラス名等覚えていないと二度と探し出せません

値はプロパティから取得する

基本的にある数値は将来に変更したくなります.理由としては性能アップ,顧客要求,バグ修正,機能変更等です.もし顧客のところで見つかったバグでもプロパティを変更するだけなら,コード変更を含まないので迅速にデプロイできます.

何が言いたいかというととにかく値のハードコードはやめようということです.よっぽどアルゴリズム内部で使用される値とか,変更されたら動作に支障がでる値以外はプロパティで変更できるようにした方が無難です.

物理システムと連携するソフトウェア設計におけるRisk Assessment

自動運転システム等,物理システムと連携するソフトウェア開発においては,人的被害・生産性への影響,特に人的被害について綿密なRisk Assessmentが求められます.

Risk Assessmentにおいては,SIL(Safety Integrity Level)評価を実施することが一般的でしょう.リスクレベルとリスク確率に基づいてSILランクを決定されます.SILランクが所望の値となるように安全設計を施します.

https://www.mhlw.go.jp/file/05-Shingikai-11201000-Roudoukijunkyoku-Soumuka/gaiyou\_1.pdf

なお,要求動作が高頻度なのか低頻度なのかで,確率の定義が異なります.高頻度は定常的に動作する機能に対して,故障側に至る確率であるのに対して,低頻度モードでは非常時のみ動作するような要求ケースが低頻度である機能に対して,その機能が故障側に至る確率です.

高頻度モードにおいては,10^-9 ~ 10^-8という値で最高評価のSIL4になりますので,10万年に1回発生するかどうか,という値が指標になります.

勿論,この値に対して生産数が影響を与えますので,企業は予定生産数に対して,SIL4以上の機能安全を求める必要となる可能性もあります.

flagで持つか,stateとするか

ある状態を表すのにbooleanフラグを導入する必要がある場合を検討します.

それを設計として,一つのフィールドとしてflagで表現するか,stateとしてflag=ON / OFFの状態として表現するか,が課題です.

全てのflagがシステムに規定されている全てのstateと関連する訳ではない.
flagで規定する機能が関連するstate数が重要.
この状態で考える.

flagで定義した場合,flag (ON / OFF) * state数の状態が新規に発生する.
stateで定義した場合, state Xの状態が新規に発生する.

この場合,新規に追加すべきテスト項目としては,
flagで定義した場合,2 * state数における仕様定義
stateで定義した場合,2 * state数における遷移条件定義

あるstateに応じて定義される関数method Aが存在する.
method Aが新フラグにより影響を受けるため,stateとして分離すべきかどうかを検討する
flagの場合は,method A内で,on / offでif分岐されるイメージ.
stateの場合は,method Aが新規クラスstateとして定義されるイメージ

flag state
メリット
デメリット

オブジェクト数が複数になることを頭に叩き込む

複数オブジェクト・システムをサポートしなければならない時がある。1:1で上手く設計できていても、1:Nでは正しく動作しない場合がある。特に通信が絡む場合は注意である。1:1の設計では、リソースの競合、状態管理、並行性に関する問題が顕在化しにくい。

Web Serverとして動作するなら、
・Destination Portは同一でよいのか、異なるべきなのか、
・Source Portは同一でよいのか
を検討する必要がある。

Web Server内のApplicationがどのようにClientを識別するかに依存する。

例:
Web ServerがClientごとに異なるPortを要求する
理由:Streamの識別のため。WebRTC ServerのRTP Sourceはこの形式をとることが多い。
* メリット: 複数のストリームを容易に識別可能。
* デメリット: クライアントはポート範囲を管理する必要がある。ファイアウォールの設定が複雑になる場合がある。

Web ServerがClientごとにSource IPを元に識別する場合は、宛先Portは同一でよい。
* メリット: クライアント側の設定が容易。
* デメリット: NAT環境下では、同じSource IPからの接続を区別できない場合がある。セキュリティ上のリスクが高まる可能性も(IPスプーフィングなど)。

対象Serverがどちらのロジックを採用しているかでClient側の対応が異なるため注意が必要。

例:

  • チャットサーバー: 1:1の設計では、単純なメッセージ送受信のみを想定していたため、ルーム機能(1:N)を追加する際に、メッセージのブロードキャスト処理やルームごとの状態管理が複雑になった。
  • ファイルサーバー: 1:1の設計では、ファイルの同時編集を考慮していなかったため、複数ユーザーが同時に同じファイルを編集すると、データの不整合が発生する可能性があった。

結論
ネットワーク: NATがSource Portをズラしてくれるので、OSレベルの通信は成立する。

アプリ: Session IDやTokenを使って、「どの通信がどのユーザーか」を紐付ける。

設計上の注意: 常に「同じ処理が並列で動く」ことを前提に、データをカプセル化(インスタンス化)して管理する必要がある。

Identification

特にセッションがない素のUDP棟を受け付ける場合、どのように送信元を識別・特定するかを検討する必要がある。
(ip, port)のペアか?
NAT下ではどうする?
(ip, port)の識別だけでよいのか、そのアプリケーションを動かす機種まで把握したいのか、その場合、その管理表は誰が持つのか、誰が作るのか、どのようにシステムにインプットするか、
これらを精緻に検討する必要がある。

特にセッション管理のないプロトコル(UDP等)を扱う場合、送信元の識別設計は慎重に行う必要があります。以下の観点で整理します。

  1. 識別子の階層化:
    • ネットワーク識別 (IP/Port): 最も基本ですが、NAT環境下では複数の端末が同一IPに見えたり、通信の途中でポート番号が変わる(ポートマッピングの変更)可能性があるため、これだけに依存するのは危険です。
    • アプリケーション識別 (Session ID / Token): ペイロード内に一意のID(UUID等)を含めます。これにより、ネットワーク経路が変わっても「誰からのデータか」を継続して特定できます。
  2. デバイス・コンテキストの紐付け:
    • 単なる通信路の識別だけでなく、「どの機種か」「どのバージョンか」といったメタ情報が必要な場合、初回接続時(ハンドシェイク時)にこれらの情報を交換し、アプリケーションIDと紐付けてサーバー側で管理(セッション管理表)する必要があります。
  3. 管理責任の明確化:
    • 生成: 識別子は誰が発行するか?(クライアントが生成して重複チェックするか、サーバーが払い出すか)
    • 維持: タイムアウト処理はどうするか?(UDPは「切断」イベントがないため、ハートビートによる生存確認が必須)
    • インプット: 許可されたデバイスリスト(ホワイトリスト)が必要な場合、そのマスターデータはDBで持つのか、設定ファイルか、運用フローを含めて設計します。

べき等性(Idempotency)の確保

分散システムや不安定なネットワーク(UDP/モバイル回線)を扱う設計では、「同じ操作を複数回実行しても、結果が一度だけ実行した時と同じになる」という性質が極めて重要です。

  • リトライ耐性: ネットワークの瞬断で応答が返らなかった際、クライアントは再送します。サーバー側で「既に処理済みか」をリクエストID等で判定できないと、二重課金やデータの重複登録が発生します。
  • 設計指針:
    • updateValue(newValue) はべき等ですが、incrementValue(1) はべき等ではありません。
    • 副作用のある操作には必ず一意のリクエストトークンを付与し、サーバー側で一定期間その結果をキャッシュする設計を検討してください。