継承よりコンポジション | Hello, Stupid World!

Hello, Stupid World!

いろいろとメモ代わりに書いていきます。

「継承よりコンポジション」とは有名なJava本、EffectiveJavaの言葉
ですが最近、共感する事が多かった為、改めてまとめてみます。

継承とは知っての通り、親の特性(メソッド、メンバ)を引き継ぐ事です。
これがメリットにもデメリットにもなり得ます。

親の持つ特性を利用できるというメリットは分かりやすく
大抵の継承の説明でされています。

■問題点
デメリットの説明はあまりされていません。
デメリットはPrivate以外全ての特性を引き継いでしまうという事です。
(引き継ぐ対象を指定できない)

これが問題を引き起こします。

■例

 public class FileLog {
   protected String fileName;

   protected String message;


   public FileLog(String fileName) {
     this.fileName = fileName;
   }


   public void changeFile(String fileName) {
     this.fileName = fileName;
   }


   public void print(String message) throws IOException {
     PrintWriter pw;
     pw = new PrintWriter(new BufferedWriter(new FileWriter(new File(fileName))));
     pw.println(message);
     pw.close();
   }
 }

上記のようなファイル出力用クラスがあったとします。
また、それを利用した以下のような、ファイル名に日付を自動的につける
子クラスをつくったとします。
※サンプルなんでクラス設計がおかしいことは気にしないで下さい

 public class TimeFileLog extends FileLog {
   public TimeFileLog(String fileName) {
     super(fileName);
     SimpleDateFormat sdf1 = new SimpleDateFormat("yyyyMMdd_");
     this.fileName = sdf1.format(new Date()) + fileName;
   }
 }


実はこの時、欠陥が存在します。

 TimeFileLog timeFile = new TimeFileLog("test.txt");
 timeFile.changeFile("test2.txt");
 timeFile.print("Hello");


本来はファイル名に日付をつけるはずが親クラスのメソッドを利用する
事で子クラス日付がつかないファイル名とする事ができてしまいます。
もちろん、この動作を子クラス作成者が理解できていれば問題は
ありません。

しかし、このようにオーバライドしなかったメソッドを利用された場合の
動作を想定するのは難しいことで、気づかずにこのような状況に
なってしまう事が多いです。

子クラスを作るときに気付くのでは無いかと思う人もいるでしょう。
しかし、子クラスを作成してから親クラスが拡張されて上記のような
欠陥が生まれてしまう事も多いですし、メソッドが多くなってくると
見逃す可能性も増えます。

つまり、継承を使う事で子クラスが意図しないメソッドまでを
引き継いでしまう事が問題の原因です。

■解決策
解決策はコンポジションです。
子クラスにあたるものをコンポジションを利用する事で
作成者が意図した特性のみを引き継げます。
前述のTimeFileLogというクラスをコンポジションを利用するよう
変更したものが下記です。

 public class TimeFileLog {
   private FileLog fileLog;

   public TimeFileLog(String fileName) {
     SimpleDateFormat sdf1 = new SimpleDateFormat("yyyyMMdd_");
     String _fileName = sdf1.format(new Date()) + fileName;
     this.fileLog = new FileLog(_fileName);
   }

   public void
print(String message) throws IOException {
     
fileLog.print(message);
   }
 }

拡張しないメソッドについては元クラスのメソッドに
そのまま引数を渡すだけです。(printメソッドのように)

このように作っておけば親クラスが知らない間に拡張されようが
上記の問題が生じません。

使用するものを全部、明示的に記述するのは多少の手間ですが
その分、動作をしっかり保障したものとなります。