17
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

完全に理解したTalkAdvent Calendar 2021

Day 9

SOLID原則をC++使って解説する

Last updated at Posted at 2021-12-09

この記事は「完全に理解したTalk Advent Calendar 2021」9日目の投稿記事です。

##はじめに
去年のアドベントカレンダーでは死ぬほどライブラリについて語りました。

今回はソフトウェア設計について語ります。

ソフトウェアの設計や開発は学校で多少習うと思いますが、正直確信的なことを教えてもらうことが少ないと思います。
c言語の授業だと、printfとかポインタとかマジで言語についての知識のみですね。
ソフトウェア設計の授業だとMVPとかアジャイル開発とかデザインパターンとかそんなもんでした。
もちろん自分もそういうのしか習っていません。

だからエンジニアやものづくり好きな人は自力で調べて身につけて己の力に変えていると思います。

ジョジョでは波紋があるからスタンドがあるみたいに、基礎があり応用があるわけです。(多分)
ソフトウェアの設計も基礎があり応用があります。今回はその基礎について書いていきます。

いや、原則について書いていきます。

*本記事はSOLIDの紹介のためソースコードの例を出していますがそもそもの設計等怪しいところがございます。あくまでSOLIDの紹介として読んでいただければと思います。

##SOLID
KISSの原則やYAGNI、DRYなどありますが、この中でも今回はSOLIDについて書いていきます。
SOLIDは5つの原則の頭文字を並べてSOLIDと書きます。
5つの原則について以下になります。
・SRP : 単一責務の原則(Single Responsibility Principle)
・OCP : オープン・クローズドの原則(Open–Closed Principle)
・LSP : リスコフの置換原則(Liskov Substitution Principle)
・ISP : インターフェイス分離の原則(Interface Segregation Principle)
・DIP : 依存関係逆転の原則(Dependency Inversion Principle)
これらをC++ソースコードと一緒に解説していきます。

SRP

SRPは Single Responsibility Principle の略で単一責務の原則という意味になります。

A class should have only one reason to change

つまりクラスを変更する理由は一つだけにしなさいという意味になります。
もっとわかりやすく言うと、クラスは1つのことだけ責任を負うべきということになります。
では、クラスは1つのことだけに責任を負うべきってどういう意味かソースコードを見ながら理解していきましょう。

SRPに違反したコード}
typedef std::string AudioFile;
class Audio{
public:
  bool start();
  bool stop();
};
class Uploader{
public:
  bool upload(AudioFile &audioFile);
};
class SoundRecord{
public:
  bool recordUpload(Audio &audio,Uploader &uploader);// <- !? SRP違反
  AudioFile encoder(std::string format,AudioFile audioFile);// <- !? SRP違反
};
int main() {
  Audio audio;
  Uploader uploader;
  SoundRecord soundRecord;
  soundRecord.recordUpload(audio,uploader);
  return 0;
}

SoundRecordクラスを作りました。SoundRecordクラスに怪しいメソッドがありますね、recordUpload()encoder()です。
ここでは、SoundRecordクラス任せたい責任とは、録音機能の管理です。アップロードやエンコーダーは他所でして欲しいです。
なので、SoundRecordクラスは録音以外に、アップロードやエンコーダーの役割が入ってしまいSRPの違反になります。
呼び出しの処理も目を疑ってしまいます。違反しないように書いてみましょう!

SRPに則ったコード}

typedef std::string AudioFile;
class Audio{
public:
  bool start();
  bool stop();
};
class Uploader{
public:
  bool upload(AudioFile &audioFile);
};
class SoundRecord{
public:
  bool recordStart(Audio &audio);
  bool recordStop(Audio &audio);
};
class Encoder{
public:
  static AudioFile encode(std::string format,AudioFile audioFile);
};

int main() {
  Audio audio;
  Uploader uploader;
  SoundRecord soundRecord;

  soundRecord.recordStart(audio);
  std::cout << "1 second later" << std::endl;
  soundRecord.recordStop(audio);
  auto uploadFile = Encoder::encode("mp3","record");
  uploader.upload(uploadFile);
  return 0;
}

