kagamihogeの日記

kagamihogeの日記です。

なりすまし継承にご用心

インターフェイスには記述されていないけど、実装クラスには存在するpublicメソッド。(以下省略)

こうすることにより、そのメソッドへのアクセスパスを隠すコトができます。いざ、ライブラリ内で使用したくなったら、実装型にキャストするなり何なりで、使う事ができるようになります。

メソッドのアクセス制御 - 都元ダイスケ IT-PRESS より抜粋

引用元エントリではこのように、言語仕様に由来しない方法でアクセス制御を行うという、ユルい制約を課すやり方の一つを紹介している。なるほどねー、と。

コメント欄のやり取りも中々興味深い。

例えばEclipseプラグインを作る時、internalという単語が含まれるパッケージのクラスを使用するのは、基本、御法度となってます。バージョンアップ時の互換性を保証しないよ、という領域です。
でもやっぱり、どーしても使いたい時はある。Eclipseのバージョンが上がった時に、そのプラグインは動かなくなる可能性が高い、ということを認識した上で、リスクを知った上で使うのであれば、アリなんじゃないかなぁ。

メソッドのアクセス制御 - 都元ダイスケ IT-PRESS コメント欄 daisuke_m のコメントより抜粋

この例のように「知ってて使う分には構わないよ」という知見が共有されていれば、このテのユルい制約は良い方向に回ると思う。無論、知らずに使った場合は恐ろしいことになる可能性があるけど、リスクと得られるリターンを考えれば……まぁ、それなりにアリなのかな、と個人的には思います。



でまぁ、ちょっとコレ読んでて思い出したことがあるのでそれについて書く。ユルい制約の話からはスッ飛んでるんだけど、まぁキニシナイでくれ。何が言いたいかっていうと「実装継承って基本的にキケンだよね」ってコトだけなんだけどさ。

なりすまし継承

なりすまし継承とは、オーバーライドしたメソッドがオーバーライド元のメソッドの責務を大幅に上回る実装を行うこと。IPA 独立行政法人 情報処理推進機構 に「ポリモーフィズムを利用したなりすまし」として詳しく解説されている。ちなみに「なりすまし継承」って言い方は一般的な用語じゃないと思うのであしからずw

実際のコードはどんなものかというと…… 昔一度書いたことがある んだけど、まぁもっかい書くw

public class ClassA {
	public void method1() {
		System.out.println("ClassA#method1");
	}
}

class ClassB extends ClassA {
	public void method1() {
		System.out.println("ClassB#method1");
	}
}

...

	//こんな使い方を「なりすまし継承」
	ClassA a = new ClassB();
	a.method1(); //ClassB#method1 と表示される。

