Magicode logo
Magicode
11 min read

【オブジェクト指向プログラミング】誰のもの?で理解するインターフェース

https://cdn.apollon.ai/media/notebox/997c17c5-6cb0-4a92-a91a-68b6722d315d.jpeg

前置き

背景

オブジェクト指向プログラミングのインターフェースについて、以下のような例で学んだ方は多いのではないでしょうか。
public interface Animal {
    void bark(); 
}

public class Dog implements Animal {
    void bark() {
        System.out.println("ワン");
    }
}
public static void main(String[] args){
  Animal animal = new Dog(); // Animal型の変数にDogインスタンスを代入できる
  animal.bark();
}
上記は極端な例ですが、
  • Dog dog = new Dog(); と比べて何がいいの?」 という疑問に答えてくれる情報をなかなか見つけることができず、言語仕様と割り切って覚えてきた方は、実際に多いのではないでしょうか。
この記事では、一般的な説明とは少し異なる切り口から、インターフェースを解説したいと思います。
指針
インターフェースを定義するとき、「インターフェースは誰のもの?」ということについて、強くイメージしてコードを書いているように思います。
この感覚をシェアできれば、インターフェースについて皆さんの理解を深める助けになるのではないかと思います。
そこで、この記事では、前半で「インターフェースは誰のもの?」というテーマでインターフェースという概念の「見方」を説明し、後半でインターフェースの使い方を解説したいと思います。

想定読者

インターフェースの言語仕様を押さえていることが前提です。
※どの言語でも構いません。個別言語で細かな違いがあったり、interfaceの用途が広い場合がありますが、一般的なインターフェースの機能は必ずカバーしていると思われるためです。
例えば以下のような方を読者として想定しています。
  • 言語仕様は分かっているものの、いまいちインターフェースの使い道が分かっていない初学者
  • 今までなんとなくインターフェースを使ってきた、あるいは避けてきたエンジニア

サンプルコードの言語

オブジェクト指向プログラミングにおけるインターフェースを理解することが目的ですが、サンプルコードが必要です。今回はTypeScript風言語で記載します。
※選定に特段意味はありません。

注意点

    1. 本記事のサンプルコードは、よくあるオブジェクト指向教材と同様、Dogなど実用性のないものを使います。そちらのほうがシンプルだからです。実用的なインターフェースの使い方については、別記事で取り上げます。
    1. インターフェースとは、そもそもクラスやif文のように頻繁に使用するものではありません。この記事でインターフェースを理解しても、むやみに大量のインターフェースを定義しないように気をつけてください。

1. 前半:インターフェースは誰のもの?

1-1. サンプルコード

「インターフェースは誰のもの?」について考えるために、以下をサンプルコードとします。
Dogはワンなど、鳴き声を管理する責務を持っています。SomePersonは聞き取った物事を聞き取って何かをする責務を持っています。
そして、Personインターフェースを定義しています。コードがこれだけであればインターフェースは意味を為しませんが、今後の改修の性質次第では、インターフェースが役に立ちます(この記事を通して、それを理解できると思います)
[型]
class Dog {
  bark(target: Person) {
      target.hear('ワン');
  }
}

interface Person {
    hear(sound: string): void;
}

class SomePerson implements Person {
    hear(sound: string) {
        console.log(sound)
    }
}
[スクリプト]
const dog = new Dog();
const person = new SomePerson();
dog.bark(person); // console.log("ワン")となる

1-2. クイズ:インターフェースは誰のもの?

本題の「インターフェースは誰のもの」について、解説を見る前に30秒くらい時間を取って考えてみていただきたいです。
このPersonインターフェースは、SomePersonのものでしょうか?それともDogのものでしょうか?
もしフォルダを分けるとしたら、インターフェースはどちらに入れますか?
※すぐに正解を出しますので、スクロールしないようにお気をつけください。

1-3. 正解発表

正解は、Bです。
もちろん状況に寄りますが、アプリケーション開発においては9割型、Bが正解になります。
ここで迷いなくBを選べた方は、この先の記事から得るものはないかもしれません。
ここで迷った方やAを選んでしまった方は、最後まで読んでいただきたいです。

1-4. 解説