Encoderをクラスにし、ファイルのエンコード処理の責任を持たせました。また、SoundRecordは録音処理に特化し、アップロードの処理を行わせないようにしました。これでそれぞれのクラスは単一の機能を持つことができました。

クラスに新たな責任を持たせないようにしよう!
クラスが多くなってしまって不安だと思いますが、
一つ一つの役割を一つ一つのクラスに持たせることが技術的負債を減らす大きな一歩です。

OCP

OCPはOpen Close Principleの略で開放閉鎖の原則という意味になります。

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

直訳すると、「クラス、モジュール、関数などは、拡張のために開いている必要がありますが、変更のために閉じている必要があります。」
クラスの動作を変更せずに拡張できる必要があることを意味します。これは不思議に思えるかもしれませんし、クラスを変更せずにクラスの動作をどのように変更できるのか疑問に思います。

まずは違反したソースコードをお見せします。

OCP違反したコード}

enum class SIZE { SMALL, MEDIUM, LARGE };
struct Fruit{
  int id;
  SIZE size;
  std::string name;
};
typedef  std::vector<Fruit> Fruits;

class BadSearch{
public:
  static Fruit* byId(Fruits fruits, const int id) { //OCP違反
    for (auto &i : fruits)
      if (i.id == id)
        return &i;
    return nullptr;
  }
  static Fruit* bySize(Fruits fruits, const SIZE size) { //OCP違反
    for (auto &i : fruits)
      if (i.size == size)
        return &i;
    return nullptr;
  }
  static Fruit* byIdAndSize(Fruits fruits, const int id,const SIZE size) { //OCP違反
    for (auto &i : fruits)
      if (i.size == size && i.id == id)
        return &i;
    return nullptr;
  }
};
int main() {
  const Fruits fruits{
          Fruit{0, SIZE::SMALL,"Orange"},
          Fruit{1, SIZE::LARGE,"Apple"},
          Fruit{1, SIZE::MEDIUM,"Apple"},
  };
  std::cout << BadSearch::byId(fruits,0)->name << std::endl;
  std::cout << BadSearch::bySize(fruits,SIZE::MEDIUM)->name << std::endl;
  std::cout << BadSearch::byIdAndSize(fruits,0,SIZE::SMALL)->name << std::endl;
  return 0;
}

ここでは、条件に合う果物を探すソースコードをお見せしました。
しかしこれはOCP違反になります。IDで検索する機能、サイズで検索する機能、そしてIDとサイズで検索するという3つの機能があります。しかし、新たなパラメータが追加され新たな検索機能の拡充が必要になった場合BadSearchに対して新たな検索機能を追加する必要が出てきます。

ここでOCPに違反しない書き方はどういうものでしょうか。
それは、検索ロジックのクラスとロジックを扱うクラスに分割し、インターフェースを作ることです。新たな検索ロジックが必要な場合は新たにクラスを作成するのみで、ロジックを扱うクラスに手を入れる必要がなくなります。

OCPに則ったコード}

enum class SIZE { SMALL, MEDIUM, LARGE };
struct Fruit{
  int id;
  SIZE size;
  std::string name;
};
typedef  std::vector<Fruit> Fruits;

template <typename T>
class CheckerInterface{//検索ロジックインターフェース
public:
  virtual bool check(T *item) const = 0;
};
class IdChecker:public CheckerInterface <Fruit>{ //ID検索ロジッククラス
  int id_;
public:
  IdChecker(int id):id_(id){}
  bool check(Fruit *item) const {return item->id == id_;}
};
class SizeChecker:public CheckerInterface <Fruit>{ //サイズ検索ロジッククラス
  SIZE size_;
public:
  SizeChecker(SIZE size):size_(size){}
  bool check(Fruit *item) const {return item->size == size_;}
};

template <typename T>
class SearchInterface{//検索インターフェース
public:
  virtual T* search(std::vector<T> items, const CheckerInterface<T> &checker)=0;
};
class SearchFruit:public SearchInterface<Fruit>{
public:
  SearchFruit() = default;
  Fruit* search(std::vector<Fruit> items, const CheckerInterface<Fruit> &checker) override {
    for (auto &p : items)
      if (checker.check(&p))//検索は検索ロジッククラスに任せる
        return &p;
    return nullptr;
  }
};

