Java の interface を模倣する

2020年1月18日

クラス設計の技法の一つに、インタフェースと呼ばれるものがあります。オブジェクト指向らしい設計を実現するための、非常に重要な考え方です。

Java や C# だとこれが言語レベルで、すなわち interface というキーワードとして、サポートされています。一方、C++ にこのキーワードはありません。

ですが、考え方を採り入れることは可能です。というよりもむしろ、採り入れなければオブジェクト指向は完成しないと言ったほうが適切なぐらいです。

そこで今回は、C++ におけるインタフェースの書き方・目的・応用例・注意点について述べていきます。

インタフェースの書き方

インタフェースとはクラスの一種です。言い換えると、特定の条件を満たすクラスがインタフェースと呼ばれているに過ぎません。

そこで、C++ ではこう定義します。「純粋仮想関数だけで構成されているクラスがインタフェースである」と。

例えば次のクラスは、インタフェースです。

class IPortOutput
{
public:
  virtual void lower() = 0;
  virtual void raise() = 0;
  virtual bool isHigh() const = 0;
};

次のクラスは、インタフェースではありません。

class IPortOutput
{
private:
  bool m_isHigh;
public:
  virtual void lower() = 0;
  virtual void raise() = 0;
  virtual bool isHigh() const { return m_isHigh; }
};

メンバ変数があったり、関数の実装があったりするクラスは、インタフェースの条件を満たしません。

インタフェースの目的

インタフェースの目的とは、クラス相互の結合を疎にすることにあります。そしてその効果として、クラスの単体テストが可能になります。

ここで、過去記事「副作用を考える」を読まれたことのある方は、そこに登場する副作用の概念との類似点にお気づきになられたかもしれません。そうです。クラス相互の結合を疎にするとは、「クラスの持つ副作用を除去すること」に他なりません。

クラスの副作用にあたる行為

ここでもう一度、「関数の」副作用についておさらいしておきます。

  • グローバル変数を読む、あるいは書く
  • ペリフェラルを読む、あるいは書く
  • 標準入力または標準出力を使う
  • 自身がスタティック変数を持つ
  • マイコン固有の命令を使う
  • 副作用のある関数を呼ぶ

これらはそっくりそのまま「クラスの」副作用の条件にもなります。つまり、自身のメンバ関数が一つでも上記の行為を含んでいれば、それは副作用のあるクラスです。そしてクラスの場合、さらに次の行為も副作用に該当します。

  • 他のクラスのポインタをメンバ変数に持つこと

クラスの副作用にあたらない行為

次の行為は、似ていますが、クラスの副作用ではありません。

  • 他のクラスのインスタンスをメンバ変数に持つこと
  • グローバル変数のポインタをメンバ変数に持つこと
  • ペリフェラルのポインタをメンバ変数に持つこと
  • インタフェースのポインタをメンバ変数に持つこと

最後の項目に注目してください。「他のクラスの」ポインタを持つことは副作用ですが、「インタフェースの」ポインタを持つことは副作用ではないのです。これが、インタフェースの使い方の要諦です。

インタフェースの応用例

あるクラスが、他のクラスのポインタを持ちたくなるのは、どのようなときでしょうか。

例えば、LEDを扱うクラスを考えます。LEDを点灯したり消灯したりするには、ポート出力の上げ下げが必要です。ここでは、ポート出力はすでにクラスとして用意されていて、LEDはそのインスタンスを利用するだけでよいものとします。紙面の都合もあるので簡潔に書きますが、コードは次のようなものになるでしょう。

class PortOutput
{
public:
  void lower() { PORT_A &= ~0x01; }
  void raise() { PORT_A |=  0x01; }
};

class LED
{
private:
  PortOutput *const m_po;
public:
  explicit LED(PortOutput *po) : m_po(po) { }
  void turnOff() { m_po->lower(); }
  void turnOn()  { m_po->raise(); }
};

悪くないコードですが、このLEDクラスは「単体テストができない」という大きな弱点を抱えています。ポート出力クラスがペリフェラルに依存しており、ビルドして動かすためには実機が必要になるからです。

このペリフェラルへの依存を断ち切るには、インタフェースを利用して、コードを次のように修正すればいいのです。

class IPortOutput
{
public:
  virtual void lower() = 0;
  virtual void raise() = 0;
};

class LED
{
private:
  IPortOutput *const m_po;
public:
  explicit LED(IPortOutput *po) : m_po(po) { }
  void turnOff() { m_po->lower(); }
  void turnOn()  { m_po->raise(); }
};

こうすれば、LEDクラスは必ずしもペリフェラルに依存する必要はなくなります。単体テストのときは、例えば次のようなクラスのインスタンスを与えてビルドすればよいからです。

class MockOutput : public IPortOutput
{
public:
  virtual void lower() { printf("Low.\n"); }
  virtual void raise() { printf("High.\n"); }
};

もちろん、本来のポート出力クラスを次のように書けば、修正前の動きを損ねることもありませんね。

class PortOutput : public IPortOutput
{
public:
  virtual void lower() { PORT_A &= ~0x01; }
  virtual void raise() { PORT_A |=  0x01; }
};

インタフェースの注意点

最後に、インタフェースを活用するにあたっての注意点について述べておきます。

できるだけ小さく設計すること

インタフェースを設計する際は、呼ばれていない、つまり存在しなくてもビルドが通るようなメンバ関数は、迷わず削除してください。インタフェースのメンバ関数は、その一つ一つが単体テストの負担を重くします。一つのインタフェースに所属するメンバ関数は、1~3個程度がよい設計の目安です。

多重継承をいとわないこと

多重継承は設計を複雑にするので好ましくないとされますが、インタフェースに限っては別です。むしろインタフェース自体は小さく作り、多重継承を使ってインタフェースを一つ一つ注入するように設計しましょう。

クラス名にルールを設けること

冒頭で述べたように C++ は言語レベルでインタフェースをサポートしないため、クラス名でそれを表現し、チームの共通認識とする必要があります。世間一般では、インタフェースを表現するクラス名は I(大文字のアイ)で始めるのが多数派のようです。