はじめに
広くはこちらの続き
Interfaceのdefaultでやろうと思って、やっぱりやめたというのを、やっぱりやってみたという感じです。
先に断っておきますが、以降はOOPLの話ではありません。
あくまで Javaの言語仕様で実現できる実装パターンの話です。
一般的な批判
おそらく以下で紹介する実装は一般的に「推奨されない」ものです。
OOPLにおける「継承よりも委譲」に反しているためです。
また多重継承による継承パズルも良くないというのが一般的だと思います。
こちらの記事などを参考にされると良いと思います。
インターフェースのdefaultメソッド、ちゃんと使えていますか? #Java - Qiita
プログラミング応用a 第14回 『インタフェイス(interface )』 -01〜多重継承の問題点
何が嬉しいの?
嬉しいの?というか、なんでこんなことをやろうと思ったの?というのを振り返りから。
基本は継承より委譲
ユーティリティを使うと
String hoge = "abc";
String upperHoge= StringUtils.toUppercase(hoge);
これをStringをプロパティにする文字列クラス(例えば「Text」)の場合
Text hoge = Text.of("abc"); Text upperHoge = hoge.toUppercase();
こういう感じになります。
Textみたいな汎用的なものを使い回すのではなくて、もうちょっとドメインに特化したようなクラスがあって、そこでも「toUppercase()」を設けたい場合、委譲になるんだけど同じようなことを書くのが面倒です。
では親クラス定義して継承を使えば良いんじゃないか?と考えがちです。
ようは「委譲よりも継承」という逆アプローチ。
例えば、COBOLからJavaを知った僕自身が「おぉ、これは冗長なロジックを回避できるので良い!」と思って継承地獄(?)にハマりました*1。
まずやってみて困ったことは「親への依存が強すぎて使いにくい」です。
使う必要のないユーティリティも常に巻き込んでいるのでやりたいことに対してやれることが多すぎます。
開放したくないメソッドがあった場合に、サブクラス側でブラックリストのようにUnsupportedOperationException
で蓋閉じをすることも出来なくは無いですが、もはや本末転倒です。
Interfaceに対する個人的なイメージ
Interfaceは 操作の属性 というように個人的には思っています。
Cloneable
や Serializable
などがその例になります。
クラスにimplements ととして宣言されている属性といえばいいでしょうか。
上記のInterfaceは属性だけだったりしますが、多態をつかった実装をしていると「クラスは操作の属性の組み合わせで出来ている」というように僕は感じるようになりました。
そういうイメージをするようになって、試しに多態を使用した実装*2をするようになって、よりこのイメージが強くなりました。
ここまではInterfaceだけの話です。
ここからはJavaの言語仕様で、できるようになったことの話です。
Interfaceという型(操作の属性)としては、これで十分だったのですが、ここでJavaの言語仕様として InterfaceのdefaultがJava8から使えるようになりました。
実装が持てるという事は、this.getValue()
というような実装をすれば インスタンスのプロパティ値を参照することも出来るという事だったりします。
Interface自身ではプロパティを持つことは出来ませんが、プロパティへのアクセスをするメソッドを経由してインスタンスのプロパティ値を参照することは出来ます。
「あー、、できるかぁ・・・」
このボンヤリとした「言語仕様として出来ること」と「クラスは操作の属性の組み合わせで出来ている」というイメージが僕の中でモヤモヤと。。
Interfaseのdefaultを使ってみようと考えるまでの経緯
委譲で実装
元々のキッカケとなったのはBigDecimalの計算を「単価クラス」を例にします。
委譲のやり方は以前の記事を抜粋しつつ、例えば
public interface Plus<T> extends CalculatorBase { public T plus(T... other); }
でInterfaceを作っておいて、実装で
public class Price implements Plus<Price> { private final BigDecimalCalculator calculator; private Price(BigDecimal value) { this.calculator = BigDecimalCalculator.builder(value).build(); } public static Price of(BigDecimal value) { return new Price(value); } @Override public final Price plus(Price... other) { return Price.from(this.calculator.plus(other)); } }
という感じにしました。
Plus
はInterfaceなので、他の属性を追加できて親クラスを持った継承でもありません。
他に数値を扱うクラスで加算を行う場合は同じように機能を割り当てる感じで実装をします。
そこで思うわけです「冗長だなぁ」と。
ユーティリティで対応
(実装例は省略)
BigDecimalの加算は独自性も無く単純なユーティリティを呼ぶだけです。
そういうものはユーティリティクラスでカバーするという方法もあります。
欠点はドメインオブジェクトのプロパティを外部へ公開しなければいけません。
(内部で内包したパターンは委譲と同じなので割愛)
Interfaceのdefaultを使った実装へ
Interfaceと委譲の組み合わせを使った実装は
- 型による統一的な操作の属性を宣言的に実施
- ユーティリティメソッドを呼ぶだけの実装をする
という繰り返しだったりします。
この「繰り返し」をしていると、Interfaceのdefaultを使う「操作の属性の標準実装」と何が違うのだろう? と思い至ります。
一般的に「やらないこと」という理解はしつつ、それをもって思考停止するのも良くないな、と。
せっかくJavaの言語仕様としてできることなのだから、やってみて、良いところ、気を付けるところ、みたいなのを自分なりに考えてみても良いのではないだろうか?と。
やってみて分かったこと
良かったところ、気を付けた方が良さそうなところですが、明確な学術的な論拠ではなく あくまで僕の肌感覚です。
気を付けた方が良さそうなところの記載が多いですが、僕としてはネガティブな意味合いではなく気を付けさえすれば、それなりに使えるんじゃないかな?という意図で書いています。
良かったところ
属性を宣言するだけで合成できる
一番やりたかったところ。
基本操作の属性の組み合わせで汎用的なロジックで充足するところは完結できます。
冗長なコードは無くなりました。
メンバ変数値の所在が実装クラスだけ
メンバ変数が実装クラスだけで宣言するので、this.getValue()
の取得場所は1つだけです。
実装継承だと親クラスのメンバ変数かサブクラスのメンバ変数か分かりにくくなったりすることがありますがInterfaceにはメンバ変数の宣言ができないので this
は確実に実装クラスだけになります。
プリミティブな型の拡張がやりやすい
やりやすいというか、向いているという言い方の方が良いかもしれません。
気を付けた方が良さそうなところ
親が子のメンバ変数を参照する
良かったところの逆で、Interfaceが親クラス的な位置付けだとして、this
という子クラス的なメンバ変数の値を参照するということに違和感があるといえばあります。
馴染んでしまえばそういうものと僕は割り切れましたが、嫌な人には嫌な感じはすると思います。
ドメインオブジェクト的なものに限った方が良さそう
IOなどの副作用が伴う実装はやるべきではないと思います。
あくまでPOJOというか、副作用のないものに限った方が良いというニュアンスとして「ドメインオブジェクト的なもの」としました。
プロパティは1つ(もしくは少数)が良さそう
上述の例はインスタンスの複製をしていませんが実装例としてはインスタンスの複製も行います。
ここでコンストラクタをリフレクションで実行するのですが引数の組み合わせや順序まで考慮した汎用的な仕組みを組み込むとパズルがより複雑になります。
さすがにそれはやりすぎかなぁと思います。
プリミティブな型の拡張に留めておいた方が良さそう
プロパティは1つとほぼ同じです。
プリミティブな型(もしくはそれに相当するもの)を拡張するくらいに収めておけるものであれば、まだ使う側も作る側も扱える範囲かな?と思います。
合成を前提に1つ1つは動詞単位で小さく作る
思っているよりも細かい粒度でInterfaceは作った方が良いです。
たとえば「計算」というInterfaceを作る場合は「加算」「減算」「乗算」「除算」に分割をするという感じです。
操作の属性の粒度なので、名詞ではなく動詞を単位に分割しておいた方が合成がやりやすいです。
実装の継承は避けた方が良さそう
型を組み合わせたクラス(実装)を継承するというのは止めた方が良いと思います。
あくまで型の合成(もしくは型の継承)の範囲で留めておくのが良いと思います。
ただでさえ継承ツリーで複雑なところに依存の強制まで入ってくると飼いならす(?)のはかなり困難な印象があります。
委譲の方が楽
楽というか継承ツリーを意識しながら実装するのはInterfaceを作る側としては手間がかかります。
テストもInterfaceの実装をテストするためクラスのテストを作るのと比べると一般的ではないので慣れないと違和感があります。
シンプルな分かりやすさとしては委譲が良いと思います。
GitHub(コード全量)
Release interface.default.3 · vermeerlab/domains · GitHub
さいごに
ボイラーなコードもコピペとIDEの力を借りれば簡単に(?)解消できたり、これからはAIがコードを量産してくれる時代なので面倒なことはAIに任せるのが良いでしょう*3。
ただ、あくまで個人的には 副作用を伴わない、POJOなドメインオブジェクトだったら使っても良いんじゃないかな?と思います。
思いますが、おそらく反対の声が多いことも予想されます。
ご利用は計画的に(?)、用法用量を守って(?)試してみても良いかもしれません。*4*5
「はじめに」としてはここまでです。
具体的なコードとそこに至った感想や説明を今後書いていく予定です。
上述のコード全量を順番に説明をしていくだけなので、そんな説明とかいらない人は そちらを参照していただければと思います。
記事リスト
記事の索引的なもの
Interfaceのdefaultで多重継承(下準備編) - システム開発で思うところ
Interfaceのdefaultで多重継承(文字列編) - システム開発で思うところ
Interfaceのdefaultで多重継承(数値編) - システム開発で思うところ
Interfaceのdefaultで多重継承(単位編) - システム開発で思うところ