int main() {
  SearchFruit sf;
  const Fruits fruits{
          Fruit{0, SIZE::SMALL,"Orange"},
          Fruit{1, SIZE::LARGE,"Apple"},
          Fruit{1, SIZE::MEDIUM,"Apple"},
  };
  std::cout << sf.search(fruits, IdChecker(0))->name << std::endl;
  std::cout << sf.search(fruits, SizeChecker(SIZE::MEDIUM))->name << std::endl;
  return 0;
}

ロジックをオブジェクト単位で分けることで共通の呼び出しで振る舞いを変えることができます。
オブジェクトのチェック(if文など)で挙動を変えたり、キャストなどが必要になってしまっている場合は
一度OCP違反していないか確認してみよう!

LSP

LSPはLiskov Substitution Principle の意味で、リスコフの置換原則と言います。

Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.

数学色が強いですが、「すべての派生クラスが、それらの親クラスの代わりに使用できる必要がある」ことを意味します。
要するに、「基底クラスで動かしている処理は継承クラスに置き換えても動きますよね?
ということになります。

ちなみに、継承は「is a」の関係になります。例えば車クラスを継承したトラックは車であるので、「is a」の関係になります。

特にLSPで意識すると良いのは、事前事後の条件です。
1.事前条件は親クラスと同じか、それよりも弱める事ができ、逆に条件を強める事はできません。
2.事後条件は親クラスと同一か、それよりも強める事ができ、逆に条件を弱める事はできません。

このように事前事後の条件を考慮した開発方法を契約プログラミングと言います。

早速、LSP違反のコードをお見せします。

LSP違反コード}
#include <iostream>
class Encoder {
public:
  const std::string media;
  virtual void encode(std::function<void()> complete){
    std::cout << "encode!" << std::endl;
    complete();
  }
};
class WAV:public Encoder {
public:
  const std::string media = "wav";
  void encode(std::function<void()> complete) override{
    std::cout << "wav encode!" << std::endl;
    complete();
  }
};

class MP3:public Encoder {
public:
  const std::string media = "mp3";
  void encode(std::function<void()> complete) override{ //LSP違反
    std::cout << "mp3 encode!" << std::endl;
  }
  void mp3Encode(){
    std::cout << "mp3 encode!" << std::endl; //LSP違反
  }
};

bool testFunction(Encoder *encoder){
  if(encoder->media == "mp3"){ //OCP違反
    MP3 *mp3 = static_cast<MP3*>(encoder);
    mp3->mp3Encode();
  }else {
    encoder->encode([]() {
      std::cout << "finish" << std::endl;
    });
  }
}

int main(){
  Encoder *wav = new WAV;
  Encoder *mp3 = new MP3;

  testFunction(wav);
  testFunction(mp3);

  delete wav;
  delete mp3;
}

WAVクラスまでは問題ないですが、MP3クラスには2点問題があります。
Encoderクラスはencode関数内で、complete()コールバック関数を呼び出していますが、
MP3クラスでは呼び出しておらず、契約違反になり、LSP違反になります。

また、MP3クラスには、mp3encodeという独自の関数が生成され、
mp3encodeを呼び出したいため、OCPの違反も生じています。

LSPに則ったコード}

#include <iostream>
class Encoder {
public:
  const std::string media;
  virtual void encode(std::function<void()> complete){
    std::cout << "encode!" << std::endl;
    complete();
  }
};
class WAV:public Encoder {
public:
  const std::string media = "wav";
  void encode(std::function<void()> complete) override{
    std::cout << "wav encode!" << std::endl;
    complete();
  }
};

class MP3:public Encoder {
public:
  const std::string media = "mp3";
  void encode(std::function<void()> complete) override{
    mp3Encode();
    complete();
  }
  void mp3Encode(){
    std::cout << "mp3 encode!" << std::endl;
  }
};

