msawady’s learning memo

ただのJavaエンジニアが、学んだことをログっていくところ

【Java】abstract と interface の使い分け 〜「オブジェクト指向でなぜ作るのか」から学ぶ〜

オブジェクト指向でなぜ作るのか」の読書メモとして

あたりは全然分かっていなかったなと本書を読むことで実感しました。なので

  • オブジェクト指向は何を解決したかったのか
  • abstract class は何を解決したのか
  • interface は何を解決したのか
  • abstract class と interface の使い分け

について書きたいと思います。対象読者はJava初級〜中級者になります。

オブジェクト指向は何を解決したかったのか

機械語アセンブリ高級言語(e.g:FORTRAN)→構造化言語(e.g: C言語)とプログラミング言語は進化しましたが、以前として解決されない問題が有りました。

そして問題を加速させたのが、以下の問題でした。

  • システム要件の複雑化(本格的な業務利用)
  • コードベースの巨大化

それらの問題を解決するために以下のような方針で解決策が取られました。こういった方針を具体化したプログラミング手法がオブジェクト指向プログラミングであると本書では述べられています。

方針 解決したかったこと
変数、サブルーティンの整理・集約
重複するサブルーティンの集約
コードベースの巨大化
システム要件の複雑化
共通メインルーティンの作成 モジュールの再利用が困難
変数の隠蔽、ローカル変数→インスタンス変数 グローバル変数の管理

abstract class は何を解決したのか

方針 解決したかったこと
変数、サブルーティンの整理・集約
重複するサブルーティンの集約
コードベースの巨大化
システム要件の複雑化

この問題を解決するためのキーワードは「継承」です。まず、「クラス」という仕組みを導入すことで、変数、サブルーティンの整理・集約が行いやすくなりました。 さらに、「abstract class の継承」を導入することで、「整理されたクラス間で重複するサブルーティンの集約(脱コピペ!)」を直感的に行えるようになり、システム開発の生産性・保守性を大きく上げました。

実装コストと言う面から見ると、以下のようなメリットが有ります。

  • 重複する変数・サブルーティンを共通化することによる、コードベースの縮小
  • コードを整理することによる、ソースコード管理の効率化(=保守性の向上)
  • 修正の分散を抑えることによる、保守性の向上

また、設計の質を向上させる効果も有りました。

  • 共通するサブルーティンを集約するための、外部・内部設計のブラッシュアップ
    • 共通のサブルーティンを持つ=似たような機能/属性である、という抽象化

これにより、複雑なシステム要件を「現実世界に則した」形で表現し、直感的に設計することが可能になりました。

サンプルとしては以下のような形になります。定番ではありますが、DogクラスとHumanクラスを実装する例を考えます。

before

abstract クラスを利用しない場合は、重複するコードが発生します。

public class Dog{
    private int legCount;
    
    public Dog(){
       this.legCount = 4; 
    }

    public boolean isMammal(){
        return true;
    }
    
    public boolean isFourLeg(){
        return true;
    }
}

public class Human{
    private int legCount;

    public Human(){
        this.legCount = 2;
    }
    
    public boolean isMammal(){
        return true;
    }
    
    public boolean isFourLeg(){
        return false;
    }
}

after

abstract なMammalクラスを実装し、それを継承することで、コードの重複を減らすことが出来ます。 また、isFourLegメソッドについては「犬はtrue, 人間はfalse」という定義から、「4本足かどうか」という抽象的かつ直感的な再定義を行うことが出来ています。

public abstract class Mammal {
    private int legCount;

    public Mammal(int legCount) {
        this.legCount = legCount;
    }

    public boolean isMammal() {
        return true;
    }

    public boolean isFourLeg() {
        return this.legCount == 4;
    }
}

public class Dog extends Mammal {
    public Dog() {
        super(4);
    }
}

public class Human extends Mammal {
    public Human() {
        super(2);
    }
}

interface は何を解決したのか

方針 解決したかったこと
共通メインルーティンの作成 モジュールの再利用が困難

この問題を解決するためのキーワードは「ポリモーフィズム」です。「ポリモーフィズム」を実現することで、「共通化されたメインルーティン」を作成しやすくなり、「再利用可能なモジュール」を作成することが可能になりました。

