X

A blog about Oracle Technology Network Japan

  • November 20, 2020

クイズに挑戦:コア関数型インタフェースのリファクタリング(中級者向け)

Guest Author

※本記事は、Simon RobertsとMikalai Zaikinによる"Quiz yourself: Refactoring with the core functional interfaces (intermediate)"を翻訳したものです。


リファクタリングでコードを改善する方法に関する知識を確認する 

著者:Simon Roberts、Mikalai Zaikin 
2020年6月16日 | 本記事をPDFでダウンロード
その他の設問はこちらから

過去にこのクイズの設問に挑戦したことがある方なら、どれ1つとして簡単な問題はないことをご存じでしょう。クイズの設問は、認定試験の中でも難しいものに合わせています。「中級者向け」「上級者向け」という指定は、設問の難易度ではなく、対応する試験による分類です。しかし、ほとんどすべての場合において、「上級者向け」の方が難しくなります。設問は認定試験対策として作成しており、認定試験と同じルールの適用を意図しています。文章は文字どおりに解釈してください。回答者を引っかけようとする設問ではなく、率直に言語の詳細な知識を試すものだと考えてください。

このJava SE 11クイズでは、リファクタリングを行ってコードを改善したいと思います。コードのコミット履歴を確認しているとき、同僚が次のメソッド・シグネチャを置き換えていたことに気づいたとします。

public void testDrive(Vehicle x) {
  // ...
}

置換後のメソッド・シグネチャは次のようになっていました。

public void testDrive(Supplier<? extends Vehicle> x) {
  // ...
}

次のうち、このようなリファクタリングのメリットを表しているものはどれですか。1つ選んでください。

A. 実行が高速になる
B. カプセル化が向上する
C. 遅延インスタンス化が行われる
D. 車両のキャッシュがサポートされる
E. 別の車両サブクラスが使いやすくなる

解答:設問の選択肢が価値の判断に見える場合や意見に基づいているように見える場合は常に、設問が正確であるかどうかや、出題者が考えていたことをどのようにして知らなければならないのかを考えて、注意をそらされてしまいがちです。しかし、出題者が考えていたことを知る必要はありません。

安心していただきたい点は、実際の試験の設問(と選択肢)は細かく精査され、綿密なベータ・テストを経て、合理的な疑いを差し挟む余地があったものは取り除かれるということです。本記事では、設問を作成したのが私たち2人だけであるため、議論や精査は実際の試験よりはるかに少ないものになっています。選択肢を検討するうえで、「これをこのように見たらどうだろうか」と別の角度から考えることに余念がない場合でも、皆さんがそのような設問にどのようにして解答できるかを根拠と論理で説明したいと思います。

それでは、順番に選択肢について考えてみます。

選択肢Aには、コードの実行が速くなるとあります。そのため、リファクタリングの前後をどのように比較するかについて考える必要があります。元々のコードでは、Vehicleオブジェクトは直接的に提供されており、その提供を行うのはコール元の責任です。リファクタリングされた形式では、コール元はVehicleにアクセスする手段であるSupplierを提供しています。ただし、Supplierには、Vehicleを作成しなければならないという制約はなく、既存のオブジェクトを返すこともできます。

一方で、コール元が何らかの理由ですでにVehicleオブジェクトを保持している可能性もあります(同じVehicleに対して連続して多くのテストを行っている場合など)。既存のオブジェクトには、リファクタリング前のコードでもリファクタリングしたコードでも対応できます。しかし、リファクタリングしたコードのtestDriveメソッドは、レイヤーを1つ挟んで間接的にVehicleにアクセスせざるを得なくなります。これまでのところ、リファクタリングしたアプローチには明らかな利点がない一方で、潜在的な欠点が1つ存在します。

ここでもう1つ、testDriveメソッドの動作に実はVehicleが必要なかったらどうなるかについて考えてみます。このメソッドの名前を考えれば、少し飛躍していると思う方もいらっしゃるかもしれません。しかし、おそらくは構成フラグが原因で、テスト自体によって実行のスキップが決定されることも考えられます。その場合、リファクタリングした形式ではSupplierを使用できないというだけの話です。すると、Vehicleがまったく作成されない可能性が出てきます。それによって、実行が速くなるかもしれません。

この時点で、低下と向上の両方向でパフォーマンスに関する多少の議論がありました。いずれも状況次第ではありますが、この状況については何も提示されていません。そのため、この選択肢はさほど魅力的ではありません。

選択肢Bには、リファクタリングした形式ではカプセル化が向上するとあります。これが妥当であるとは思えません。リファクタリングの有無にかかわらず、Vehicleオブジェクトは得られます。また、オブジェクトがカプセル化されているかいないかは、使用方法ではなく実装によるものです。いずれの場合でも、testDriveメソッドはVehicleを知っています。変わったのは、Vehicleへのアクセスの仕方だけです。Vehicle自体について言えば、testDriveメソッドや、このメソッドが属するクラスがカプセル化されているかどうかによる影響はありません。カプセル化は明らかに違うと思われるため、選択肢Bは誤りだと考えることができます。

選択肢Cでは遅延インスタンス化に触れています。これはどういう意味でしょうか。この考え方の一部は、選択肢Aの説明の際に触れました。つまり、Vehicleを作成するタイミング、さらには作成自体が必要かどうかを、呼び出される側のメソッドが制御できるようにすることです。車両なしに車両のテスト運転を行うというのは想像しにくいですが、先ほども挙げたように、何らかの構成フラグによってテストが制御される場合や、実行がスキップされる場合もあるでしょう。そのような状況では、このアプローチによって直接かつ効率的にスキップを行うことができます。ただし、遅延インスタンス化でできることはそれだけではありません。たとえば、テスト運転で複数台の車両が必要になる事態を想定できます(おそらく、衝突で1台目が壊れてしまっているのでしょう)。Supplierを受け取ったメソッドは、Supplierを呼び出さないことも、1回だけ呼び出すことも、複数回呼び出すこともできます。そのため、オブジェクトを必要な数だけ作成できます。