bool testFunction(Encoder *encoder){
  encoder->encode([]() {
    std::cout << "finish" << std::endl;
  });
}

int main(){
  Encoder *wav = new WAV;
  Encoder *mp3 = new MP3;

  testFunction(wav);
  testFunction(mp3);

  delete wav;
  delete mp3;
}

MP3をEncoderクラスに合わせた実装をすることで解決しました。
mp3encoderクラスをどうしても実装したい場合は、Encoderクラスを継承しないことでLSP違反を回避することもできます。

一度規定クラスを代入し問題がないか確認してみましょう。
LSPに違反すると連鎖してOCPに違反することがあります。

LSPに関して、
@hmito さんからコメントいただきました。
newしたものをfreeしているのは問題なので訂正させていただきました。
コメントにて、LSPに則ったコードに対してのコメントがかなり勉強になりましたので、
参照いただけたらと思います。

ISP

Interface segregation principle の略です。インターフェース分離原則です。

Clients should not be forced to depend upon interfaces that they do not use.

不要なインターフェースの実装は強要しないようにしましょうという意味です。
これまでの原則よりわかりやすいと思います。まずは違反したコードをみてみましょう。

ISPに違反したコード}
class I2C {
public:
  virtual char address() = 0;
  virtual bool writeRegister(char data) = 0;
  virtual bool readRegister(const char *data) = 0;

  virtual void onAccelerometer(int x, int y, int z) = 0;
  virtual void onGyroscope(int roll ,int pitch,int yaw) = 0;
};

class Accelerometer: public I2C {
public:
  char address() override {
    return 0x1A;//i2cのアドレス
  }
  bool writeRegister(char data) override {
    std::cout << "加速度センサのレジスタを操作する処理が入る。" << std::endl;
  }
  bool readRegister(const char *data) override {
    std::cout << "加速度センサのレジスタからデータを読み取る処理が入る。" << std::endl;
  }

  void onAccelerometer(int x, int y, int z) override {
    std::cout << "加速度センサからの割り込みを使ってコールバックを呼び出す。" << std::endl;
  };
  void onGyroscope(int roll, int pitch, int yaw) override {
    std::cout << "ジャイロはないので意味ないインターフェースである。" << std::endl;//良くない
  };
};

class Gyroscope: public I2C {
public:
  char address() override {
    return 0x2A;//i2cのアドレス
  }
  bool writeRegister(char data) override {
    std::cout << "ジャイロセンサのレジスタを操作する処理が入る。" << std::endl;
  }
  bool readRegister(const char *data) override {
    std::cout << "ジャイロセンサのレジスタからデータを読み取る処理が入る。" << std::endl;
  }

  void onAccelerometer(int x, int y, int z) override {
    std::cout << "加速度センサはないので意味ないインターフェースである。" << std::endl;//良くない
  };
  void onGyroscope(int roll, int pitch, int yaw) override {
    std::cout << "ジャイロセンサからの割り込みを使ってコールバックを呼び出す。" << std::endl;
  };
};


上記のI2CクラスはI2C通信(シリアル通信の一種)を用いてセンサ設定の読み書きを行うインターフェースと
加速度センサ、ジャイロセンサのインターフェースが実装されています。

しかし、ISPに違反してしまっています。なぜならば、
実際I2Cを継承した**AccelerometeronGyroscopeの実装を強要され**、
GyroscopeonAccelerometerの実装を強要されてしまっています。

このISP違反を二つのパターンで防いでみます。
まずは多重継承で防いでみます。

・多重継承したパターン

ISPに則ったコード1}

class I2C {
public:
  virtual char address() = 0;
  virtual bool writeRegister(char data) = 0;
  virtual bool readRegister(const char *data) = 0;
};
class AccelerometerEvent{
public:
  virtual void onAccelerometer(int x, int y, int z) = 0;
};
class GyroscopeEvent{
public:
  virtual void onGyroscope(int roll ,int pitch,int yaw) = 0;
};

