スポンサーリンク

【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が表示されるためクラス名等覚えていないと二度と探し出せません

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

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

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

タイトルとURLをコピーしました