どのようにしてクラス設計を行うのか?クラス設計の考え方とコツ

 今回は、詳細設計(内部設計)におけるクラス設計について、考えてみたいと思います。

スポンサーリンク
rectangle

クラス設計とは

 外部的な振る舞いを設計する基本設計(外部設計)と違い、クラス設計は、プログラム内部の構造を設計する詳細設計フェーズに位置します。

 極端な話、クラス設計などなくても、全ての処理をひとつのメソッドに書いてしまうこともできます。

 では、なぜそうしないのか?先に述べたようなプログラムでは、様々なコストが掛かってしまう可能性が非常に高いからです。

劣悪な設計によるコスト増大

保守コスト

 たとえば、一人でプログラムを組むとしましょう。プログラムの隅々までしっかり把握しており、何か変更を加えなくてはならなくなったときでも、素早くその箇所に辿り着き、変更を加えることができます。

 しかし、現実に、一人でそのプログラムを永久にメンテナンスすることはあり得ません。自分ひとりで運営しているサービスであったとしても、不慮の事故により、誰かにサービス運営を任せることになるかもしれません。

 そうなったとき、先ほど述べたようなプログラムは、コードリーディングに膨大な時間が掛かることは容易に想像できます。

変更コスト

 変更は常に起こり得るものです。サービスの拡張や、新機能の追加など、変更の理由は挙げればキリがありません。

 単一の、メソッド分割すら行っていないプログラムでは、1つの変更が発生したとき、必ず複数個所の変更が発生します。場合によっては、100か所を超える変更が必要になるかもしれません。

 いくらIDEの進化が進み、置換一発で変更できるとはいえ、無限に発生する変更のことを考えるたび、頭を抱えたくなることは疑いようもありません。

クラス設計の手順

インターフェース設計

 プログラムは、基本的に何かを受け取って何かを返すものです。こういった入出力を表現するため、まずはインターフェースを設計します。

 インターフェースは、ごく抽象的な概念です。お馴染みの、JavaのListインターフェースを例にとって考えてみましょう。

 Listは、Listであることが明示されていますが、その内部構造までは示されていません。これは、ただのインターフェースであり、使用者はこの仕様さえ理解していれば、中身が何であっても利用できます。

 我々が通常使うのは、「ArrayList」ですが、内部の構造や、アルゴリズムが違う「LinkedList」や、「Vector」なども存在します。これらはListとしてのメソッドを全て備えており、定義された規約どおりに実装されているので、細かい実装を理解することなく使うことができます。

 インターフェースは、現実世界の自動車などに例えて説明されることがあります。先ほどのListになぞらえると、add()メソッドは「アクセルペダル」という規格、「要素を追加する」という規約は、「自動車が動く」という規約に当てはまります。これにより、自動車の使用者は何の疑問もなく利用することができますが、内部的にどういう仕組みで動いているのかは、一般人にはあまり知られていませんよね。

 インターフェース設計のコツは、「入力と出力」のみに着目することです。この段階で実装を見越した定義を行ってはいけません。そうしてしまうと、どうしても「実装ありき」のインターフェースになってしまうからです。

クラス構造を設計する

 クラス設計におけるメインのフェーズです。

 ここでは、クラスにおける「責務」を常に意識して構造を組み立てます。たとえば、通販サイトで使う、「商品」クラス構造を考えてみます。

 これは、取扱い製品の例です。まずItemインターフェースを次のように定義します。

public interface Item {

    protected final int TAX = 5;

    public int getPrice();

    public String getName();

    public String getDescription();

}

 定数「TAX」は、消費税率を表します。getPriceは、商品の価格を返すメソッド、getNameは、商品名をStringで返すメソッド、getDescriptionは、サイトに掲載する説明文を返すメソッドです。

 次に、実装クラスであるBookを作成していきましょう。

public class Book implements Item {
    private String name;
    private int price;
    private int ISBNCode;
    private String author;
    private String publisher;
    private Date publishedAt;

    public int getPrice(){
        return price * (100 + TAX) / 100;
    }

    public String getName(){
        return name;
    }

    public String getDescription(){
        return "[著者]" + author + " [出版社]" + publisher + "[ISBN]" + ISBNCode + "[発行日]" + publishedAt.toString();
    }
}

 概ね、このような実装が想定されます。

 しかし待ってください。この実装には、getPriceやgetNameなど、共通化できる箇所がありそうですね。この場合、AbstractClassを作って、実装クラスのコーディング量を減らすことができます。

public class AbstractItem implements Item {
    protected String name;
    protected int price;

    public int getPrice(){
        return price * (100 + TAX) / 100;
    }

    public String getName(){
        return name;
    }