シンプルな現実世界のたとえ話からイメージを膨らませていければと思います。

1-4-1. 現実世界の例でイメージを膨らませる

例えばここにType-Cで充電できるPCと、周辺機器メーカの充電器があるとします。
このとき、「充電器はType-Cでつながなければならない」という制約は、PC側が提示するものです。Mic●osoftがPCを作って、取扱説明書や仕様書に「充電はType-Cで行う」と記載しているはずです。
そして充電器がこの制約を満たしているため、この充電器でPCを充電することができます。
この図における、「充電器はこう」というPC側が提示している制約が、オブジェクト指向プログラミングにおけるインターフェースのイメージに近いです。

1-4-2. このイメージからDogの例を理解する

コードでインターフェースを定義するときの感覚は、上記の構造に良く似ています。
この構造をDogの例に適用すると、次のようなイメージになります。
PersonインターフェースがDogのものである、という感覚がお分かりいただけますでしょうか。
このイメージを見た上で、もう一度コードを眺めてみてください。 DogPersonの強い結びつきが見えてくるかと思います。
[型(再掲)]
class Dog {
  bark(target: Person) {  // DogにとってのPersonを受け取る
      target.hear('ワン');
  }
}

// DogにとってのPersonはこう
interface Person { 
    hear(sound: string): void;
}

// DogにとってのPersonを満たす(実装する)
class SomePerson implements Person { 
    hear(sound: string) {
        console.log(sound)
    }
}
[スクリプト(再掲)]
const dog = new Dog();
const person = new SomePerson();
dog.bark(person); // DogにとってのPersonを実装する、SomePersonインスタンスを渡す
同様に、同じような関係性のABインターフェースBクラスがあるとしたら、以下の感覚でコードを書いている場合が多いです。
  • BインターフェースはAのものである
注意
  • 例えばDogインターフェースがあったとしたら、それはおそらくPersonのもので、関係性が逆になります。
    • オブジェクトを受け取る側がインターフェースを所有する場合が多いです。
  • PCの例は、理解を促すための単なるたとえ話ですので、深く考えないでください。

2. 後半:インターフェースの使い方

ここまで理解できれば、インターフェースが価値を発揮するケースを理解することができます。
ケーススタディ形式で説明します。
インターフェースがない状態から始め、改修が発生し、インターフェースを導入する流れを見ていきます。

2-1. 改修:新しいPersonの定義

吠えられたらコンソールにログを吐くのではなく、脳内に記憶するタイプのPersonを導入したくなったとします。

2-2. サンプルコード

先程の例から、インターフェースを取り除きます。 DogはSomePersonを受け取っています。
[型]
class Dog {
  bark(target: SomePerson) {
      target.hear('ワン');
  }
}

class SomePerson {
    hear(sound: string) {
        console.log(sound)
    }
}
[スクリプト]
const dog = new Dog();
const person = new SomePerson();
dog.bark(person); // console.log("ワン")となる
interface定義を抜いただけに見えますが、インターフェースが誰のものかを意識できるようになると、クラスの関係性が全く異なることに気がつくと思います。
先程のイメージと比べてみてください。

2-3. 改修手順

2-3-1. MemorizingPersonを定義する

インターフェースを定義しないまま、サンプルコードに新しくMemorizingPersonを追加します。
新しいPersonの定義自体は、これで完了です。
[型]
...

// 脳内に記憶するPerson
class MemorizingPerson {
    sounds:string[] = [];
    hear(sound: string) {
        this.sounds.push(sound);
    }
}

2-3-2. 定義したクラスを使う(エラー発生)

これらのクラスを使って以下のようなスクリプトを書きます。
[スクリプト]
const dog = new Dog();
const somePerson = new SomePerson();
const memorizingPerson = new MemorizingPerson();
dog.bark(somePerson)
dog.bark(memorizingPerson); // ここでエラー
dog.bark(memorizingPerson); // ここでエラー

for (const sound of memorizingPerson.sounds) {
    console.log('memory: ' +sound)
}
このようなコードは、機能しません。 Dogは引数として具体的にSomePersonを指定しており、MemorizingPersonSomePersonの一種ではないためです。
例えば普通の静的型付け言語だと、コンパイルエラーになります。
※言語によっては、このコードは動いてしまいますが、望ましくない点は同様です。