class Accelerometer: public I2C ,public AccelerometerEvent{
public:
  char address() override {
    return 0x1A;//i2cのアドレス
  }
  bool writeRegister(char data) override {
    std::cout << "加速度センサのレジスタを操作する処理が入る。" << std::endl;
  }
  bool readRegister(const char *data) override {
    std::cout << "加速度センサのレジスタからデータを読み取る処理が入る。" << std::endl;
  }

  void onAccelerometer(int x, int y, int z) override {
    std::cout << "加速度センサからの割り込みを使ってコールバックを呼び出す。" << std::endl;
  };
};

class Gyroscope: public I2C ,public GyroscopeEvent{
public:
  char address() override {
    return 0x2A;//i2cのアドレス
  }
  bool writeRegister(char data) override {
    std::cout << "ジャイロセンサのレジスタを操作する処理が入る。" << std::endl;
  }
  bool readRegister(const char *data) override {
    std::cout << "ジャイロセンサのレジスタからデータを読み取る処理が入る。" << std::endl;
  }
  void onGyroscope(int roll, int pitch, int yaw) override {
    std::cout << "ジャイロセンサからの割り込みを使ってコールバックを呼び出す。" << std::endl;
  };
};

・多重継承しないパターン

ISPに則ったコード2}
class I2C {
public:
  virtual char address() = 0;
  virtual bool writeRegister(char data) = 0;
  virtual bool readRegister(const char *data) = 0;
};
class AccelerometerEvent:public I2C{
public:
  virtual void onAccelerometer(int x, int y, int z) = 0;
};
class GyroscopeEvent:public I2C{
public:
  virtual void onGyroscope(int roll ,int pitch,int yaw) = 0;
};

class Accelerometer: public AccelerometerEvent{
public:
  char address() override {
    return 0x1A;//i2cのアドレス
  }
  bool writeRegister(char data) override {
    std::cout << "加速度センサのレジスタを操作する処理が入る。" << std::endl;
  }
  bool readRegister(const char *data) override {
    std::cout << "加速度センサのレジスタからデータを読み取る処理が入る。" << std::endl;
  }

  void onAccelerometer(int x, int y, int z) override {
    std::cout << "加速度センサからの割り込みを使ってコールバックを呼び出す。" << std::endl;
  };
};

class Gyroscope: public GyroscopeEvent{
public:
  char address() override {
    return 0x2A;//i2cのアドレス
  }
  bool writeRegister(char data) override {
    std::cout << "ジャイロセンサのレジスタを操作する処理が入る。" << std::endl;
  }
  bool readRegister(const char *data) override {
    std::cout << "ジャイロセンサのレジスタからデータを読み取る処理が入る。" << std::endl;
  }
  void onGyroscope(int roll, int pitch, int yaw) override {
    std::cout << "ジャイロセンサからの割り込みを使ってコールバックを呼び出す。" << std::endl;
  };
};

違反したまま、他のセンサを追加しようとするとさらに不要なインターフェースの実装を強要され技術的負債が積まれていきます。また、超音波センサーを追加した時、I2CクラスにonUltrasonic()関数を追加したとき大変なことが起きることが容易に想像できます。

必要なインターフェースのみを実装するためには、抽象クラスのメソッドを別の抽象クラスに定義していくことがコツになります。
もし、新たなクラスを生成した際に空の実装を余儀なくしてしまった時はインターフェースを見直すとよさそうです。

DIP

Dependency inversion principle の略で、「依存関係逆転の原則」といいます。

High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.

訳すと、
・上位モジュールは下位モジュールに依存せず、どちらもインターフェースに依存する必要があります。
・インターフェースは実装について依存しない。実装の詳細はインターフェースに依存する必要があります。
となります。
この2点が重要となります。

上位モジュールと下位モジュールについて

上位モジュールは下位モジュールを扱い、
下位モジュールは上位モジュールに扱われる。
(DIP違反ですが、)プログラムで書くとこんな感じです。

class LowLevelModule{
public:
 void print(){std::cout << "low level module" << std::endl;}
}
class HighLevelModule{
 LowLevelModule lowLevelModule;
public:
 void print(){lowLevelModule.print();}
}

DIP違反のコード

