技術とか戦略とか

IT技術者が技術や戦略について書くブログです。

リスコフの置換原則とは

リスコフの置換原則とはオブジェクト指向の設計の原則の一つであり、「基本クラスをサブクラスに入れ替えても問題なく動かなけらばならない」という原則です。
もしサブクラスに入れ替えると正しく動かなくなるのであれば、クラスを利用する側はサブクラスを使用してはならないというのを意識しながらコーディングしなければならず、保守性が低下します。
 
クラスの入力・出力に目を向けると、以下の条件を満たすとリスコフの置換原則を満たすことができます。
1.サブクラスの入力のパターンは、基本クラスの入力のパターンよりも緩い
2.サブクラスの出力のパターンは、基本クラスの入力のパターンよりも厳しい
 
もし、サブクラスの入力のパターンが基本クラスの入力のパターンよりも厳しいと、利用する側はサブクラスを利用する際にサブクラスが許容する入力であるかどうかを意識する必要が出てきてしまいます。
また、サブクラスの出力のパターンが基本クラスの出力のパターンよりも緩いと、利用する側はサブクラスを利用する際にサブクラスが予期せぬ出力をしないか意識する必要が出てきてしまいます。
 
以下のサンプルコードは、割り算を行う基本クラスであるDivideクラスを、サブクラスのDivideBusinessクラス・DivideBad1クラス・DivideBad2クラスで入れ替えてみるサンプルです。
 
DivideBusinessクラスは、基本クラスが許容する入力パターンに加え分母=0のパターンも許容し、基本クラスが出力することがある負数を出力しません。Divideクラスと入れ替えても特に考慮するパターンが増えることはなく、リスコフの置換原則を満たした良いクラスです。
しかし、DivideBad1クラスは、基本クラスが許容する分子=負数のパターンを許容せず、基本クラスと入れ替えた場合は分子が負数になっていないか意識する必要が出てきてしまいます。
また、DivideBad2クラスは、基本クラスと異なりnullを出力するパターンがあり、基本クラスと入れ替えた場合は出力がnullになる場合を意識する必要が出てきてしまいます。
 
【サンプルコード】
・DivideMain.java
public class DivideMain {

    public static void main(String[ ] args) {

        Divide divide[ ] = new Divide[4];
        divide[0] = new Divide();
        divide[1] = new DivideBusiness();
        divide[2] = new DivideBad1();
        divide[3] = new DivideBad2();

        for (int i = 0; i < 4; i++) {
            System.out.println("[テストその" + (i + 1) + "]");
            Calc(divide[i],6,2);
            Calc(divide[i],-6,2);
            Calc(divide[i],6,0);
            Calc(divide[i],null,2);
        }
    }

    public static void Calc
        (Divide divide, Integer numerator, Integer denominator) {

        System.out.println("クラス名:" + divide.getClass().getName());
        System.out.println("分子  :" + numerator);
        System.out.println("分母  :" + denominator);

        try {
            Integer result = divide.Calc(numerator, denominator);
            if (result == null) {
                System.out.println("結果  :想定外の値!");
            } else {
                System.out.println("結果  :" + result);
            }

        } catch (Exception e) {
            System.out.println("結果  :想定外の例外!");
        }

        System.out.println("");
    }
}
 
・Divide.java
// 入力:分母はnullや0は不可(例外送出)
// 出力:整数(負になり得る)
public class Divide {

    public Integer Calc(Integer numerator, Integer denominator)
        throws Exception {

        int numeratorTemp;
        int denominatorTemp;

        if (numerator == null) {
            numeratorTemp = 0;
        } else {
            numeratorTemp = numerator;
        }

        if (denominator == null) {
            denominatorTemp = 0;
        } else {
            denominatorTemp = denominator;
        }

        return numeratorTemp / denominatorTemp;
    }
}
 
・DivideBusiness.java
// 入力:分母はnullや0も許容
// 出力:整数(負にはならない)
public class DivideBusiness extends Divide {

    public Integer Calc(Integer numerator, Integer denominator)
        throws Exception {

        int numeratorTemp;
        int denominatorTemp;

        if (numerator == null || numerator < 0) {
            numeratorTemp = 0;
        } else {
            numeratorTemp = numerator;
        }

        if (denominator == null || denominator < 0) {
            denominatorTemp = 0;
        } else {
            denominatorTemp = denominator;
        }

        if (denominatorTemp == 0) {
            denominatorTemp = 1;
        }

        return numeratorTemp / denominatorTemp;
    }
}
 
・DivideBad1.java
// 入力:分子は負数は不可(例外送出)、分母はnullや0未満は不可(例外送出)
// 出力:整数(負になり得る)
public class DivideBad1 extends Divide {

    public Integer Calc(Integer numerator, Integer denominator)
        throws Exception {

        int numeratorTemp;
        int denominatorTemp;

        if (numerator == null) {
            numeratorTemp = 0;
        } else {
            numeratorTemp = numerator;
        }

        if (denominator == null) {
            denominatorTemp = 0;
        } else {
            denominatorTemp = denominator;
        }

        if (numeratorTemp < 0 || denominatorTemp < 0) {
            throw new Exception();
        }

        return numeratorTemp / denominatorTemp;
    }
}
 
・DivideBad2.java
// 入力:分子はnull許容、分母は0は不可(例外送出)
// 出力:整数(負になり得る)、ただし分子や分母がnullの場合はnull
public class DivideBad2 extends Divide {

    public Integer Calc(Integer numerator, Integer denominator)
        throws Exception {

        int numeratorTemp;
        int denominatorTemp;

        if (numerator == null) {
            return null;
        } else {
            numeratorTemp = numerator;
        }

        if (denominator == null) {
            return null;
        } else {
            denominatorTemp = denominator;
        }

        return numeratorTemp / denominatorTemp;
    }
}
 
【結果】
[テストその1]
クラス名:Divide
分子  :6
分母  :2
結果  :3

クラス名:Divide
分子  :-6
分母  :2
結果  :-3

クラス名:Divide
分子  :6
分母  :0
結果  :想定外の例外!

クラス名:Divide
分子  :null
分母  :2
結果  :0

[テストその2]
クラス名:DivideBusiness
分子  :6
分母  :2
結果  :3

クラス名:DivideBusiness
分子  :-6
分母  :2
結果  :0

クラス名:DivideBusiness
分子  :6
分母  :0
結果  :6

クラス名:DivideBusiness
分子  :null
分母  :2
結果  :0

[テストその3]
クラス名:DivideBad1
分子  :6
分母  :2
結果  :3

クラス名:DivideBad1
分子  :-6
分母  :2
結果  :想定外の例外!

クラス名:DivideBad1
分子  :6
分母  :0
結果  :想定外の例外!

クラス名:DivideBad1
分子  :null
分母  :2
結果  :0

[テストその4]
クラス名:DivideBad2
分子  :6
分母  :2
結果  :3

クラス名:DivideBad2
分子  :-6
分母  :2
結果  :-3

クラス名:DivideBad2
分子  :6
分母  :0
結果  :想定外の例外!

クラス名:DivideBad2
分子  :null
分母  :2
結果  :想定外の値!