2-3-3. インターフェースを導入する

ここで、インターフェースを導入します。
DogSomePersonに依存するのではなく、DogPersonというインターフェースを宣言し、SomePersonMemorizingPersonがこのインターフェースを満たしに来るようにします。
このとき、頭の中に以下のようなイメージを描きます。
コードにすると次のようになります。
[型]
class Dog {
  bark(target: Person) {
      target.hear('ワン');
  }
}

interface Person {
    hear(sound: string): void;
}

class SomePerson implements Person {
    hear(sound: string) {
        console.log(sound)
    }
}

class MemorizingPerson implements Person {
    sounds:string[] = [];
    hear(sound: string) {
        this.sounds.push(sound);
    }
}
これにより、以下のコードは機能するようになります。 改修はこれで完了です。
[スクリプト(再掲)]
const dog = new Dog();
const somePerson = new SomePerson();
const memorizingPerson = new MemorizingPerson();
dog.bark(somePerson) // DogにとってのPersonを実装する、SomePersonインスタンスを渡す
dog.bark(memorizingPerson); // DogにとってのPersonを実装する、MemorizingPersonインスタンスを渡す
dog.bark(memorizingPerson); // 同上

for (const sound of memorizingPerson.sounds) {
    console.log('memory: ' +sound)
}
フォルダを分けるなら、クイズから学んだとおり、以下のような構成になります。
これで、このケーススタディは完了です。

2-4. インターフェースによって何が得られたか

インターフェースを導入したことで、Dogクラスは複数のPersonクラスの実装と一緒に使うことができるようになりました。
加えて、今回はインターフェースを導入するためにDogクラスの改修が必要になりましたが、今後はDogを一切改修せずに新しいPersonを定義できるようになったことも、大きなメリットです。(疎結合性)
例えばDogクラスのみをライブラリとして世界に公開するとします。
ライブラリは全世界で使われるため、ライブラリの利用者が定義するPersonに応じて、都度Dogを改修するわけにはいきません。
このような場合、DogにとってのPersonインターフェースを定義することは不可欠になります。

まとめ

「誰のもの?」を意識するようにすることで、インターフェースを上手く定義することができます。
Dogクラス、SomePersonクラス、Personインタフェースがあれば、PersonインターフェースはたいていDogクラスのものです。
飼い主クラス、ペットインターフェース、Someペットクラスがあれば、ペットインターフェースはたいてい飼い主クラスのものです。
インターフェースから得られるのは、疎結合性です。今回の例では、Dogクラスに一切修正を加えず、新たなPerson実装クラスを定義し、連携させて使うことができるようになります。
だからこそ、この目的に従ってインターフェースを使っていれば、インターフェースはDogクラスなど、オブジェクトを受け取る側のものになります。
PersonインターフェースがSomePersonクラスの持ち物だとしたら、新しいPersonが定義されるたびにPersonインターフェースが影響を受け、結果としてDogクラスを何度も修正することになるからです。

参考情報

実務経験の他には、分厚い古典から学んだ知識が多いように思います。
読んだのは3〜5年ほど前なので、具体的にどの書籍に何が載ってた、というレベルでは覚えていませんが、「インターフェースについて良く学んだ気がする」ベスト3を載せておきます。
※個人的には、この記事に記載されている内容が分かっていれば、(特にアプリケーション開発であれば)わざわざ時間割いて読まなくても良いかと思います。

追伸

オブジェクト指向にハマったのは5年くらい前でした。
オブジェクト指向分析設計のような沼にもハマったので、「オブジェクト指向は、神が与え給うた、現実世界を忠実にモデリングできる手法である」とでも言わんとするオブジェクト中二病を、1年くらいこじらせていた気がします。
一度発信してみたかったのですが、こうして5年越しに記事を書けてよかったです。
需要がありそうなら、インターフェースの実践的な使い方や、「オブジェクト指向プログラミングらしく書くコツ」といったテーマで、オブジェクト指向シリーズをいくつか書くかもしれません。

Discussion

コメントにはログインが必要です。