DIP違反したコード1}
#include <iostream>
#include <libc.h>
#include <thread>

class Led{
public:
  void turnOn(){
    std::cout << "Led turn on" << std::endl;
  }
  void turnOff(){
    std::cout << "Led turn off" << std::endl;
  }
};
class Button{
  bool state = false;
public:
  Led led; //DIP違反 ButtonがLedに依存している。
  void startPolling(){
    std::thread([this]() {
      for(int i=0;i<5;i++){
        /*
       * switch
       */
        state = !state;
        std::cout << "Switched the button" << std::endl;
        if(state){
          led.turnOn(); //ledに対しての操作になってしまっている。
        }
        else{
          led.turnOff(); //ledに対しての操作になってしまっている。
        }
        sleep(1);
      }
    }).join();
  }
};
int main(){
  Button b;
  b.startPolling();

}

Buttonクラス(下位モジュール)にLedクラス(上位モジュール)が依存している状態です。
上位モジュールと下位モジュールを交換しても変わらず違反になります。

DIP違反したコード2}

class Button{
  bool state = false;
public:
  std::function<void(bool)> callback = nullptr;
  void poll(){
    std::thread([this]() {
      for(int i=0;i<5;i++){
        /*
       * switch
       */
        state = !state;
        std::cout << "Switched the button" << std::endl;
        if(callback){
          callback(state);
        }
        sleep(1);
      }
    }).join();
  }
};
class Led{
public:
  Button button; //DIP違反 LedがButtonに依存している。。
  Led(){
    button.callback = [this](bool state){
      if(state){
        turnOn();
      }
      else{
        turnOff();
      }
    };
    button.poll();
  }
  void turnOn(){
    std::cout << "Led turn on" << std::endl;
  }
  void turnOff(){
    std::cout << "Led turn off" << std::endl;
  }
};

モジュールを入れ替えましたが結局、
Ledクラス(下位モジュール)にButtonクラス(上位モジュール)が依存している状態です。
このままですと、Ledの代わりにLampやモーターを動かしたい場合、Buttonクラスを変更する必要があります。
また、Buttonの代わりにリモコンやキーボードなどでLedを動かしたい場合、Ledクラスを変更する必要があったりします。

依存をなくすためには、やはり、

上位モジュールは下位モジュールに依存せず、どちらもインターフェースに依存する必要があります。

を行う必要がありそうです。

DIPに則ったコード1}

class Interface{
public:
  virtual void turnOn() = 0;
  virtual void turnOff() = 0;
};
class Led:public Interface{
public:
  void turnOn() override {
    std::cout << "Led turn on" << std::endl;
  }
  void turnOff() override {
    std::cout << "Led turn off" << std::endl;
  }
};
class Lamp:public Interface{
public:
  void turnOn() override {
    std::cout << "Lamp turn on" << std::endl;
  }
  void turnOff() override {
    std::cout << "Lamp turn off" << std::endl;
  }
};
class Button{
  std::shared_ptr<Interface> module_ = nullptr; //Interfaceに依存し原則を守ります。
  bool state = false;
public:
  Button(std::shared_ptr<Interface> module){
    module_ = module;
  }
  void poll(){
    std::thread([this]() {
      for(int i=0;i<5;i++){
        /*
       * switch
       */
        state = !state;
        std::cout << "Switched the button" << std::endl;
        if(state){
          if(module_)module_->turnOn();
        }
        else{
          if(module_)module_->turnOff();
        }
        sleep(1);
      }
    }).join();
  }
};
int main(){
  Button button(std::make_shared<Led>());//Ledの操作が可能になった
  button.poll();
  Button button2(std::make_shared<Lamp>());//Lampの操作も可能になった
  button2.poll();
}

今まで、ButtonはLedに依存していましたがインターフェースに依存するように変更しました。
Interfaceクラスに依存しているので、Lampクラスに対しても操作ができるようなりました。

DIPに違反した二つ目のコードに対しても違反しないようにしてみましょう。

DIPに則ったコード2}

