拡張性のあるクラス設計とは何かを考えてみる
オブジェクト指向プログラミングやクラス設計に興味を持ち始めた方に向けて、拡張性のあるクラス設計について考えてみたいと思います。
対象読者
下記の状態や疑問を持っている方
- プログラムはある程度書けるようになった
- クラス設計ってなに?
- オブジェクト指向を学んでいるけど、何が便利なの?
- インターフェースって使う必要ある?
概要
車を表現するクラスを例に3つの異なる設計パターンを紹介し、それぞれのメリットとデメリットを考えてみたいと思います
クラスの中身やディレクトリ構成は雑に作成...
また、フレームワークを使用していればデメリットが消える部分はあるが今回は度外視
パターン1: 単純なクラス構造
CarクラスはBodyとWheelクラスをメンバ変数として持っており(コンポジション ※)
クライアントであるMainから使用される。
※コンポジションについて
(記事お借りしますm(_ _)m)
メリット:
・クラス構成がシンプル
・ファイルが少ない
デメリット:
・クラス設計をほとんど行わず拡張性がない構成
・carパッケージが密結合であり、Body、Wheelクラスの修正がCarクラスにもモロに受ける
クラス図
ディレクトリ構成
com/
└── pattern1/
├── car/
│ ├── Body.java
│ ├── Car.java
│ └── Wheel.java
│
└── main/
└── Main.java
コード
package com.pattern1.main;
import com.pattern1.car.Car;
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.drive();
}
}
package com.pattern1.car;
public class Car {
private Body body = new Body();
private Wheel wheel = new Wheel();
public void drive(){
wheel.spin();
System.out.println("車が走りました");
}
}
package com.pattern1.car;
public class Wheel {
void spin(){
System.out.println("タイヤを回します");
}
}
package com.pattern1.car;
public class Body {
}
メインメソッドの実行結果
タイヤを回します
車が走りました
パターン2: BodyとWheelクラスを抽象化し、部品の修正がCarクラスに影響を与えないように変更
CarクラスのBodyとWheelはMainクラスから依存性の注入(※)を行い使用する
※依存性の注入について
(記事お借りしますm(_ _)m)
メリット:
・車の部品であるBodyとWheelがインターフェースになったことで拡張性UP(部品の付け替えが可能となった)
デメリット:
・少し複雑
・クライアント側で依存性を注入する必要があり利用者側でcarパッケージの利用方法を知っておく必要がある
クラス図
ディレクトリ構成
com/
└── pattern2/
├── car/
│ ├── BodySedan.class
│ ├── Car.class
│ ├── IBody.class
│ ├── IWheel.class
│ ├── WheelStudless.class
│ └── WheelSummer.class
│
└── main/
└── Main.class
コード
package com.pattern2.main;
import com.pattern2.car.BodySedan;
import com.pattern2.car.Car;
import com.pattern2.car.WheelStudless;
public class Main {
public static void main(String[] args) {
Car car = new Car(new BodySedan(), new WheelStudless());
car.drive();
}
}
package com.pattern2.car;
public class Car {
private IBody body;
private IWheel wheel;
public Car(IBody body, IWheel wheel) {
this.body = body;
this.wheel = wheel;
}
public void drive(){
wheel.spin();
System.out.println("車が走りました");
}
}
package com.pattern2.car;
public interface IBody {
}
package com.pattern2.car;
public class BodySedan implements IBody{
}
package com.pattern2.car;
public interface IWheel {
void spin();
}
package com.pattern2.car;
public class WheelSummer implements IWheel{
@Override
public void spin(){
System.out.println("高温に耐えながらタイヤを回します");
}
}
package com.pattern2.car;
public class WheelStudless implements IWheel{
@Override
public void spin() {
System.out.println("滑りにくく回ります");
}
}
メインメソッドの実行結果
滑りにくく回ります
車が走りました
パターン3: CarクラスをInterfaceとし抽象化、さらにFactory Methodパターンの採用でクライアント側の依存性の注入を簡略化
※Factory Methodについて
(記事お借りしますm(_ _)m)
メリット:
・Carクラスを含め、拡張性が高い
・クライアント側の依存性注入の手間をFactoryクラスであるCarFactoryが簡略化
デメリット:
・構成が複雑でコード理解に時間を要する
・修正を行う人も設計思想を理解している必要がある
クラス図
ディレクトリ構成
com/
└── pattern3/
├── car/
│ ├── BodySedan.java
│ ├── Car.java
│ ├── CarFactory.java
│ ├── IBody.java
│ ├── ICar.java
│ ├── IWheel.java
│ ├── Wheel.java
│ ├── WheelStudless.java
│ └── WheelSummer.java
│
└── main/
└── Main.java
コード
package com.pattern3.main;
import com.pattern3.car.CarFactory;
import com.pattern3.car.ICar;
public class Main {
public static void main(String[] args) {
ICar car = CarFactory.getSedanStudlessCar();
car.drive();
}
}
package com.pattern3.car;
public interface ICar {
void drive();
}
package com.pattern3.car;
public class Car implements ICar{
private IBody body;
private IWheel wheel;
public Car(IBody body, IWheel wheel) {
this.body = body;
this.wheel = wheel;
}
@Override
public void drive(){
wheel.spin();
System.out.println("車が走りました");
}
}
package com.pattern3.car;
public class CarFactory {
public static ICar getSedanStudlessCar() {
return new Car(new BodySedan(), new WheelStudless());
}
public static ICar getSedanSummerCar() {
return new Car(new BodySedan(), new WheelSummer());
}
}
以下pattern2と同様のため割愛
メインメソッドの実行結果
滑りにくく回ります
車が走りました
まとめ
・クラス構成によってメリットとデメリットが存在する
・最低限の拡張性を持った構成にしておくと後々楽になる反面、修正コストもかかる
・プロジェクトの規模やメンバの理解度を考慮したクラス設計が必要
・複雑なクラス設計を行う際は、設計思想のドキュメント化など対応が必要