ちょっと分かりづらいかと思うのでサンプルを。雑なサンプルですが、伝えやすくはなるかと。

before

もし、Listというinterfaceが存在しない世界で、「ArrayList のすべての要素をnew Element()に変更する」メソッドと「LinkedList のすべての要素をnew Element()に変更する」メソッドを実装しようとすると、以下のようになります。

public class HogeService {

    public void replaceWithNewElement(ArrayList<Element> list) {
        int size = list.size();
        list.removeAll();
        for (int i = 0; i < size; i++) {
            list.add(new Element());
        }
    }

    public void replaceWithNewElement(LinkedList<Element> list) {
        int size = list.size();
        list.removeAll();
        for (int i = 0; i < size; i++) {
            list.add(new Element());
        }
    }
}

まったく同じことをやっているのに、ArrayListLinkedListという2つのクラスを処理するためにreplaceWithNewElementというメインルーティンを2つ用意する必要が有ります。言い方を変えると、replaceWithNewElement(ArrayList list)replaceWithNewElement(LinkedList list)として再利用できません。

after

Listというsize(), removeAll(), add()といったメソッドを持つ interface を作成します。

public interface List<E> {

    public int size();

    public boolean removeAll();

    public boolean add(E element);
    
}

そして、interface は実装を提供せず、仕様を提供します。「実現の仕方は約束しないけど、このメソッドはこういうことをする」という仕様の約束だけを行います。上記3メソッドについては以下のように仕様が定められます。

  • size(): このリスト内にある要素の数を返します。
  • removeAll(): このリストから、指定されたコレクションに含まれる要素をすべて削除します。
  • add(): 指定された要素をこのリストの最後に追加します。

List (Java Platform SE 8) *1

そして、ArrayListLinkedListという実装クラスはListという interface の仕様に対して、実装を提供します。

public class ArrayList<E> implements List<E> {
    public int size(){
        // 具体的な実装
    }

    public boolean removeAll(){
        // 具体的な実装
    }

    public boolean add(E element){
        // 具体的な実装
    }
}

public class LinkedList<E> implements List<E> {
    public int size(){
        // 具体的な実装
    }

    public boolean removeAll(){
        // 具体的な実装
    }

    public boolean add(E element){
        // 具体的な実装
    }
}

そして、replaceWithNewElement の引数をListという interface の仕様に従ったクラスと定義します。

public class HogeService {

    public void replaceWithNewElement(List<Element> list) {
        int size = list.size();
        list.removeAll();
        for (int i = 0; i < size; i++) {
            list.add(new Element());
        }
    }
}

こうすることにより、replaceWithNewElementというメインルーティンは、ArrayListでもLinkedListでも利用できるようになりました。つまり、モジュールの再利用性が高まったのです。

  1. 仕様(interface)を定義
  2. 仕様に対する実装(implement)を定義
  3. interface を処理するメインルーティンを定義する

とすることで、「interface を implement するクラスすべてを扱うことに出来る、再利用性が高いメインルーティン」を作成することができます。

abstract class と interface の使い分け

ここまで書くと、なんとなく伝わってくるでしょうか。abstract class と interface という2つの仕組みは似たような特徴を持ちますが、そもそも解決したい問題が違うのです。

仕組み 目的
abstract class 重複するメソッドの集約
実装における抽象度の整理
interface メインルーティンの再利用性の向上
仕様と実装の分離

ということですね。この目的の違いを意識してあげると、abstract class と interface の使い分けを上手く出来るのではないかと思います。

おわりに

オブジェクト指向でなぜ作るのか」は今回書いたようなプログラミング手法以外にも、設計やプロジェクト運営についても幅広く書かれており、新人〜若手におすすめできる良書です。

特に「継承」「interface」はオブジェクト指向プログラミングにおいて重要な要素である一方で、勉強を進める中でのカベになりやすいところです。「そもそも何を解決したかったのか」から学ぶことで、スッキリ理解することが出来るのではないかと思います。

以上です。長文にお付き合い下さり、ありがとうございました。

*1:java8からは replaceAllが有るので、今回のサンプルのイケてなさが…