ここで1つの疑問が生じます。このアプローチでは、呼び出されたメソッドがインスタンス化を事実上制御することになります。しかし、これによってコール元は制御を奪われることになるのでしょうか。選択肢Aで問題となっていた欠点が打ち消される可能性はあるでしょうか。その答えはノーです。どのようにVehicleを作成するかをコール元が完全に制御できる点は変わりません。失うのは、オブジェクトを作成するタイミングという一部だけです。実際、コール元は、新しいオブジェクトを作成するのではなく、既存のオブジェクトを渡すだけのSupplierを提供することもできます。ただし、今回の場合は、この2つの側面が互いを打ち消し合わないように、コール元のプログラマーと呼び出されるメソッドのプログラマーとの間で、設計の意図についての共通認識が必要になります。

この時点で、選択肢Aで説明した潜在的な利点が、選択肢Cの利点の一部でしかないことは明らかなはずです。この設問が単一選択で、選択肢Bが誤りであることを考えれば、選択肢Aは誤りと判断し、この時点では選択肢Cが唯一の正解候補と考えることになるはずです。

選択肢Dには、リファクタリング後はVehicleインスタンスのキャッシュが可能になるとあります。当然ながら、Supplierの実装ではキャッシュを使用できます。しかし、Supplierを提供するのはコール元の責任であるため、キャッシュをサポートするかどうかを決めるのはコール元の責任です。リファクタリング前の設計では、コール元が直接Vehicleを提供していますが、この設計でもキャッシュ自体には簡単にアクセスすることができます。結論としては、コール元が制御するキャッシュをSupplierで使用できるのは確かですが、これは具体的な利点であるとは言えません。リファクタリング前のアプローチでも同じことができ、いずれかのアプローチの方が優れていると考えられるわけではないからです。そのため、選択肢Dは誤りです。

選択肢Eには、Supplierによって、Vehicleの別のサブクラスが使いやすくなるとあります。この選択肢が合理的でないのは明らかです。一般化という考え方により、Vehicleのサブクラスは実行時に置き換えることができます。この設問では、コール元はVehicle、またはVehicleのSupplierを提供しなければなりません。そして、いずれの場合でも、コール元は代入互換性があるオブジェクトを渡してメソッドを呼び出すことができます。そのため、選択肢Eは誤りです。

以上の内容から、選択肢Cは明らかに選択肢Aより優れており、その他3つの選択肢に実際の利点はないことがわかったはずです。よって、選択肢Cは正解です。

1つ補足しておきます。遅延インスタンス化は、適切な状況で使用すれば非常に大きなメリットが得られる可能性があります。そして、このアプローチは現在、ロギング・フレームワークで広く使用されています。ログ・メッセージを準備する場合、Exceptionのスタック・フレームを走査して多くの文字列連結を行うのが普通です。これをすべて行うためには多くのCPUが必要になり、メモリも大量に使用するかもしれません。それにもかかわらず、ロギング・レベルが「error」や「severe」に設定されている場合の「trace」レベルのメッセージのように、完全に破棄されるメッセージもよくあります。実際のStringではなくSupplier<String>をロギング・システムに渡すことで、メッセージが実際には使用されない場合のメッセージ作成処理を避けることができます。

リファクタリング前のバージョンで、単純にロギングを呼び出すコードは次のようになるでしょう。

logger.log(Level.FINER,
  Stream
    .of(t.getStackTrace())
    .map(StackTraceElement::toString)
    .collect(Collectors.joining("\n"));

この場合、非常に高価なlogの呼出しが行われる前にメッセージが作成されている点に注意してください。実際にはロガーがメッセージをそのまま捨ててしまうかもしれない場合でも同様です。

一方、次の形式では、メッセージを必要としないロギング・レベルの場合、メッセージ作成操作は必要ないと認識されます。なお、この例で示した、手動でのスタックの走査と収集は、論点を強調するためにあえてこのようにしています。例外のトレースを処理する際の適切な方法を示すものではありませんので、ご注意ください。

logger.log(Level.FINER, 
  () -> Stream
    .of(t.getStackTrace())
    .map(StackTraceElement::toString)
    .collect(Collectors.joining("\n"));

正解は選択肢Cです。


Simon Roberts

Simon Roberts:Sun Microsystemsがイギリスで初めてJavaの研修を行う少し前にSunに入社し、Sun認定Javaプログラマー試験とSun認定Java開発者試験の作成に携わる。複数のJava認定ガイドを執筆し、現在はフリーランスでPearson InformITにおいて録画やライブによるビデオ・トレーニングを行っている(直接またはO'Reilly Safari Books Onlineサービス経由で視聴可能)。OracleのJava認定プロジェクトにも継続的に関わっている。

 

Mikalai Zaikin

Mikalai Zaikin:ベラルーシのミンスクを拠点とするIBA IT ParkのリードJava開発者。OracleによるJava認定試験の作成に携わるとともに、複数のJava認定教科書のテクニカル・レビューを行っている。Kathy Sierra氏とBert Bates氏による有名な学習ガイド『Sun Certified Programmer for Java』では、3版にわたってテクニカル・レビューを務めた。

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.