    public String getDescription(){
        throw new Exception("not yet implemented.");
    }
}

 このAbstractClassを組み入れたクラス図を書いてみましょう。

 AbstractItemクラスで処理を共通化したので、Bookクラスを以下のように変更します。

public class Book extends AbstractItem {
    private int ISBNCode;
    private String author;
    private String publisher;
    private Date publishedAt;

    @Override
    public String getDescription(){
        return "[著者]" + author + " [出版社]" + publisher + "[ISBN]" + ISBNCode + "[発行日]" + publishedAt.toString();
    }
}

 ずいぶん簡潔になりました。

AbstractClass(抽象クラス)とInterface(インターフェース)の違い

 これまで説明してきたように、インターフェースとは、外部に対する入出力を定義したものです。対して抽象クラスは、もう少し具象的な概念として、メソッドの振る舞いや、フィールドの定義を行うことができます。

 極端な話、全てを抽象クラスとして定義することも可能です。しかしJavaは、多重継承ができません。

public class Example implements InterfaceA, InterfaceB{} // これは可能

public class Example extends AbstractA, AbstractB{} // これは不可

 つまり、全て抽象クラス化すると、「AとしてもBとしても振る舞うことができる」というクラスを作ることができないのです。

 ところで、「フォークとしてもスプーンとしても使うことができる」という、スポークと呼ばれる便利な食器があります。コードを書くと…、

public interface Fork{
    public void pierce();
}
public interface Spoon{
    public void up();
}
public class Spork implements Fork, Spoon {
    public void pierce(){
        // ...
    }

    public void up(){
        // ...
    }
}

 となります。これを継承で表現することは出来ませんから、こういった概念を表現したい場合は、必然的にインターフェースを利用することになります。

クラス図を作成する

 何も、UMLを用いてしっかりと作る必要はなく、場合によっては紙とペンや、ホワイトボードを用いて設計する場合もあります。

 クラス図は、クラス構造の理解を助ける大切なドキュメントです。他者とのチーム開発を行う場合は、必ず形として残しておきましょう。

 先ほど述べたように、UMLで作る必要はありません。クラス同士の結びつきがわかりさえすれば、エクセルやパワポ、ペイントなどでも構いません。ノートに手書きで残してもよいのです。大切なのは、プロジェクトメンバーや後任者に「伝わること」です。

クラス設計における重要な原則

単一責任原則

 Single Responsibility Principle

 「クラスを変更する理由は、単一でなければならない」

 クラスを変更する要因は様々ありますが、変更を見越した設計にすることが大切です。

 もし、変更する理由が複数ある場合、チーム開発ではタスクを割り当てられた複数人が同時にそのクラスを触ることになります。この場合、クラスは非常に不安定な状態になり、多数のコンフリクトを生む原因となってしまいます。

 これが、単一責任原則に則った設計になっていれば、ひとつの変更理由に対応するメンバーは通常1名ですので、コンフリクトを懸念することなく作業することができます。

DRY原則

 Don’t Repeat Yourself
 
 「重複を避けよ」

 非常に有名なこの原則は、様々な事象に適用することができます。ことプログラミングにおいては、重複コードというのは大抵の場合、「邪悪」なものです。

 遠い未来に渡ってコードやビジネスロジックが変更されない保証はありません。コピー&ペーストで作るロジックは、確かに新規に記述するときは楽でしょうが、変更が生じた場合、そのすべての重複箇所を変更しなくてはなりません。それが2つや3つなら大した問題にはなりませんが、20、30だったら?あるいは、変更漏れがあったら?

 こういった問題を避けるために、プログラム上のコードは、できるだけ重複を避けることが大切です。クラス設計に限って言えば、適切な継承や、クラスの分離などが挙げられます。

KISS原則

 Keep it simple, stupid!

 「シンプルにしておけ!」

 プログラムにおいて、シンプルであることは美徳であると同時に、最も重要なことです。必ずこれを念頭に置いてクラス構造を設計しましょう。

 しかし、上で述べた単一責任原則は、KISS原則と相容れません。単一責任原則を突き詰めると、クラス数が増大し、お世辞にもシンプルとは言えない構造になります。

 どちらを重視するかは、正直なところケースバイケースであると言わざるを得ません。設計者の好みによるところではありますが、シンプルにしておくことは非常に重要な要素ですので、可能な限り意識して設計を行っていきましょう。

まとめ

 プログラムはどのような乱暴な設計でも動かすことが可能ですから、クラス設計に答えはありません。

 しかし、メンテナンスをしやすい、他者が理解しやすいクラス設計を行うことは、チームの生産性向上にもつながり、効率的な製造を行うことが可能になります。

 うまく抽象化を行って、変更に強いシステムを構築しましょう。