LoginSignup
8
12

More than 3 years have passed since last update.

変更に強いプログラムを書こう

Last updated at Posted at 2020-05-08

image.png

対象読者

以下『変更に強いプログラムがもたらす利益』を得たいと思う全てのIT従事者

変更に強いプログラムがもたらす利益

・ プログラミングの作業時間が短くなる
・ ミスや副作用が減りリグレッションが起こりにくくなる
・ 結果として質の高いプログラムが早く完成する
・ 時間がかからない分開発コストを削減できる

変更に強いプログラムの重要性はここにあり、これはプログラマ、プロジェクト管理者、経営者とポジションにかかわらす組織に属する多くの人に利益をもたらす重要な考え方だと分かっていただけると思います。

今回『変更に強いプログラムを書こう』をテーマに、どのような特徴を持ったプログラムがこれらの利益を実現するのか、単なる一般論で終えることなく最終的にコードまで落とし込んで詳細に解説していきます。

変更に強いプログラムの定義の前に少しだけ前置きから・・・

プログラムの変更の目的

仕様の変更、バージョンアップ、バグ修正など変更の理由は様々ですが、簡単に言えば既存のプログラムをより良くするために変更を行うわけです。ただしそこには変更に伴うリグレッションの可能性や開発費用の増加というリスクが伴う・・・。言うまでもなく変更は必要でもできるだけ避けたいものです。

理想

プログラムは拡張したいが変更に伴うリグレッションは起こしたくない。つまり・・・
『変更によるリスクを排除し、機能拡張のメリットのみを享受する。つまりリグレッションを起こさずにプログラムを変更・拡張する』
都合のいい理想論に聞こえますが、要はプログラムを変更せずに拡張することができれば最高なんです。

変更せずに拡張する

拡張は変更の一種なので変更せずに拡張するというのは基本的に矛盾してるわけですが『変更せずに拡張する』これを実現するプログラミングの原則があるんです。
この原則についてRobert C.Martin著『Clean Architecture』には以下のように書かれています。

これこそが我々がソフトウェア・アーキテクチャを学ぶ根本的な理由だ

変更に強いプログラム

『変更せずに拡張できるプログラム』
今回はこれを変更に強いプログラムの定義とします。そしてここから少し具体的に、以下プログラムその1とその2を使ってこれをテーマに説明します。

プログラムその1

サービスを提供しているサービス提供側とそのサービス利用側があります。サービスは現在A~Cの3種類ですが最終的にZまで26種類のサービスを提供する予定です。
image.png
サービス提供者はサービスA,B,Cを提供し、利用者はそれらサービスの受け入れ体制をサービスごとに整えて使用しています。

image.png
サービス提供側は新たにサービスDを作りました。

image.png
サービス利用側は新しいサービスDを利用するために受け入れ態勢を整えます。

image.png
サービス利用側は新しいサービスを受け入れ、サービスDの利用を開始します。

さて、ごく自然な処理に見えたかもしれませんが、これは変更に弱いプログラムの例となります。
サービス利用側は新しいサービスDが提供(変更1)される際、サービスD用の受け入れ態勢を整える(変更2)必要があります。
つまり新しいサービスが提供されるたびにサービス利用側はそのサービス用に受け入れ態勢を整えなければなりません。サービス提供側の変更がサービス利用側にも影響する変更が変更を呼ぶ構造で、サービスがZまで増えることを考慮すると変更のコストは大きいと言わざるを得ません。

プログラムその2

次に今回のテーマである、少ない変更で保守・拡張を可能にする変更に強いプログラムの例を説明します。つまりサービス利用側は新しいサービスDが提供された際、一切の変更なしで新しいサービスの利用が可能となります。
具体的に見てみましょう
image.png
サービス提供側はサービスA,B,Cを提供し、利用者はそれらサービスを抽象的に受け入れ使用しています。

image.png
サービス提供側は新たにサービスDを作りました。サービス利用側はすべてのサービスを抽象的に操作することが可能なためすでに新しいサービスDの受け入れ態勢が整っています。

image.png
サービス利用側は自身を変更することなく新しいサービスを受け入れサービスDの利用を開始します。

このプログラムは変更に強いプログラムの例となります。
サービスの利用側は新しいサービスを利用する際、一切の変更を必要としません。サービス利用側のプログラムを変更することなく新しいサービスの利用が可能、つまりサービス提供側の変更がサービス利用側に影響しない構造となります。これはサービスがZまで増えることを考慮すると大きな利益であり、サービスが増える方向への拡張性が高く変更に強いプログラムと言えます。

いったん結論

変更に強いプログラムの構造的な特徴

  1. 複数の具象が抽象化されている
  2. 複数の具象が抽象で一括操作可能となっている
  3. 利用側は抽象にのみ依存しているため(その向こう側の)具象が変わっても影響を受けない

「抽象化」が一つの鍵になっていることが分かると思います。

詳細

さて上の例で『変更せずに拡張する』が実現されていることに気づきましたか?以下技術者向けに掘り下げて解説します。

Open Closed Principle(OCP:開放閉鎖の原則)

※別名『オープン・クローズドの原則』
SOLIDの五つの原則のうちのひとつ、'O'に当たる原則です。

プログラムは拡張に対して開いて、変更に対して閉じていなければならない

非常にわかりにくい表現ですが、上の図で説明したプログラムその2の例にはこの原則が適用されています。
かみ砕くと『プログラムは変更することなく拡張可能でなければならない』という意味で、今回の場合:
- サービス利用側を変更することなく(閉鎖)
- 新しいサービスを追加・拡張している(開放)
ことで『拡張に対して開いて、変更に対して閉じている』OCPに準拠しています。

プログラムをOCPに準拠させるには

関連する設計概念

OCPを適用するうえで必須となる設計概念・原則は以下の通りです。