class Delegate{ //Buttonコールバック関数群
public:
  virtual void on() = 0;
  virtual void off() = 0;
};
class Interface{ //ButtonのInterface
public:
  Delegate* delegate = nullptr;
  virtual void poll() = 0;
};
class Button:public Interface{
  bool state = false;
public:
  void poll() override {
    std::thread([this]() {
      for(int i=0;i<5;i++){
        state = !state;
        std::cout << "Switched the button" << std::endl;
        if(delegate){
          if(state)
            delegate->on();
          else
            delegate->off();
        }
        sleep(1);
      }
    }).join();
  }
};
class Led:public Delegate{ //Ledクラスにdelegateを実装する。
public:
  std::shared_ptr<Interface> module_;
  Led(std::shared_ptr<Interface> module){
    module_ = module;
    module_->delegate = this; //moduleにコールバックを渡します。
    module_->poll();
  }
  void turnOn(){
    std::cout << "Led turn on" << std::endl;
  }
  void turnOff(){
    std::cout << "Led turn off" << std::endl;
  }
protected:
  void on() override {
    turnOn();
  }
  void off() override {
    turnOff();
  }
};
int main(){
  Led l(std::make_shared<Button>());
}

いかがでしょうか。
DIPに違反しないためにはインターフェースを通すことが重要になります。

クラス内でダイレクトに定義するのではなくどれもインターフェースを定義しておきましょう。

DIについて

DIという言葉もあるのでここで簡単に説明します。DIは dependency injection と言い、
依存性の注入と言います。クラス間に生じる依存関係をクラス内のコードをいじらずに外部から何らかの形で与えるようにする技です。
メリットとしてはコードを変えずに単体テストや振る舞いを変えることができることです。
サッとコードも載せておきます。

DIについて}

class Interface{
public:
  virtual void action()=0;
};

class AModule{
public:
  Interface *module_ = nullptr;
  AModule(Interface *module){
    module_ = module;
  }
  ~AModule(){
    delete module_;
  }
  void action(){
    module_ -> action();
  }
};

class BModule : public Interface{
public:
  void action() override{
    std::cout << "Production action!!" << std::endl;
  }
};

class MockBModule : public Interface{
public:
  void action() override{
    std::cout << "Unit test action!!" << std::endl;
  }
};

int main(){
  bool mock = false;
  AModule *aModule = nullptr;
  if(mock){
    aModule = new AModule(new MockBModule); // DI(依存性注入)実行!単体テストならMockBModuleを動かす。
  }else{
    aModule = new AModule(new BModule); // DI(依存性注入)実行!本番環境ならBModuleを動かす。
  }
  aModule -> action();
  delete aModule;
}

##最後
SOLIDについてどうでしょうか、理解できたでしょうか。
このように原則に基づいて設計し開発していくことが、技術的負債や機能の拡充をスムージに行い、バグが少なく生み出しづらい
ソフトウェアを作ることができます。

ポエム

ここからはポエムになります。

原則について理解し、過去の自分のプログラムを見ると原則違反している点がたくさん出てこないでしょうか?
実際、自分は大量の原則違反に気づきのたうち回っています><
原則をきちんと守ることで技術的負債を大幅に抑えることができます。そして成長していく、成長させていくソフトウェアを作ることが可能です。

しかし、0から1の開発やスタートアップした開発を進めるにあたり、きっちり原則を守っていった方がいいのでしょうか?
原則にとらわれて作りたいものを作ることができない、なんてこともあり得そうです。
実際、「机上の設計で満足したもの」と「とりあえず作って完成させたもの」どちらが世の中に認めてもらえるかというと後者にあたります。

言いたいことは何かと言いますと、
必要に応じて原則を適用していくということです。
全体の設計がイケてなくても、ある機能においては拡充の可能性があればそこについて原則を適用させていく、
または、細かいスパンで原則を適用していく(リファクタする)という形で作り上げていくことが重要だと思います。

そして素早くローンチしていくことだと思います。

##最後の最後

qiita投稿も登壇も全然できなかった年なので、来年こそは・・・!
次の投稿は、微分方程式とか出してみようかなと。

読んでいただきありがとうございました!

17
19
2

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
17
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?