はじめに
オブジェクト指向におけるクラスの再利用について、継承・委譲・インターフェースの3パターンについて解説する。
継承と委譲
どちらもクラスの機能を再利用するための手段。
違いは一つのクラスが背負う責務の数にあり、以下のように使い分けるといい。
責務とは
≒そのクラスを変更する理由の数、一つのクラスが持つ責務はなるべく少なくする(単一責任原則)
例えば一つのクラスに、ビジネスロジック・値の出力・データへのアクセスの全てが実装されているとすると、この3つのうちどれか一つを変えるだけでこのクラスは変更する必要がある。
Class CashRegistor{
public int calcTax(int sum){
return sum * 1.10
}
public void showDisplay(int sum){
String outputText = "合計:" + sum + "(円)"
System.printIn(outputText)
}
public void saveDB(int sum){
String sql = "INSERT INTO `HISOTORY` VALUES ... "
doSql(sql)
}
}
継承
再利用される側のクラスが自分の責務を全て、再利用する側にも押し付けたい時。
class Animal{
private String barking;
private void bark(){
System.printIn(this.barking);
}
}
class Dog extends Animal(){
public Dog(){
this.barking = "bow-wow"
}
public void run(){
...
}
}
class Chiken extends Animal(){
public Chiken(){
this.barking = "cock-a-doodle-doo"
}
public void run(){
...
}
}
この例では、鳴くという動作を表現をする機能をSystem.PrintInから別の処理に変更したい時。
その影響は、DogにもChikenにも及ぶはずなので、これはAnimalにbark関数を定義し、このクラスを継承する形で再利用するのが望ましい。
委譲
再利用する側のクラスが、再利用される側のクラスとは違った責務を背負う時。
class Database {
void connectToDatabase(String dbHost, String dbName, ...){
// DBへの接続ロジック
}
Result doSQL(String sql){
// 接続しているDBにクエリを投げ、その結果を返すロジック
}
// そのほかデータベースの接続状態の管理など
}
class UserRepository {
Database database;
UserRepository(Database database){
// UserRepositoryのインスタンス生成時にDatabaseのインスタンスをセット
this.database = database;
}
User getUser(int userId) {
String sql = String.format("SELECT * FROM users WHERE user_id=%d", userId);
Result result = this.database.doSQL(sql)
// クエリの実行結果をもとにユーザーオブジェクトを作って返す
}
}
この例では、Databaseへの接続を行うDatabaseクラスとユーザー情報を取得するUserRepositoryクラスを定義している。
Databaseの責務は、Databaseへ接続すること。
UserRepositoryの責務は、ユーザー情報を管理すること。
一見、UserRepositoryがDatabaseをextendsしても良さそうだが、もしそうすると下記のような弊害が出てくる。
- 単体テストがしづらくなる。(モックが作れない)
- UserRepositoryクラスが持たなくてもいいDatabaseクラスの関数を持つことになり、事故の原因になりうる。
Practice1
下記の例では、面積を計算するArea関数を持ったRectangleクラスを再利用したいと考えている。継承と委譲どちらで再利用するべきか。
class Rectangle{
private int width;
private int height;
public int Area(){
return width * height;
}
}
class Window extends Rectangle{
public Window(int width, int height){
this.width = width;
this.height = height;
}
public void open(){ ... }
public void close(){ ... }
}
class Rectangle{
private int width;
private int height;
public int Area(){
return width * height;
}
}
class Window{
private Rectangle rectangle;
public Window(int width, int height){
this.rectangle = Rectangle()
rectangle.width = width;
rectangle.height = heighht;
}
public int Area(){
return this.rectangle.Area()
}
public void open(){ ... }
public void close(){ ... }
}
正解
委譲で実装する。
Areaの計算は窓特有の責務ではなく、あくまで四角の物体の責務。
なので、なるべく単一責任原則からここは委譲で責任を分散するべき。
例えば、開発の途中でもし丸型の窓が出てきた時、
継承での実装だと新たにCircleクラスを実装して、CircleWindowクラスを定義するとする。
こうすると、本来まとめておきたいopen, closeなどの窓特有の責務が分散してしまう。
class Rectangle{
private int width;
private int height;
public int Area(){
return width * height;
}
}
class Circle{
private int radient;
public int Area(){
return radient * radient * 3.14;
}
}
class Window{
private Rectangle rectangle;
private Circle circle;
private int type //1 -> rectangle, 2 -> circle
public Window(int width, int height, int type){
if(type == 1){
this.rectangle = Rectangle()
rectangle.width = width;
rectangle.height = heighht;
}else if(type == 2){
this.circle = Circle()
circle.radient = width;
}
}
public int Area(){
if(type == 1){
return this.rectangle.Area();
}else if(type == 2){
return this.circle.Area();
}
}
public void open(){ ... }
public void close(){ ... }
}
Practice2
継承の例のAnimalでは、設計の際に留意すべきことがある。
それは何か。
正解
本当に鳴く動物しか扱わないのか。
例えば、魚など鳴かない動物も世の中にはいる。そのシステムが何を扱い、そのクラスをどのような意図で実装するのかを設計段階でよく考えておく必要がある。
インターフェース
クラス内の手続きを抽象化して保持しておく。
実際に何をするかはImplementsした先で詳しく書く。
interface Window{
int Area();
void open();
void close();
}
public class RectangleWindow implements Window{
private int width;
private int height;
public RectangleWindow(int width, int height){
this.width = width;
this.height = height;
}
private int Area(){
return this.width * this.height;
}
}
public class CircleWindow implements Window{
public CircleWindow(int radient){
this.radient = radient;
}
private int Area(){
return this.radient * this.radient * 3.14;
}
}
参考