抽象化

具象サービスを正しく抽象化する

Dependency Inversion Principal(DIP:依存関係逆転の原則)

サービス利用側を(具象サービスではなく)抽象サービスに依存させる

Dependency Injection

サービスをその利用側スコープの外側で作成し、外側からそのサービス利用側に注入する

ポリモーフィズム

サービス利用側が抽象サービスを介して複数の異なる具象サービスをまとめて操作する

OCPの基本適用手順

OCPの基本的な適用手順を次に説明するサンプルコードと合わせて説明すると以下のようになります。
1. 具象サービスを正しく抽象化し
class ServiceA implements Service

2. サービス利用側をその抽象クラスに依存させ
static List<Service> listService = new ArrayList<Service>();

3. サービス利用側スコープの外側で具象サービスを生成し
Program02.set(new ServiceA());

4. サービス利用側スコープの外側から具象サービスを注入する
static void set(Service obj)

5. 結果として具象サービスが増える方向への高い拡張性が実現される
Program02.set(new ServiceD()); <-- No Error

サンプルコード

上の図で説明したプログラムその1(class Program01)とその2(class Program02)のサンプルコードです。


public interface Service {  
    void doSomething();
    String identify();
}
public class ServiceA implements Service {
    @Override
    public void doSomething() { }
    @Override
    public String identify() {return "A";}
}
public class ServiceB implements Service {
    @Override
    public void doSomething() { }
    @Override
    public String identify() {return "B";}
}
public class ServiceC implements Service {
    @Override
    public void doSomething() { }
    @Override
    public String identify() {return "C";}
}
public class ServiceD implements Service {
    @Override
    public void doSomething() { }
    @Override
    public String identify() {return "D";}
}
public class Provider {
    public static void main(String[] args) {        
        //プログラムその1
        Program01.set(new ServiceA());
        Program01.set(new ServiceB());
        Program01.set(new ServiceC());
        Program01.set(new ServiceD());  <--Error
        Program01.doSomething();

        //プログラムその2
        Program02.set(new ServiceA());
        Program02.set(new ServiceB());
        Program02.set(new ServiceC());
        Program02.set(new ServiceD());  <-- No Error
        Program02.doSomething();        
    }
}
public class Program01 {

    private static ServiceA objA;
    private static ServiceB objB;
    private static ServiceC objC;

    public static void set(ServiceA obj) {
        objA = obj;     
    }

    public static void set(ServiceB obj) {
        objB = obj;     
    }

    public static void set(ServiceC obj) {
        objC = obj;     
    }

    public static void doSomething(final String id) {
        if(id.equals("A")){
            objA.doSomething();
        }else if(id.equals("B")){
            objB.doSomething();
        }else if(id.equals("C")){
            objC.doSomething();
        }
    }
}

このProgram01のコード(プログラムその1に相当)はOCPに準拠していないためサービスDを使用するためには
- クラス変数 objD の追加
- setterメソッドの追加
- objD.doSomething();と、その分岐処理の追加
- 新しいライブラリの取り込み
をサービス利用側であるProgram01に行う必要があります。この変更は新しいサービスが提供側から提供されるたびに(サービスZまで)繰り返されます。

public class Program02 {
    static List<Service> listService = new ArrayList<Service>();

    public static void set(Service obj) {
        listService.add(obj);       
    }

    public static void doSomething(final String id) {
        for(Service obj : listService) {
            if(!id.equals(obj.identify()))continue;
            obj.doSomething();  
            break;
        }
    }
}

一方このProgram02のコード(プログラムその2に相当)はOCPに準拠しているため変更することなくサービスDを使用可能です。以降サービスZまで一切の変更を必要としません。

このサンプルは非常に単純なものですが、『クラスを抽象化し、呼びだし側をその抽象クラスに依存させ、複数の具象オブジェクトをポリモフィックに操作することで、少ない変更でプログラムの保守・拡張可能にする』ソフトウェア開発のカギとなる重要な考え方が詰まっています。

おまけ

コード補足

サービス提供側:Class Provider
サービス利用側:Class Program01(プログラムその1)、Class Program02(プログラムその2)
サービスA ~ Z:Class ServiceA ~ Z
Service:Interface Service

instanceof演算子

instanceof演算子が多用されているプログラムはOCPに準拠していない変更に弱いプログラムの良い例です。

検査例外

検査例外もOCPに反すると言われています。
*詳細はRobert C.Martin著『Clean Code』を参照

まとめ

『変更に強いプログラムを書こう』をテーマに、Open Closed Principleを適用した変更に強いプログラムついて解説しました。今回の内容は、開発言語やシステムの種類、業界業種、さらに時代にも依存しない非常に汎用性の高い考え方です。さらに抽象的な考え方であるにもかかわらず、具体的な設計と実装が可能で非常に実用的です。

プログラマとして『変更に強いプログラム』を理解しよう
高い生産性を実現するプログラムの構造的な特徴やヒントが分かるようになります。目的が分からず抽象クラスやインターフェースを使っていませんか?そのインターフェースはまず意味をなしていないでしょう。

管理者として『変更に強いプログラム』を理解しよう
開発者からの進捗報告を整理・調整するのみでなく、進捗を一定レベルコントロールできるようになります。受け売りの一般論ばかり言ってませんか?具体的な設計論でチームの開発効率を上げましょう。

経営者として『変更に強いプログラム』を理解しよう
生産性の向上につながる設計力を持った管理者、技術者が分かるようになります。チームの力を客観的に判断できていますか?チームからのアウトプットは悪い設計故の開発チーム都合によるものかもしれませんよ。

image.png
◆記事更新履歴
2020/04/13投稿
2020/05/08削除(更新履歴が多いため)
2020/05/08再投稿

8
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
12