何が問題なの? と思う人は、是非 IPA 独立行政法人 情報処理推進機構 を読んで欲しい。俺が挙げたソースコード例はカナリ端折ったものなので、何がマズいか分かりにくいと思うけど、リンク先ではちゃんと解説されているので。まぁ、ポルナレフ先生に言ってもらうとですね、

  i|:!ヾ、_ノ/ u {:}//ヘ     | あ…ありのまま 今 起こった事を話すぜ!
  |リ u' }  ,ノ _,!V,ハ |     < 『おれは ClassA#method1 だと
  fト、_{ル{,ィ'eラ , タ人.    |  思ったらいつのまにか ClassB#method1 だった』
 ヾ|宀| {´,)⌒`/ |<ヽトiゝ   | 催眠術だとか超スピードだとか(以下略


とまぁ、人間というのは想定の範囲外の動作が来ると思考が止まるんですよ。「型は ClassA だけどインスタンスは ClassB なんだから当たり前じゃないの?」と思う人も居ると思います。が、サンプルぐらい短いコードならいいんだけど、数千行とかのコードを読む場合、変数が宣言される場所とそのインスタンスが実際に使われる場所がずっっっと離れてることはザラです。あー、プログラマーの腕が設計が動的型付け言語が型推論ががががうんぬんの話は一旦置いておいてねw

……それはさておき。クソ長いソース読む時、なりすまし継承は何で困るかというと、メソッド名から「タブンこんな動作すんだろ」と推測しつつコードを読むから。


「あれ? ClassA の method1 っていうメソッド名から想像するのとはゼンゼン違う動き
 してっぞ? なんぞ?」
となり

「あーインスタンスは ClassB だからか。ってか、method1 って名前から想像できねー実装に
 なってんぞ」
となるワケ。

こういう不意打ちがあると、コードを読むスピードがガクンと落ちる(=仕事のスピードが落ちる)のです。

ラッパーなら仕方ないな

俺は、実装継承はこのテの読むのが難解なコードを生み出しやすいんで避けた方が良い、と思っている。ただし、これはこれで有用なテクニックになるケースもあると思う。たとえば、ソースが無いか、いじれないライブラリなどのラッパーをラクして作りたい場合とか。他社が提供してるライブラリを使う必要があるんだけど、ウチの会社のプロジェクトで使いやすいインタフェースに加工したいぜー、なんて時は……まぁ、実装継承もアリかな、と。

とはいえ、そういうケースでも Java ではキホン的には委譲をベースにしたやり方のほうを検討したほうがいいかなぁ、と思うけれども。やはり実装継承は副作用が大きいから、後々扱いに困ることが多くなるように思うので……

さらに、なりすまし継承

それでもまだ「なりすまし継承とかいうけど、そんなに問題じゃなくね?」と思う人に、俺が遭遇した体験談をお伝えしよう。

public class ClassA {
	public void printName() {
		printFamilyName();
		printMiddleName();
		printLastName();
	}
	
	protected void printFamilyName() {
		System.out.print("kagami");
	}
	
	protected void printMiddleName() {
		System.out.print("------");
	}
	
	protected void printLastName() {
		System.out.print("hoge");
	}
}

class ClassB extends ClassA {
	public void printMiddleName() {
		System.out.println("");
	}
}

class ClassC extends ClassB {
	public void printName() {
		System.out.print("hatena_id:");
		super.printName();
		System.out.println();
	}
	
	public void printMiddleName() {
		System.out.print("nan-tara-kantara");
	}
	
	protected void printLastName() {
		printMiddleName();
		System.out.print("hogehogehogehoge");
	}
}

...

	
	ClassC a = new ClassC();
	//ClassA a = new ClassC(); でもどっちもいいけど。
	a.printName();
	

さて、a.printName() したときどんな文字列が出力されるでしょうか? コレを見るまで、まだそれほどなりすまし継承の恐ろしさが分かっていなかったアナタが「なりすまし継承ないわー」となったのなら幸いです。また、あなたが「継承による差分プログラミング」の幻想を広めたヤツを爆発させたくなってきたなら、これまた幸いです。俺もフルボッコしたいです。

俺は、こういうクラス階層を上に下にアッチコッチ見て回らないとコードの動作が理解できない実装継承を「goto 型継承」と脳内で呼んでます。各メソッドがウン百行とかあると、コードを順に追ってクラス階層をアッチ行ったりコッチ行ったりする内に、何が何だかわからなくなるんですよね。

まーこの例の場合は、迷ったら Template Method より Strategy にしときましょう、の一例にもなっちゃってるんだけど。その辺については省略。

原因

 三           三三
      /;:"ゝ  三三  f;:二iュ  三三三
三   _ゞ::.ニ!    ,..'´ ̄`ヽノン << 何が起きたらこんなコードになるんだ……ッ
    /.;: .:}^(     <;:::::i:::::::.::: :}:}  三三
  〈::::.´ .:;.へに)二/.::i :::::::,.イ ト ヽ__
  ,へ;:ヾ-、ll__/.:::::、:::::f=ー'==、`ー-="⌒ヽ
. 〈::ミ/;;;iー゙ii====|:::::::.` Y ̄ ̄ ̄,.シ'=llー一'";;;ド'
  };;;};;;;;! ̄ll ̄ ̄|:::::::::.ヽ\-‐'"´ ̄ ̄ll 

と、思う人もいると思います。実はコレ、俺がデスマで追い込まれて実際に書いたことのあるコードです。なりすまし継承の脅威を知ってなお、このコードにしたのは……まぁ、アリガチですが、追い詰められてたからですな。

シナリオとしてはこんな感じでした。

  1. printName するクラス ClassA 作るよ。first,middle,last とメソッド分けるよ
  2. middleName を表示しない機能も作れ、って仕様変更来たよ。でも ClassA の設計見直してる時間は無いよ。だから動いてる ClassA のコードは変更しないでね。
  3. うーん……せめて委譲で、って hogeMethod(ClassA) 通過させる必要あるから意味ないなぁ…… ClassA を継承した ClassB 作って printMiddleName だけオーバーライドするなりすまし継承で切り抜けるか……
  4. printName するときに先頭に hatena_id: と表示したり(以下略)な仕様変更来たよ。動いてる ClassA と ClassB のコードは(以下略)
  5. ぐぬぬ……いっそコード全部丸コピって新しいクラス作っちまうか? それはさすがに却下だ。となると、となると……もう、なりすまし継承を更に悪用するしかないよなぁ……

3 日後、そこにはややこしい継承構造を持ちながらも何故か動作するコードの元気な姿が!


一度動いたコードには出来るだけ手をつけず、かつ既存の動作が 100% 壊れることなく、かつ出来るだけ早く機能を追加実装する……疲労しきった精神状態ではラクな道を選び勝ちだなぁ、って思い知らされましたよw

対策

仕事としてプログラマーをするようになった後、当たり前のことを当たり前にやることは難しい、と思い知らされることばかりだが、この問題に対する対処も同じだと思う。リファクタリングとか……と続き書こうとして疲れたんでこの文書はここで終わっている。