Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ソフトウェア設計原則【SOLID】を学ぶ #5 リスコフの置換原則

k-abe
November 29, 2023

ソフトウェア設計原則【SOLID】を学ぶ #5 リスコフの置換原則

2023/11/30(木)に実施する社内勉強会、X スペース 【連続講座】ソフトウェア設計原則【SOLID】を学ぶ #5 リスコフの置換原則の資料です。

勉強会概要:
https://k-abe.connpass.com/event/297777/

Xスペース(録音)
https://twitter.com/i/spaces/1nAKEaOpWzVKL

※URLリンクを多用しています。リンクが有効な資料はこちらを参照ください。
https://www.docswell.com/s/juraruming/Z6Y8X7-2023-11-30-081051

k-abe

November 29, 2023
Tweet

More Decks by k-abe

Other Decks in Technology

Transcript

  1. 目次 自己紹介 SOLID について リスコフの置換原則(Liskov Substitution Principle )について 原則違反の例 サンプルコードについて

    原則に則った例 今回の設計所感 設計についてのディスカッション・質問 参考資料 2
  2. 自己紹介 名前: 阿部 耕二(あべ こうじ) 所属: パーソルクロステクノロジー株式会社 第1 技術開発本部 第4 設計部 設計2

    課 医療機器の組込みソフトウェア開発。C 言語。 趣味: 宇宙開発(リーマンサットプロジェクト広報メンバー) LAPRAS ポートフォリオ: https://lapras.com/public/k-abe Twitter: @juraruming 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 3
  3. SOLID について 設計の5 原則の頭文字をとったもの。 S 単一責務の原則(Single Respomsibility Principle ) O

    オープン・クローズドの原則(Open Closed Principle L リスコフの置換原則( Liskov Substitution Principle ) I インターフェイス分離の原則(Interface Segregation Principle ) D 依存関係逆転の原則(Dependency Inversion Principle ) 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 4
  4. リスコフの置換原則( Liskov Substitution Principle )について 出典: wikipedia サブタイプのオブジェクトはスーパータイプのオブジェクトの仕様 に従わなければならない 基底型オブジェクトを派生型オブジェクトで型安全に代替できるこ

    と ※この資料ではつぎの用語の定義とする スーパータイプ → 基底クラス・スーパークラス, サブタイプ → 派生 クラス・サブクラス 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 6
  5. サンプルコードについて サンプルコードはこちらのGitHub リポジトリに格納している。 原則違反のサンプルコード サンプルコードコード内容 GitHub リポジトリのディレクトリ名 1. サブクラスに実装 no_lsp_add_impl_sub_class

    2. 事前条件 ng_preconditions 3. 事後条件 ng_postconditions 4. 不変条件 ng_invaritants 5. 例外 ng_exception 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 7
  6. 原則に則ったサンプルコード サンプルコードコード内容 GitHub リポジトリのディレクトリ名 1. サブクラスに実装 ok_lsp_add_impl_sub_class 2. 事前条件 ok_preconditions

    3. 事後条件 ok_postconditions 4. 不変条件 ok_invaritants 5. 例外 ok_exception 今回の設計所感 -> 継承で使われる意図がないことを明示する no_inheritance 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 8
  7. サンプルコードの実行方法について make とC++ コンパイラが必要 GitHub リポジトリを自分のローカル環境にzip ダウンロードもしく はgit clone する。

    確認したいサンプルコードのディレクトリでmake を実行する ディレクトリ名.app の実行ファイルができるので実行する 私の確認環境(MacOS, clang で確認) $ gcc -v Apple clang version 15.0.0 (clang-1500.0.40.1) Target: x86_64-apple-darwin22.6.0 Thread model: posix InstalledDir: /Library/Developer/CommandLineTools/usr/bin 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 9
  8. // Rectangle.hpp class Rectangle { protected: int width, height; public:

    Rectangle(const int width, const int height) : width(width), height(height) {} virtual void setWidth(const int w); virtual void setHeight(const int h); int getWidth() const ; int getHeight() const ; int area() const ; }; 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 12
  9. // Square.hpp #include "Rectangle.hpp" class Square : public Rectangle {

    public: Square(int size) : Rectangle(size, size) {} void setWidth(const int w) override; void setHeight(const int h) override; }; 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 13
  10. // Rectangle.cpp #include "Rectangle.hpp" void Rectangle::setWidth(const int w) { width

    = w; } void Rectangle::setHeight(const int h) { height = h; } int Rectangle::getWidth() const { return width; } int Rectangle::getHeight() const { return height; } int Rectangle::area() const { return width * height; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 14
  11. // Square.cpp #include "Rectangle.hpp" #include "Square.hpp" void Square::setWidth(const int w)

    { width = height = w; } void Square::setHeight(const int h) { width = height = h; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 15
  12. // no_lsp_add_impl_sub_class.cpp #include <iostream> using namespace std; #include "Rectangle.hpp" #include

    "Square.hpp" void process(Rectangle& r) { int w = r.getWidth(); r.setHeight(10); std::cout << "expected area = " << (w * 10) << ", got " << r.area() << std::endl; } int main() { Rectangle r(5, 5); process(r); // expected area = 50, got 50 Square s(5); process(s); // expected area = 50, got 100, LSP violation! return 0; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 16
  13. 2. 事前条件をサブクラスで強めている 事前条件:メソッドの引数など 基底クラスの定義 // Parent.hpp #ifndef PARENT_HPP_ #define PARENT_HPP_

    class Parent { public: virtual void doWork(int value); }; #endif // PARENT_HPP_ 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 18
  14. 基底クラスの実装 value < 0 か判定している。 // Parent.cpp #include <iostream> #include

    "Parent.hpp" using namespace std; void Parent::doWork(int value) { if (value < 0) { throw std::invalid_argument("Parent requires value >= 0"); } // 作業をする cout << "Parent value = " << value << endl; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 19
  15. サブクラスの定義 // Child.hpp #include "Parent.hpp" class Child : public Parent

    { public: void doWork(int value) override; }; 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 20
  16. サブクラスの実装 value < 10 か判定している。基底クラスは value < 0 の判定だった。 事前条件を強化(条件が厳しく)

    しているため基底クラスとサブクラ スが置換不可になっている。 // Child.cpp #include <iostream> #include "Child.hpp" using namespace std; void Child::doWork(int value) { if (value < 10) { throw std::invalid_argument("Child requires value >= 10"); // 事前条件を強化している } // 子クラス固有の作業をする cout << "Child value = " << value << endl; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 21
  17. 実行結果: 基底クラスと同じ引数をサブクラスに指定した場合 // ng_preconditions.cpp #include <iostream> using namespace std; #include

    "Parent.hpp" #include "Child.hpp" int main() { Parent parent; parent.doWork(0); // Parent value = 0 Child child; child.doWork(10); // Child value = 10 // 例外発生 std::invalid_argument: Child requires value >= 10 child.doWork(0); return 0; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 22
  18. 3. 事後条件をサブクラスで弱めている 事後条件:メソッドの戻り値など 基底クラスの定義 // Parent.hpp class Parent { public:

    virtual int getValue(); }; 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 23
  19. 基底クラスの実装 // Parent.cpp #include <iostream> #include "Parent.hpp" using namespace std;

    int Parent::getValue() { // 常に正の値を返す return 42; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 24
  20. サブクラスの定義 // Child.hpp #include "Parent.hpp" class Child : public Parent

    { public: int getValue() override; }; 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 25
  21. サブクラスの実装 // Child.cpp #include <iostream> #include "Child.hpp" using namespace std;

    int Child::getValue() { int val = Parent::getValue(); // 事後条件を弱化している(負の値を返す可能性がある) return val - 50; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 26
  22. 実行結果: サブクラスが負の数を返している(基底クラスは正の数の戻 り値を想定している) int main() { int ret_val; Parent parent;

    ret_val = parent.getValue(); cout << "Parent return value = " << ret_val << endl; // Parent return value = 42 Child child; ret_val = child.getValue(); cout << "Child return value = " << ret_val << endl; // Child return value = -8 return 0; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 27
  23. 4. 不変条件をサブクラスで保持していない 基底クラスの条件をサブクラスで保持していない、条件を緩めるなど した場合 基底クラスの定義 // Parent.hpp class Parent {

    protected: int value; // 不変条件: 常に正の数 public: Parent(int val) : value(val >= 0 ? val : throw std::invalid_argument("value must be non-negative")) {} virtual void setValue(int val); }; 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 28
  24. 基底クラスの実装 // Parent.cpp #include <iostream> #include "Parent.hpp" using namespace std;

    void Parent::setValue(int val) { if (val < 0) { throw std::invalid_argument("value must be non-negative"); } value = val; cout << "Parent value = " << value << endl; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 29
  25. サブクラスの定義 // Child.hpp #include "Parent.hpp" class Child : public Parent

    { public: Child(int val) : Parent(val) {} void setValue(int val) override; }; 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 30
  26. サブクラスの実装 #include <iostream> #include "Child.hpp" using namespace std; void Child::setValue(int

    val) { if (val < -10) { // 親クラスよりも許容範囲を狭めている throw std::invalid_argument("Child requires value >= -10"); } // 基底クラスの不変条件「正の数」を破っている value = val; cout << "Child value = " << value << endl; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 31
  27. 実行結果: 基底クラスのメンバ変数は常に正の数というきまりをサブク ラスで破っている // ng_invaritants.cpp int main() { Parent parent(0);

    parent.setValue(1); // Parent value = 1 Child child(0); child.setValue(-10); // Child value = -10 return 0; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 32
  28. 5. サブクラスで独自の例外を投げている 基底クラスの定義 // Parent.hpp class Parent { public: virtual

    void doSomething(); }; 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 33
  29. 基底クラスの実装 // Parent.cpp #include <iostream> #include "Parent.hpp" using namespace std;

    void Parent::doSomething() { cout << "Parent -> doSomething() execute." << endl; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 34
  30. サブクラスの定義 // Child.hpp #include "Parent.hpp" class Child : public Parent

    { public: void doSomething() override; }; 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 35
  31. サブクラスの実装 // Child.cpp #include <iostream> #include "Child.hpp" using namespace std;

    void Child::doSomething() { throw std::runtime_error("Error occurred"); // 基底クラスが予期しない例外を投げる cout << "Child -> doSomething() execute." << endl; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 36
  32. 実行結果: 基底クラスは例外を投げないがサブクラスが例外を投げてい る // ng_exception.cpp int main() { Parent parent;

    parent.doSomething(); // Parent -> doSomething() execute. Child child; child.doSomething(); // 例外発生: std::runtime_error: Error occurred return 0; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 37
  33. 共通インターフェース: 長方形、正方形はこのクラスを実装する // Shape.hpp class Shape { public: virtual int

    area() const = 0; virtual int getWidth() const = 0; virtual int getHeight() const = 0; virtual void setWidth(const int w) = 0; virtual void setHeight(const int h) = 0; virtual ~Shape() {} }; 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 41
  34. 長方形の定義 // Rectangle.hpp #include "Shape.hpp" class Rectangle : public Shape

    { protected: int width, height; public: Rectangle(const int width, const int height) : width(width), height(height) {} int area() const override; int getWidth() const override; int getHeight() const override; void setWidth(const int w) override; void setHeight(const int h) override; }; 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 42
  35. 長方形の実装 // Rectangle.cpp #include "Rectangle.hpp" int Rectangle::area() const { return

    width * height; } int Rectangle::getWidth() const { return width; } int Rectangle::getHeight() const { return height; } void Rectangle::setWidth(const int w) { width = w; } void Rectangle::setHeight(const int h) { height = h; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 43
  36. 正方形の定義 // Square.hpp #include "Shape.hpp" class Square : public Shape

    { private: int size; public: Square(int size) : size(size) {} int area() const override; int getWidth() const override; int getHeight() const override; void setWidth(const int w) override; void setHeight(const int h) override; }; 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 44
  37. 正方形の実装 // Square.cpp #include "Square.hpp" int Square::area() const { return

    size * size; } int Square::getWidth() const { return size; } int Square::getHeight() const { return size; } void Square::setWidth(const int w) { size = w; } void Square::setHeight(const int h) { size = h; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 45
  38. 実行結果: 正方形・長方形は共通インターフェースを実装し、長方形・ 正方形それぞれ独自の実装を進めることができた。 // ok_lsp_add_impl_sub_class.cpp void process(Shape& shape) { int

    w = shape.area() / shape.getHeight(); shape.setHeight(10); std::cout << "expected area = " << (w * 10) << ", got " << shape.area() << std::endl; } int main() { Rectangle r(5, 5); process(r); // expected area = 50, got 50 Square s(5); process(s); // expected area = 100, got 100, LSP is not violation! return 0; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 46
  39. 2. 【原則違反改善例】事前条件をサブクラスで強めている サブクラスは基底クラスと同じ事前条件にするか、もしくは事前条 件を弱く(緩めれば)すれば置換可能。 // Child.cpp void Child::doWork(int value) {

    // if (value < 10) { // throw std::invalid_argument("Child requires value >= 10"); // 事前条件を強化している // } // 基底クラスの事前条件を維持 ( または下記を削除することで事前条件を緩める) Parent::doWork(value); // 子クラス固有の作業をする cout << "Child value = " << value << endl; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 47
  40. 3. 【原則違反改善例】事後条件をサブクラスで弱めている サブクラスは基底クラスと同じ事後条件にするか、もしくは事前条 件を強く(より大きな値に)すれば置換可能。 // Child.cpp int Child::getValue() { int

    val = Parent::getValue(); // LSP NG: 事後条件を弱化している(負の値を返す可能性がある) // return val - 50; // LSP OK: 事後条件を強化している(より大きな正の値を返す) return val + 10; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 48
  41. 4. 【原則違反改善例】不変条件をサブクラスで保持していない 基底クラスの不変条件を使えば置換可能 // Child.cpp void Child::setValue(int val) { //

    LSP NG: 親クラスよりも許容範囲を狭めている // if (val < -10) { // throw std::invalid_argument("Child requires value >= -10"); // } // // 基底クラスの不変条件「正の数」を破っている // value = val; // LSP OK: 親クラスの不変条件を保持する Parent::setValue(val); cout << "Child value = " << this->value << endl; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 49
  42. 5. 【原則違反改善例】サブクラスで独自の例外を投げている 基底クラスが予期しない例外をなげなければ置換可能 // Child.cpp void Child::setValue(int val) { void

    Child::doSomething() { // LSP NG: 基底クラスが予期しない例外を投げる // throw std::runtime_error("Error occurred"); // LSP OK: 基底クラスが予期しない例外は投げない cout << "Child -> doSomething() execute." << endl; } 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 50
  43. C++ であればfinal, C# であればsealed のキーワードで継承を禁止する ことができる。 クラス定義時にデフォルトfinal にしておくようにすれば(社内のコー ディング規約で決めるなど)継承によるリスコフ置換原則違反を防止 することができる。

    設計者の意図(このクラスは継承することを前提にしていない)を明 示することもできる。 // Rectangle.hpp class Rectangle final { } Rectangle を継承するコードはコンパイルエラーになる。 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 58
  44. 動画ではWindows Form アプリケーションのText のコントロール部品 のクラスを継承して拡張する例が紹介されていた。 Text に入力されていた文字数がxx 文字以上だったら入力文字の色を xx にする、みたいな動き

    アプリケーションの全体、複数画面で統一感のある挙動を設定できる などの効果がある。 継承も使いところによっては有効な場面があると思うので探していき たいと感じた。 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 60
  45. 参考資料 1. オブジェクト指向の原則2:リスコフの置換原則と継承以外の解決 方法 Udemy の講座。作成者はピーコック アンダーソンさん。リスコフの 置換原則以外のSOLID 原則の講座もあり。 2.

    オブジェクト指向習得のための5ステップ【SOLID 原則】 3. テスト駆動開発による組み込みプログラミング―C 言語とオブジェク ト指向で学ぶアジャイルな設計 【連続講座】ソフトウェア設計原則【SOLID 】を学ぶ #5 リスコフの置換原則 63