実践的Javaクラス設計講座 - 中級 2012年3月10日 株式会社フォーエス 作者不詳 対象者 ■現場の技術リーダーになった人、またはなりたい人。 ■ライブラリやフレームワークを作りたい人。 ■Javaによるプログラミングは一通りこなせるようになったが、クラス設計などはまだ よくわからない人。 ■一応クラス設計はできるつもりだが、本当に正しいのかイマイチ自信がない人。 ■クラス設計のコツが知りたい人。 ■他の人がどういう思想でクラス設計をやっているかに興味がある人。 ■机上の空論ではなく、実際のコーディングに活かせるクラス設計に興味がある人。 2 はじめに ■最初にこんなことを言うのもなんだが、クラス設計に絶対的な正解など無い。 プログラムは別にどこに書いても内容さえ正しければ動く。 ■しかし、クラス設計の正しい知識が無ければ非効率でミスが起きやすく、メンテし づらいプログラムに陥りやすいので、一般的な知識は押さえておいたほうが良い。 ■クラス設計と一言でいっても、実際にはプロジェクトの共通部分設計・ライブラリ設 計・フレームワーク設計では方法が若干異なるが、今回は区別せず扱う。 ■クラス設計には個人の嗜好が強く出るので、今回の内容が自分の好みと合わな いと思ったら遠慮無くスルーしてもらって構わない。 ■本資料ではUMLを使った説明は行わない。 UMLはもちろん正しく読めて、自分でも書ける方が良いが、一時のブームは去っ たのでもはや必須ではない。 とはいえ、クラス設計の際の補助や、他人にサッと説明するツールとしてはなか なか優秀なので、描ける人は描いていった方が良い。 3 ちなみに・・・ ★UMLのクラス図とはこんな感じのやつです 4 ついでに・・・ ★UMLのシーケンス図とはこんな感じのやつです 5 まずは定石を知っておこう ★デザインパターン ・過去のソフトウェア設計者が発見し編み出した設計ノウハウを蓄積し、名前をつけ、 再利用しやすいように特定の規約に従ってカタログ化したもの。 ・一般的には、GoFによる23のパターンのことを指す。 23のパターンは一通り知っておいた方が良いが、実際に自分で作成するクラスで 使用するのはごく一部。 ・Template Method、Singleton辺りはよく使う。 ・どちらかというと、抽象クラスやインターフェースの使い方や、クラス設計のコツを 学ぶ教材として優秀。 ・暗記する必要はないが、頭の片隅には残る程度にはなっておきたい。 6 まずは定石を知っておこう ★デザインパターン ■おすすめの本 増補改訂版Java言語で学ぶデザインパターン入門 作者: 結城 浩 (注)内容が大分変化しているので、いまから買うなら必ず増補改訂版にすること 7 まずは定石を知っておこう ★デザインパターン ■おすすめでない本 オブジェクト指向における再利用のためのデザインパターン(通称GoF本) 作者:エリック ガンマ 、ラルフ ジョンソン、リチャード ヘルム 、 ジョン ブリシディース デザインパターンの原本はこちらだが、読みにくい上に内容もイマイチなのでおすすめしない 8 まずは定石を知っておこう ★マルチスレッドデザインパターン ・ マルチスレッドプログラミングにおけるデザインパターン。 ・ 通常のデザインパターンほど有名ではない上、かなり難易度が高い。 ・ Immutable、Producer-Consumer、Future辺りは意味を理解して使えるようになっ ておくと便利。 ・パターンそのものよりも、マルチスレッドプログラミングやJavaの言語仕様に対する 理解を深める教材として優秀。 ・ JDK 5.0で追加されたjava.util.concurrentパッケージに便利なクラスが揃っている。 9 まずは定石を知っておこう ★マルチスレッドデザインパターン ■おすすめの本 増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編 作者: 結城 浩 (注)内容が大分変化しているので、いまから買うなら必ず増補改訂版にすること 10 まずは定石を知っておこう ★アンチパターン ・システム開発において「こうあるべきだ」というセオリーがデザインパターンである のに対し、「こうあってはならない」のがアンチパターン。 ・純粋な技術書ではなく、バッドノウハウを集めた読み物に近い。 ・プロジェクト全般について書かれた本なので、クラス設計に関する部分は一部。 ・どちらかというとプログラマーよりも、使えないマネージャーとかに読んでもらいた い本。 ・もし自分の仕事がアンチパターンに陥っているなら、すぐに改善しよう。 11 まずは定石を知っておこう ★アンチパターン ■おすすめの本 アンチパターン―ソフトウェア危篤患者の救出 作者: W.J. ブラウン他 12 まずは定石を知っておこう ★リファクタリング ・リファクタリングとは、プログラムの外部から見た動作を変えずにソースコードの内 部構造を整理すること。 ・プログラムは一度作れば終わりではなく、状況に応じて修正を加えていくことが必 要になるが、その際に役に立つテクニックについて学べる。 ・また、最初からプログラムを作る場合にも有効なテクニックも多い。 ・効果的なリファクタリングができるようになれば、つぎはぎだらけの見るに耐えない プログラムになるのを回避できる(かもしれない)。 ・リファクタリングで重要なのはバランス感覚。 何事もやりすぎは良くない。 13 まずは定石を知っておこう ★リファクタリング ■おすすめの本 リファクタリング―プログラムの体質改善テクニック(通称ファウラー本) 作者:マーチン・ファウラー (注)翻訳の割には日本語訳が悪くないので、なかなか読みやすい 14 まずは定石を知っておこう ★リファクタリング ■おすすめの本 Java言語で学ぶリファクタリング入門 作者:結城 浩 (注)先ほどから結城さんの本ばかり紹介しているが、別に私は結城さんの回し者で はない 15 オブジェクト指向の正しい定義などどうでもいい 【よくあるオブジェクト指向の説明】 ・オブジェクトとはデータと振る舞いを持った「もの」である。 ・カプセル化とはデータを隠蔽することである。 ・オブジェクト指向言語では継承を使用することによって再利用が促進される。 (差分プログラミング) ・オブジェクトとは現実世界における実体であるため、名詞と動詞を抽出することで 適切なクラス設計を行うことができる。 ・同じ哺乳類でもネコは「ニャー」、犬は「ワン」と鳴く。 これが多態性(ポリモーフィズム)である。 こんなもの実際のプログラミングにおいては何の役にも立たない 概念的な研究などお偉い学者先生様にでも任せていればいい 16 オブジェクト指向の正しい定義などどうでもいい 【より実践的なオブジェクト指向の説明】 ・クラスはフィールドとメソッドから構成されるプログラム単位である。 ・クラスは特定の機能を実現しやすくするため、便宜的に分けられる。 ・カプセル化とは、必要な機能以外は外部から操作できないようにすることである。 決して、フィールドをprivateにしてgetter, setterメソッドを用意することではない。 (getter, setterメソッドを用意する時点で外部から操作できてしまう) というか、そもそもカプセル化とかいう言葉自体がどうでもいい。 ・継承には実装の継承とインターフェースの継承の二種類がある。 このうちインターフェースの継承の方がより重要だとわかるようになれば、初心者 卒業といえる。 ・インターフェースと抽象クラスは、あくまでも多重継承ができないというJavaの言語 仕様上の都合から分かれているもので、本質的な違いはない。 あくまでも単純化した解釈なので、厳密に言うと色々と語弊があります 17 クラス設計の基本的な考え方 ・「あるべき場所を考える」、この一言につきる。 「データはどういう固まりで存在すべきか」、「この処理はどこに記述すべきか」など。 ・明確な答えはなく、人によってやり方はさまざまだが、そこには一貫した設計思想 がなければいけない。 他人に「なんでこういうクラス構成にしてるの?」と聞かれて、「いや、何となく」としか 答えられないようなら問題外。 場所によって設計思想がバラバラだともっと最悪。 ・「実際に作業する人がいかに楽をできるか」といった点を考慮するのも重要。 概念的には正しくても、実装クラスが1つしかないのがわかりきっているのに、いち いちインターフェースを作るのを強要するようなクラス設計は愚の骨頂。 ・できることの幅を狭めてしまうようなクラス設計は良くない。 定型処理は簡単にできるようにし、かつ選択肢の幅を広げるのが良いクラス設計。 18 層を意識しよう ・層(layer)を分けることにより、クラスの役割を明確化することができる。 ・フレームワークを使うと、良くも悪くも層を分けることが強制されることが多い。 ・理論的には、各層は独立しているので単体テストなどがやりやすい。 (だが、現実ではそんなにうまくいかない) ・外部のシステムと連携する部分は、専用の層を作っておいた方が良い。 ここでいう外部のシステムとは、余所の会社やチームが作ったものだけでなく、DB アクセスやネットワーク呼び出しなども含む。 ・層ごとにパッケージを分けて、各クラス名の末尾(suffix)には層の名前を入れておく とわかりやすい。 例えば、Service層に属するクラスなら、serviceパッケージに集めて、クラス名は xxxServiceといった風にすると良い。 19 層を意識しよう ・層に特有のオブジェクトは、他の層に渡してはいけない。 HttpServletRequestをDBアクセスするクラスにまで渡したり、ResultSetをJSPまで 引っ張ってくるような構造はもってのほか。 ・層がやたらと多いフレームワークは大体腐っている。 他の層をそのまま呼び出すだけの実装が多発するようであれば、その層はおそら く不要だということなので、設計を見直したほうがいい。 ・各層ごとに実装クラスが1つしかないにも関わらず、インターフェースと実装クラス を必ずセットで用意しないといけないフレームワークはさらに腐っている。 20 層を意識しよう よく目にする層 【その1】 Service(サービス) ・サーバーサイドプログラムで、リクエストを受け付けてレスポンスを返すというクラ イアントとのやり取りの受け口になる層。 ・作成単位は画面単位、または機能単位。 ・1つのServletで処理を受け付けて、割り振りするタイプのフレームワークで使われ ることが多い。 ・StrutsのActionもこの一種と言える。 ・ビジネスロジックはServiceに直接実装することもあるし、Logicに委譲することもあ る。 その辺りのさじ加減は人それぞれだが、プロジェクト内では方式を統一しておいた 方が良い。 ・少なくともDBアクセスに関する処理は直接ここには記述せずに、DAOに任せた方 がよい。 21 層を意識しよう よく目にする層 【その2】 Logic(ロジック) ・ビジネスロジックを実装する層。 人やフレームワークによって解釈が様々なので、意識のずれが発生しやすい危険 な層といえる。 ・作成単位は画面単位、機能単位、トランザクション単位などバラバラ。 ・個人的にはあまり好きではないし、必要性も感じない。 強いて言えば、複数のService間で共通の処理が必要になった場合にのみ作れば いいかと考えている。 ・DBアクセスに関する処理は直接ここには記述せずに、DAOに任せた方がよい。 22 層を意識しよう よく目にする層 【その3】 DAO(Data Access Objectの略。ディーエーオー) ・直接DBにアクセスする処理を実装する層。 ・作成単位はDBのテーブル単位。 ・Entityとセットになることが多い。 ・メソッドの分割の目安としては、1つのメソッドで1種類のSQLを実行するのが良い。 メソッド名も、insertXXX()やselectXXX()というように、実行するSQLの内容を表すも のにしておくとわかりやすい。 ・トランザクション制御はここでは行わずに、呼び出し元のServiceやLogicで行うの が良い。 ・複数のテーブルを結合するSELECT文などを実行する場合は、どのDAOにメソッド を用意すれば良いか悩むことになるが、基本的にメインとなるテーブルのDAOに すれば良い。 ・「ダオ」って読むな! 23 層を意識しよう よく目にする層 【その4】 DTO(Data Transfer Objectの略。ディーティーオー) ・データの入れ物を表す層。 ・作成単位はデータ単位。 ・他にもJavaBeans、Value Objectなど色々な呼び方があるが、本質的にはどれも同 じもの。 その中でも、DTOというと異なる層の間でデータを受け渡しするための入れ物とい う意味合いが強いが、単なるデータの入れ物の場合もDTOと呼ばれる。 ・C言語でいうところの構造体。 ・java.io.Serializableインターフェースを継承しておくのが基本。 ・あくまでもデータの入れ物に徹して、余計なメソッドは実装しない方が良い。 ・publicフィールド派と、getter, setter派がいるが、私は断然publicフィールド派。 そもそも、 getter, setterとか無意味なものが推奨されている意味がわからない。 ただし、残念ながらライブラリやフレームワークによってはpublicフィールドが扱え ないことも多い。(海外製に多い) 24 層を意識しよう よく目にする層 【その5】 Entity(エンティティ) ・DBのテーブルにマッピングするデータの固まりを表す層。 ・作成単位はテーブル単位。 ・DTOの一種だが、テーブルにマッピングするものは特別にEntityと呼ばれる。 ORマッピングのフレームワークで使われることが多い。 ・クラスがテーブル、各フィールドがカラムにマッピングされる。 ・マッピングを補助するために、アノテーションを指定するタイプのものもある。 Javaのソース内で設定を完結できるため書きやすく、後からソースを見た人にもわ かりやすいのでなかなか良い。 ただし、カラム名とフィールド名が同じ場合にでも、全フィールドにアノテーションを 指定しないといけないタイプのものは、冗長すぎてうっとうしいし、面倒くさい。 ・マッピングを補助するために、外部の設定ファイルが必要なものもあるが、ミスが 起きやすいし、無駄な労力が発生するので個人的には好きではない。 25 層を意識しよう よく目にする層 【その6】 Utility(ユーティリティ) ・ビジネスロジックに依存しない便利関数を集めた層。 層というよりも、クラス群といった趣が強い。 ・作成単位は必要になった場合その都度。機能別にクラスを分類する。 ・他のプロジェクトでも使い回しが効くような、システムに依存しない中立的な内容の ものがUtilityとして扱われる。 大抵のプロジェクトで見かけるおなじみのStringUtilクラスなどが代表例。 ・ほとんどのライブラリに何らかのユーティリティクラスが含まれているので、クラス 名が衝突しやすい。使うときに間違って別物をimportしがちなので注意。 ・自作せずともJakarta Commonsなどを探せば大抵のものは見つかるが、いまいち 使い勝手が悪かったり、依存ライブラリが増えてしまうという問題があるので、結 局みんなオリジナルのものを作ることが多い。 そして、似たようなクラスがどんどん増えていく・・・ ・パッケージ分けとしては、utilといった専用のパッケージを用意することが多いが、 commonといった共通パッケージに置くのも悪くない。 26 層を意識しよう よく目にする層 【その7】 Manager(マネージャー) ・特定の機能に対する処理を集中して行う層。 層というよりも、単独のクラスのことが多い。 ・作成単位は必要になった場合その都度。 ・役割としてはLogicに近いが、こちらは量産するタイプのクラスではないという点が 異なる。 ・個人個人が作成する類のクラスではなく、プロジェクトの共通担当や、ライブラリ作 成者のみが作る類のクラスである。 ・特殊なロジックや複雑な処理を実装することが多いので、他のプロジェクトには流 用できない、一点物のクラスになりやすい。その点がUtilityと異なる。 ・個人的には、とりあえず分類に困ったらManagerとして扱うことが多い。 何とも言葉では説明しづらいが、自分でクラス設計をするようになれば、Manager とつけたくなる気持ちがわかるはず。 ・パッケージ分けとしては、commonといった共通パッケージに置かれることが多い。 27 層を意識しよう よく目にする層 【その8】 Const(Constantの略。コンスト) ・定数を定義する層。 ・作成単位はプロジェクトに1つ。または機能ごと。 ・定数は必要なクラスに定義するのが原則だが、プロジェクトやライブラリに共通の ものは専用のクラスを作って、そこに定義する方が良い。 ・パッケージ分けとしては、 commonといった共通パッケージに置くことが多いが、 ク ラス数が増えるようであればconstantといった専用のパッケージを用意するのも 悪くない。 ・「const」だけだと予約語と認識されて使用できないので注意。 28 層を意識しよう よく目にする層 【その9】 Presentation(プレゼンテーション) ・実際に画面に表示する内容を制御する層。 ・作成単位は画面単位。 ・Viewとも呼ばれる。 ・サーバーサイドプログラムでは、JSPやHTML、XMLがこれに当たることが多い。 基本的にServiceから渡されてきたデータを元にして、画面を作成する以外のこと はしないし、してはいけない。 ここでビジネスロジックの実装、ひいてはDBアクセスを行うなど論外。 ・クライアントサイドプログラムでは、画面表示用のクラスがこれに当たることが多い。 この場合、Serviceと混合したような形式のクラスになることが多いので、ビジネス ロジック用の処理、画面制御用の処理、イベント制御用の処理がゴチャゴチャに なりがちなので要注意。 (この問題に関しては、私自身どうすればスマートになるかを模索している最中な ので、まだ解決策を語れる立場ではないです。すみません。) 29 プロジェクト用のベースクラスを用意しよう ・層ごとにプロジェクト用のベースクラスを用意しておくと、共通の処理を実装できる ので、量産対象のクラスでは固有の処理だけに集中できるようになる。 さらに、後から全体に影響する仕様変更が入った場合などにも対応しやすくなる。 ・フレームワークにおける決まった親クラスがある場合は、それを継承したクラスを ベースクラスにして、個々のクラスはそのベースクラスを継承するようにする。 特に決まった親クラスがない層の場合は、Objectクラスの代わりにベースクラスを 用意して、個々のクラスはそのベースクラスを継承するようにする。 クラス名は、「Base + 層の名前」とかにしておくといい。 ・プロジェクトの最初の頃にはベースクラスに実装すべき内容が思いつかないかもし れないが、とりあえず用意しておけば後々役に立つことが多い。 ・この方法は、次に説明するTemplate Methodとの相性がバツグン。 ・ただし、DTOクラスに関しては別にベースクラスを作らなくてもいいし、作ってもいい。 30 Template Methodを使いこなそう ・Template Methodとはデザインパターンの一種で、処理の流れをスーパークラスで 決定しておいて、具体的な処理の実装をサブクラスにまかせること。 ・個人的には、抽象クラスの継承の使い方の基礎にして究極だと思っている。 ・おそらくTemplate Methodを特別意識していなくても、クラス設計を行ううちに自然と このパターンの形式になっていることも多いと思われる。 ・特にフレームワークや、ベースクラスを作るときによく使われる。 Servletクラスなどもこの一種。 ・わざわざ紹介したのは、簡単な仕組みの割に非常に実用的だから。 また、スーパークラスからサブクラスで実装される予定のメソッドを呼び出すという 考え方をマスターすれば、今後のクラス設計においても大いに役立つ。 31 Template Methodを使いこなそう 【実装例】 スーパークラス /** * Template Methodのスーパークラスとなる抽象クラス。 */ public abstract class AbstractClass { //========================================================== //メソッド //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //通常のメソッド /** * 処理の流れを決定するメソッドです。 * 外部から直接呼び出されるメソッドでもあります。 */ public final void execute() { //サブクラスで実装される前処理を呼び出す executeBefore(); //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //抽象メソッド //(オーバーライド対象(フックポイント)のメソッドです。) //(具体的な処理はサブクラスで実装します。) /** * 前処理を実行します。 */ protected abstract void executeBefore(); //このようにサブクラスで実装されない処理を挟み込んでも良い System.out.println("☆ メインの処理 - 開始 ☆"); //サブクラスで実装されるメインの処理を呼び出す executeMain(); /** * メインの処理を実行します。 */ protected abstract void executeMain(); //ログ出力や、例外処理などが書かれることが多い System.out.println("★ メインの処理 - 終了 ★"); //サブクラスで実装される後処理を呼び出す executeAfter(); /** * 後処理を実行します。 */ protected abstract void executeAfter(); } } 32 Template Methodを使いこなそう 【実装例】 サブクラスその1、その2 /** * Template Methodのサブクラスその1。 */ public class ConcreteClass1 extends AbstractClass { //========================================================== //オーバーライドするメソッド /** * Template Methodのサブクラスその2。 */ public class ConcreteClass2 extends AbstractClass { //========================================================== //オーバーライドするメソッド /** * 前処理を実行します。 */ @Override protected void executeBefore() { System.out.println("サブクラス1の前処理ですよ"); } /** * 前処理を実行します。 */ @Override protected void executeBefore() { System.out.println("サブクラス2の前処理!"); } /** * メインの処理を実行します。 */ @Override protected void executeMain() { System.out.println("サブクラス1のメイン処理ですよ"); } /** * メインの処理を実行します。 */ @Override protected void executeMain() { System.out.println("サブクラス2のメイン処理!"); } /** * 後処理を実行します。 */ @Override protected void executeAfter() { System.out.println("サブクラス1の後処理ですよ"); } /** * 後処理を実行します。 */ @Override protected void executeAfter() { System.out.println("サブクラス2の後処理!"); } } } 33 Template Methodを使いこなそう 【実装例】 呼び出し例、実行結果 /** * Template Methodを呼び出すクラス。 */ public class TemplateMethodTest { public static void main(String[] args) { System.out.println("== サブクラスその1に対する呼び出し =="); AbstractClass concreteClass1 = new ConcreteClass1(); concreteClass1.execute(); (実行結果) == サブクラスその1に対する呼び出し == サブクラス1の前処理ですよ ☆ メインの処理 - 開始 ☆ サブクラス1のメイン処理ですよ ★ メインの処理 - 終了 ★ サブクラス1の後処理ですよ //空行出力 System.out.println(); System.out.println("== サブクラスその2に対する呼び出し =="); == サブクラスその2に対する呼び出し == サブクラス2の前処理! ☆ メインの処理 - 開始 ☆ サブクラス2のメイン処理! ★ メインの処理 - 終了 ★ サブクラス2の後処理! AbstractClass concreteClass2 = new ConcreteClass2(); concreteClass2.execute(); } } 34 Template Methodを使いこなそう 【イメージ図】 スーパークラスで処理の流れ を決定する サブクラスで具体的な処理 の内容を実装する 処理1(抽象メソッド) 処理1の具体的な実装 処理2 処理3 (抽象メソッド) 処理3の具体的な実装 処理4 処理5 (抽象メソッド) 処理5の具体的な実装 35 Template Methodを使いこなそう ★間違ったTemplate Method ・スーパークラスが抽象クラスではなく、インターフェースになっている。 →スーパークラスで処理の流れの部分を実装するのがこのパターンの肝なので、 実装を含めることのできないインターフェースではTemplate Methodになりえない。 ・サブクラスが全てのメソッドをオーバーライドしている。 →処理の流れを決定しているメソッドはオーバーライドしてはいけない。 そもそもオーバーライドできないようにするため、finalをつけておくのを推奨。 ・オーバーライド対象のメソッドがpublicになっているので、他のクラスから直接呼ば れる。 →オーバーライド対象のメソッドはあくまでも内部処理用のメソッドであり、他のク ラスから直接呼び出すようなメソッドではないので、publicではなくprotectedにし ておくこと。 36 Template Methodを使いこなそう ★こんなときはどうするの? ・オーバーライドできるメソッド(フックポイント)が足りなくて、サブクラスで固有の処理 が実装できない。 →フックポイントが足りないとどうしようもないので、スーパークラスを改造する。 ・オーバーライドしないといけない抽象メソッドが多すぎてやってられない。 →大半のケースでは処理が必要ない場合は、抽象メソッドにこだわらず、スー パークラスでは空実装のメソッドにしておいて、必要な場合のみオーバーライド するようにしても良い。 ただし、抽象メソッドの場合はサブクラスで実装を忘れていたらコンパイルエラー が発生するのに対して、空実装の場合はエラーにならないため、オーバーライド が必要な場合でもうっかり抜けてしまうという危険性があるので、一長一短。 折衷案として、よくオーバーライドするメソッドだけを実装した中間クラスを用意し ておいて、末端のクラスはその中間クラスを継承するという方式もあり。 37 Singletonとstaticの使い分け ・Singletonとはデザインパターンの中でおそらく最も有名なパターンで、そのクラス のインスタンスが1つしか生成されないことを保証するための仕組み。 ・ただし、実際はインスタンスが1つしか生成されないことを保証するという点が重要 視されることはあまりない。 むしろ他の言語におけるグローバル変数やグローバル関数のように、どこからで も自由に呼び出せる便利な仕組みとして利用されることが圧倒的に多い。 ・一方、全てのフィールドやメソッドをstaticで宣言したクラスも、Singletonとほぼ同じ 性質を持つ。 ・そのため、Singletonにすべきか、staticのみのクラスにすべきか悩むことがよくある。 ・結論から言うと、大抵の場合は別にどっちでもいい。 好みの問題で済ませられることが多い。 とは言いつつも、やはり場合によっては使い分けが必要なこともある。 38 Singletonとstaticの使い分け 【実装例】 Singletonクラスの例 /** * Singletonのサンプルクラス。 */ public class SingletonSample { //========================================================== //定数 //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //クラスメソッド /** * このクラスの唯一のインスタンスを返します。 * * @return このクラスの唯一のインスタンス */ public static SingletonSample getInstance() { //フィールドの自分自身のクラスの唯一のインスタンスを返す return singleton; } /** * 自分自身のクラスの唯一のインスタンス */ private static final SingletonSample singleton = new SingletonSample(); //========================================================== //メソッド //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //通常のメソッド //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //コンストラクタ /** * 通常のメソッドその1。 */ public void foo() { //何らかの実装 } /** * コンストラクタです。 * (Singletonクラスなので、privateにして外部からインスタンスを 作成できないようにしています。) */ private SingletonSample() { //特に何も行わない } /** * 通常のメソッドその2。 */ public void bar() { //何らかの実装 } } 39 Singletonとstaticの使い分け 【実装例】 staticのみのクラスの例 /** * Staticのみのサンプルクラス。 */ public class StaticSample { //========================================================== //メソッド /** * 通常のメソッドその2。 */ public static void bar() { //何らかの実装 } } //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //コンストラクタ /** * コンストラクタです。 * (staticのみのクラスなので、privateにして外部から無駄なイン スタンスを作成できないようにしています。) * (別にこのコンストラクタの記述はなくても成立するが、あった方 が良い。) */ private StaticSample() { //特に何も行わない } //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //クラスメソッド /** * 通常のメソッドその1。 */ public static void foo() { //何らかの実装 } 40 Singletonとstaticの使い分け 【実装例】 呼び出し例 /** * Singletonクラスとstaticのみのクラスを呼び出すクラス。 */ public class SingletonStaticCallTest { public static void main(String[] args) { //======================================================== //Singletonクラスの呼び出し //(直接呼び出すパターン) SingletonSample.getInstance().foo(); SingletonSample.getInstance().bar(); //======================================================== //コンストラクタはprivateなので、こういうことをやるとコン パイルエラーになります //SingletonSample hoge = new SingletonSample(); //StaticSample xxx = new StaticSample(); } } //======================================================== //Singletonクラスの呼び出し //(一度インスタンスを変数に受けてから呼び出すパターン) SingletonSample singletonSample = SingletonSample.getInstance(); singletonSample.foo(); singletonSample.bar(); //======================================================== //staticのみのクラスの呼び出し StaticSample.foo(); StaticSample.bar(); 41 Singletonとstaticの使い分け ★Singletonの方が良い場合 ・インスタンスを渡す必要がある場合には、staticは使えないので、必然的に Singletonを使うことになる。 ・何らかのクラスを継承する場合は、スーパークラスのメソッドはstaticでない普通の メソッドなので、Singletonを選ぶことになる。 ・フィールド(状態)を持つ場合は、staticでも実装できないことはないが、Singletonの 方が自然。 ただし、フィールドといっても、ログ出力用のオブジェクトや、フォーマット変換用の オブジェクトといった内部作業用のものは状態とは言えないので、このケースに は当てはまらない。 ・Managerクラスなどで個別のインスタンスが必要ない場合は、Singletonにすること が多い。 42 Singletonとstaticの使い分け ★staticの方が良い場合 ・ユーティリティクラスのように、独立したメソッドがズラズラ並んでいる場合はstatic の方が良い。 ・フィールド(状態)を持たない場合は、staticの方が自然。 ・Singletonと比べて、ほんの少しだけメモリ使用量が減り、メソッド呼び出しの際のコ ストも下がる(らしい)。 そのため、シビアなパフォーマンスが求められる場合はstaticの方が有利になる (らしい)。 (らしい)と付けているのは、あくまでも理論上の話で、私自身が実感できるほどの パフォーマンスの差を感じたことがないため。 43 Singletonとstaticの使い分け ★補足 ・基本的にstaticで良い場合は、Singletonでも代用できる。 そのため、迷ったらSingletonにしておく方が手堅い。 ・ただし、staticの方が呼び出しの記述が少なくて済むので、手っ取り早く使えるとい う地味ながら大きなメリットがある。 また、staticクラス自体もSingletonクラスよりも簡単に作れる。 (どこかの元ネタのソースからコピペすれば、それほどの違いはないが・・・) ・ staticでしか使わないクラスのインスタンスは生成しても意味が無いので、コンスト ラクタはprivateにしておこう。 そうすれば、外部から誤ってnewでインスタンスを生成しようとしてもコンパイルエ ラーが発生することになるので、愚かな行為を未然に防ぐことができる。 44 初期化に必要なデータはコンストラクタの時点でまとめて渡そう ・コンストラクタを呼び出した時点でクラスの初期化が完了していないと、不安定な 状態のまま使われてしまい、思わぬエラーが発生してしまうことがある。 そのため、コンストラクタの時点でそのクラスが正しく動作するのに必要なデータ は全て渡しておき、インスタンス生成が完了したタイミングで全ての準備がOKの 状態になるようにするのが望ましい。 ・同様の理屈で、コンストラクタを呼び出した後、さらにinitialize()といったメソッドで初 期化処理が必要なメソッドもよろしくない。 ・コンストラクタで受け取ったデータをフィールドに設定する場合は、finalなフィールド にしておくといい。 そうすることで、初期化漏れがあればコンパイルエラーが発生するのですぐにわ かるし、後から間違ってフィールドの値を上書きしてしまうのも未然に防げる。 ・ここで言っているのはあくまでも初期化に必要なデータのみなので、データのみで できているDTOクラスの場合などは気にしなくていい。 45 初期化に必要なデータはコンストラクタの時点でまとめて渡そう 【実装例】 悪い初期化の仕方の例 /** * 悪い初期化のサンプルクラス。 */ public class BadInitSample { //========================================================== //フィールド //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //通常のメソッド /** * 数値を設定します。 * * @param num 数値 */ public void setNum(int num) { //引数の値をそのままフィールドに設定 this.num = num; } /** * 数値 */ private int num = 0; /** * 文字列 */ private String str = null; /** * 文字列を設定します。 * * @param str 文字列 */ public void setStr(String str) { //引数の値をそのままフィールドに設定 this.str = str; } //========================================================== //メソッド //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //コンストラクタ /** * フィールドの内容を表示します。 */ public void display() { //フィールドの内容を表示 System.out.println("数値=" + this.num + ", 文字列=" + this.str); } } /** * 何も引数を取らないコンストラクタです。 */ public BadInitSample() { //何もしない } 46 初期化に必要なデータはコンストラクタの時点でまとめて渡そう 【実装例】 良い初期化の仕方の例 /** * 良い初期化のサンプルクラス。 */ public class GoodInitSample { //========================================================== //フィールド /** * 初期化に必要なデータを全て受け取るコンストラクタです。 * * @param num 数値 * @param str 文字列 */ public GoodInitSample(int num, String str) { //======================================================== //引数の値をフィールドに設定 /** * 数値 */ private final int num; //数値 this.num = num; /** * 文字列 */ private final String str; } //========================================================== //メソッド //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //通常のメソッド //文字列 this.str = str; //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //コンストラクタ /** * フィールドの内容を表示します。 */ public void display() { //フィールドの内容を表示 System.out.println("数値=" + this.num + ", 文字列=" + this.str); } } 47 初期化に必要なデータはコンストラクタの時点でまとめて渡そう 【実装例】呼び出し例、実行結果 /** * 初期化のサンプルを呼び出すテストクラス。 */ public class InitSampleCallTest { public static void main(String[] args) { //======================================================== //悪い初期化のサンプルクラスの呼び出し //(正しい手順で呼び出すパターン) { //悪い初期化のサンプルクラスのインスタンスを生成 BadInitSample sample = new BadInitSample(); //数値だけ設定して、文字列の設定が漏れている sample.setNum(456); //フィールドの内容を表示 sample.display(); } //======================================================== //良い初期化のクラスの呼び出し { //良い初期化のサンプルクラスのインスタンスを生成 //(コンストラクタで数値と文字列を渡さなければならない ので、漏れる心配がない) GoodInitSample sample = new GoodInitSample(789, "良い サンプル"); //数値と文字列を設定 sample.setNum(123); sample.setStr("悪いサンプル"); //フィールドの内容を表示 sample.display(); //フィールドの内容を表示 sample.display(); } } } //======================================================== //悪い初期化のサンプルクラスの呼び出し //(初期化漏れがあるパターン) { //悪い初期化のサンプルクラスのインスタンスを生成 BadInitSample sample = new BadInitSample(); } (実行結果) 数値=123, 文字列=悪いサンプル 数値=456, 文字列=null ←初期化漏れがある 数値=789, 文字列=良いサンプル 48 複数のデータをまとめるときは専用のクラスを作ろう ・メソッドの引数は複数指定できるので問題が起きにくいが、戻り値は1つしか返せ ないので、複数の値を一度に返したい場合に困ることになる。 (ここでいう複数の値とは、単純なリストや配列ではなく、それぞれ用途や型が違う バラバラのデータのこと) ・だからといって、MapやObjectの配列にデータを無理やり詰めて返すようなやり方 は大変よろしくない。 クラスの内部処理で使っているだけで、他からは一切見えないのであればギリギ リ許容範囲といえなくはないが、ライブラリや共通部分でこんなことを堂々とやら れた日には、「今すぐプログラマーをやめろ」と苦情の一つも言いたくなってくる。 ・このような場合は、横着せずに専用のDTOクラスを作った方が良い。 そのクラスの中だけで使うのであれば、private staticな内部クラスにすると良い。 パッケージ内だけで使うのであれば、パッケージスコープのクラスにすると良い。 どこからでも使うのであれば、普通にpublicなクラスにすると良い。 49 引数はなるべく上位の型で受けよう ・引数をなるべく上位の型で受けるようにすると、扱える型が増えるのでメソッドの可 能性が広がる。 ・特にコレクション系のクラスの場合、このケースに当てはまることが多い。 ArrayListよりもList、ListよりもCollectionというように上位の型にしておくことで、当 初想定していたArrayListだけでなく、Setなども扱えるようになる。 ・ここで注意すべきなのは、あくまでも処理を行うのに必要最低限の型にはしておく ということ。 極端な例を挙げると、Object型のためどんな型の引数でも受け付けるが、実際に はStringしか扱えないといった良くないメソッドも考えられる。 ・悪い実例としてAndroidが挙げられる。 Contextという上位の型を引数にとるのに、実際にはActivityというそれより下位の 型を渡さないと、実行時にエラーが発生してしまうという罠のようなメソッドがある。 さらに性質が悪いことに、その旨についてどこにも書かれていない。 50 引数が多いメソッドはオーバーロードを検討しよう ・ユーティリティクラスやライブラリを開発するときにありがちだが、細かい指定がで きるようにすると、メソッドの引数の数がやたらと多くなってくることがある。 そうなると、呼び出し側としては引数に何を渡せば良いのかわかりづらくなるし、毎 回ズラズラ引数を渡すのも大変なので、使い勝手が悪くなってしまう。 ・そういう場合はメソッドをオーバーロードして、標準的な使い方のために引数の数 を減らしたタイプのメソッドも用意すると良い。 そうすることで、標準的な使い方をしたい場合は簡単に呼び出すことができ、かつ 細かい指定もできるという使い勝手の良いメソッドになる。 ・この方法は、「必要のない部分の引数にはnullを指定すること」といった指定が多 いメソッドで特に効果がある。 ・もちろん引数を減らしたタイプのメソッドでは、同じ処理をコピペして実装するような 馬鹿なことはせず、足りない引数を標準的な値で補完して引数の多いタイプのメ ソッドを呼び出すようなスマートな実装にすること。 51 引数が多いメソッドはオーバーロードを検討しよう 【実装例】 効果的なオーバーロードのクラスの例 /** * オーバーロードのサンプルクラス。 */ public class OverloadSample { //========================================================== //メソッド //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //クラスメソッド /** * 細かい指定で手紙っぽい文章を出力します。 * * @param header 頭語 * @param body 本文 * @param footer 結語 */ public static void printMail(String header, String body, String footer) { //頭語、本文、結語を順番に出力 System.out.println(header); System.out.println(body); System.out.println(footer); } //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //コンストラクタ /** * コンストラクタです。 * (クラスメソッドのみのユーティリティークラスなので、private にしてインスタンスを作成できないようにしています。) */ private OverloadSample() { //特に何も行わない } /** * 細かいことは抜きにして、本文の指定のみで手紙っぽい文章を出 力します。 * * @param body 本文 */ public static void printMail(String body) { //頭語に「前略」、結語に「早々」を指定して、細かい指定が可 能なタイプのメソッドを呼び出す printMail("前略", body, "早々"); } } 52 引数が多いメソッドはオーバーロードを検討しよう 【実装例】呼び出し例、実行結果 /** * オーバーロードのサンプルを呼び出すクラス。 */ public class OverloadSampleTest { public static void main(String[] args) { //細かい指定ができるタイプのメソッドの呼び出し System.out.println("== うるさい人向けなので、細かく指定し よう =="); OverloadSample.printMail("拝啓", "平素はなんやかんや", "敬 具"); (実行結果) == うるさい人向けなので、細かく指定しよう == 拝啓 ←細かく指定した 平素はなんやかんや ←とおりに 敬具 ←なっている == 友達なので本文だけでいいや == 前略 ←自動的に標準の値が使われている どう、元気? 早々 ←自動的に標準の値が使われている //空行出力 System.out.println(); //引数が少なくて済むタイプのメソッドの呼び出し System.out.println("== 友達なので本文だけ指定すればいいや =="); OverloadSample.printMail("どう、元気?"); } } 53 super.xxx()の呼び出しを強要するようなメソッドはやめよう ・ここで言いたいのは、オーバーライドを前提としているメソッドにも関わらず、オー バーライドの実装の中でsuper.xxx()という風にしてスーパークラスの同名のメソッ ドを先に呼び出しておかないと、正しく処理が行われないようなクラス設計はよろ しくないということ。 ・AndroidのActivityクラスのonXXX()系のメソッドなど、一部の標準クラスにもこの忌 むべき構造が残っている。 ・メソッドの完全なオーバーライド(中身の全上書き)をさせたくないのであれば、final にしてオーバーライド禁止にするか、上書きしていい箇所だけサブメソッドに切り 出しておくべきである。 ・文章で違いを説明するのは難しいが、普通のメソッドをサブクラスで機能拡張のた めにオーバーライドする際に、 super.xxx()を呼び出してデフォルトの機能を呼び 出した後に、追加の機能を実装する行為まで非難しているわけではない。 こちらは、差分の機能だけを追加実装する有効な手法といえる。 54 super.xxx()の呼び出しを強要するようなメソッドはやめよう ★ホワイトボックスの継承・ブラックボックスの継承 ・ホワイトボックスというのは、ソースも一緒に提供する形態のこと。 この場合、元々スーパークラスで何をやっているかを理解した上でオーバーライド するということになるので、superを呼ぶかどうかもオーバーライドする側で判断 することができる。 それでおかしくなった場合は、オーバーライドした側の自己責任。 ・一方、ブラックボックスというのは、ソースは無しでjarファイルなどでクラスファイル だけを提供する形態のこと。 そのため、中で具体的にどのような処理をやっているかは、Javadocなどのドキュ メントでしか判断できない状態になっている。 こんな状態では、オーバーライドする側でsuperを呼ぶべきかどうか判断できない ので、superの呼び出しを強要するような作りにしてはいけない。 おかしくなった場合は、スーパークラスを提供した側の責任の方が重い。 55 一度publicにしたものは基本的にインターフェースを変更しない ・クラスやメソッドをpublicにするということは、「外部からいつでも自由に使っていい ですよ」と宣言することを意味する。 ・一度でもpublicにしたものを消したり、インターフェースを変更することによりコンパ イルエラーが発生したら、これはクラス設計者が全面的に悪いと言える。 そのため、何をpublicにするのかは最初に慎重に考える必要がある。 ・プロジェクト内でなら、インターフェースを変更することをメンバーに周知したり、最 悪自分で使用箇所を直していけば何とかなるが、第三者に提供するライブラリで はそうはいかないので、特に慎重になる必要がある。 ・クラスやメソッド自体は残しておいてdeprecatedにするのは、突然黙ってインター フェースを変更するよりははるかにマシと言えるが、それでもあまり良くはない。 特にinterfaceのメソッドをdeprecatedにされると、実装しないとコンパイルエラーに なってしまうし、かといって実装しても警告が表示されてしまうので「じゃあ、一体 どうすればいいんだよ」と苦情の一つも言いたくなってくる。 56 定数インターフェースをimplementsするのはやめよう ・定数をインターフェースで定義して、それを使用するクラスはimplementsする手法 がなぜか一時期流行っていた。 ・こんなものはただ定数のクラス名の部分を省略して使用できるというだけなので、 大したメリットはない。 むしろ、どこで定義している定数かパッと見わかりにくくなるので、プログラムを解 析しづらくなるというデメリットの方が大きい。 ・定数単位のimportも同様で、かえってわかりにくくなるのでおすすめできない。 ・定数をクラスではなくインターフェースで定義するということ自体は、良い習慣とい える。 定数定義以外の余計な機能が盛り込まれる余地がなくなる上に、誤って無駄なイ ンスタンスが生成されることもなくなる。 57 クラスやメソッドをfinalにする? ・クラス宣言にfinalをつけると、継承できないクラスになる。 ・メソッド宣言にfinalをつけると、サブクラスでオーバーライドできないメソッドになる。 ・どうしても継承してほしくないときにはfinalをつけた方が良いが、個人的にはできる だけつけずにおいて、後のプログラマーの裁量に任せた方が良いと思っている。 私の経験から言わせてもらうと、ソースがあれば改変することもできるのだが、ク ラスしか提供されない場合、finalのせいでクラスやメソッドのちょっとした挙動を 変更できなくて悔しい思いをしたことが何度もある。 無理やり継承したりオーバーライドして動作がおかしくなったとしても、それはそん なことをあえてしたプログラマーが悪いので、正直自己責任の範疇だと思う。 ・コンストラクタをprivateにした場合、継承したクラスでは親のコンストラクタが呼び 出せなくなりコンパイルエラーが発生するので、実質クラス宣言にfinalをつけた 場合と同様に継承できないクラスになる。 そういうわけで、Singletonクラスを継承することはできない。 58 protectedとprivateの使い分け ・プロジェクトで量産対象となる末端のクラスは、それ以上継承されることがまずな いので、基本的に内部処理用のフィールドやメソッドはprivateでいい。 ・使い分けが問題になりやすいのが、共通系のクラスやベースクラス、ライブラリや フレームワークで作成するクラス。 こういったクラスは、たとえ作成者はそのつもりがなくとも、使用者側では継承した くなってくることが多いので、継承されるケースも想定しておいた方が良い。 ・フィールドについては、サブクラスから参照できないと困ることが多いので、基本的 にprotectedにしておいた方がいい。 よほど触られたくない場合や、ログ出力用のオブジェクトなど一世代限りの使い捨 てのオブジェクトの場合のみprivateにする。 ・メソッドに関してはfinalにするかどうかと同じ理屈で、後のプログラマーの裁量に任 せたい場合は、たとえ内部処理用のメソッドでもprotectedにしておいた方が良い。 59 パッケージの分け方 ・パッケージの分け方もクラス設計と同じように、明確な正解はない。 個人の好みや感性に大きく左右される。 ・層でクラスを分類している場合は、基本的に層と同じ名前のパッケージを作って、 分類していくと良い。 フレームワークによっては、この分け方が推奨を通り越して強制されることもある。 ・大規模なプロジェクトにおいては、単純に層で分けるだけだと、1つのパッケージに 同じ種類のクラスが大量に置かれて不都合が生じるケースも出てくる。 そういう場合は、まず機能ごとのパッケージを作って、その配下にそれぞれの層に 該当するパッケージを作って分類すると良い。 ただし、フレームワークによってはこの手法は使えないこともあるので注意。 ・共通系のクラスはcommonといったパッケージを作って、そこに置いていくと良い。 クラス数が多くなりすぎるようであれば、さらに用途別のサブパッケージを作って分 類すると良い。 60 パッケージスコープの使い方 ・パッケージスコープというと、publicやprivateをつけ忘れた素人が意図せず作って しまうものというあまりよろしくないイメージがあるが、それなりに使い道はある。 普段のプロジェクトではあまり使うことはないかもしれないが、プロジェクトの共通 部分や、ライブラリなどを作っている場合には活躍することがある。 ・パッケージ内だけで使用する内部処理用のクラスは、クラス自体の定義をパッ ケージスコープにしておくと良い。 一方、フィールドやメソッドのレベルでパッケージスコープにすることはあまりない。 ・わざとパッケージスコープにするときは、「内部処理用のクラスなので、あえてパッ ケージスコープで宣言しています」といったコメントを添えておくといい。 そうしておかないと、後で事情を知らない人が見た場合、単純にpublicのつけ忘れ だと誤解される可能性がある。 61 内部クラスの使い方 ・内部クラスにはpublicなものとprivateなものの2種類がある。 そのうち、publicなものは呼び出す側の記述がトリッキーになるので、使わない方 が良い。 一応、protectedやパッケージスコープの内部クラスもあるにはあるが、まず使うこ しはないので気にしなくていい。 ・さらに詳しく言うと、staticな内部クラスとそうでない内部クラスがある。 staticな内部クラスの場合は、外側のクラスの通常のフィールドやメソッドにはアク セスできず、staticがついているフィールドやメソッドのみにアクセスできるという 制限がかかる。 一見デメリットであるだけで使い道が無いようだが、意図せぬメモリリークが起こり にくくなるというメリットもある。 これ以上の詳しいことは、Javaの言語仕様でも参照すること。 62 内部クラスの使い方 ・パッケージスコープにする場合と同様の理屈で、特定のクラス内でしか使わないク ラスはprivateな内部クラスにしておくと良い。 ・イベントリスナーを設定する場合や、ちょっとした処理を別スレッドで動かしたい場 合も内部クラスを使うことになる。 この場合の内部クラスは、メソッドの内部にインラインで記述する無名インナークラ ス(匿名クラス)にすることが多い。 ・無名インナークラスにメソッドの引数や、メソッド内部のローカル変数の値を渡した い場合には、変数にfinalをつけておく必要があるので注意すること。 なお、外側のクラスのフィールドの値はfinalをつけていなくても普通に渡せる。 ・無名インナークラスは手っ取り早く使えるというメリットもあるが、コードが使い捨て となるデメリットも併せ持っている。 また、Javaの中でもやや特殊な扱いで、構文も変態的であまりよろしくないので、 自信が無ければ使わずにおいて、普通のクラスとして定義した方が良い。 63 内部クラスの使い方 【実装例】 privateな内部クラスの例 /** * privateな内部クラスの使い方のサンプルクラス。 */ public class PrivateInnerClassSample { //========================================================== //フィールド //========================================================== //内部クラス /** * 別スレッドで処理を行うための内部処理用のRunnableクラス。 */ private class InnerRunnable implements Runnable { //======================================================== //フィールド /** * 文字列 */ //(フィールドなのでfinalでなくても、内部クラスからも普通に参照 できます) private String fieldStr = "フィールドですよ"; /** * 数値 */ private final int num; //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //通常のメソッド //======================================================== //メソッド /** * 別スレッドで処理を行います。 * * @param num 数値 */ public void executeByThread(int num) { //Threadクラスのインスタンスを生成 //(中身には別スレッドで処理を行うための内部処理用の Runnableクラスのインスタンスを指定する) Thread thread = new Thread(new InnerRunnable(num)); //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //コンストラクタ /** * コンストラクタです。 * * @param num 数値 */ public InnerRunnable(int num) { //別スレッドを起動する thread.start(); } →次のページに続く 64 内部クラスの使い方 【実装例】 privateな内部クラスの例(続き) //==================================================== //引数の値をフィールドに設定 //数値 this.num = num; } //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //インターフェースの実装 /** * 別スレッドで実行するためのメソッドです。<br> */ @Override public void run() { //変数の使い方を説明したいだけなので、ここの処理内容に 深い意味はないです System.out.println("外側のクラスのフィールドの値:" + PrivateInnerClassSample.this.fieldStr); System.out.println("内部クラスのフィールドの値:" + this.num); } } } 65 内部クラスの使い方 【実装例】 無名インナークラスの例 /** * 無名インナークラスの使い方のサンプルクラス。 */ public class AnonymousInnerClassSample { //========================================================== //フィールド //Threadクラスのインスタンスを生成 //(中身にはインラインで定義したRunnableの無名インナークラ スのインスタンスを指定する) Thread thread = new Thread(new Runnable() { @Override public void run() { //変数の使い方を説明したいだけなので、ここの処理内 容に深い意味はないです System.out.println("外側のクラスのフィールドの値:" + AnonymousInnerClassSample.this.fieldStr); System.out.println("メソッドの引数の値:" + num); System.out.println("メソッド内部のローカル変数の 値:" + localNum); } }); /** * 文字列 */ //(フィールドなのでfinalでなくても、内部クラスからも普通に参照 できます) private String fieldStr = "フィールドですよ"; //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //通常のメソッド //別スレッドを起動する thread.start(); /** * 別スレッドで処理を行います。 * * @param num 数値 */ //(引数にfinalをつけているのは、無名インナークラスから参照でき るようにするため) public void executeByThread(final int num) { //メソッド内部のローカル変数 //(これもfinalをつけないと、無名インナークラスから参照でき ない) final int localNum = 98765; } } 66 作業ファイルが衝突しにくいクラス構成にする ・実プロジェクトにおいては、複数の人間が同時に作業することになるので、作業 ファイルが衝突しにくいクラス構成になるように配慮する必要がある。 ・ベースクラスやEntityといった共通系のクラスやユーティリティクラスは、大勢で修 正すると滅茶苦茶になりがちなので、触っていいメンバーを限定した方が良い。 共通担当の1人だけが触れるようにするのが良いが、不慮の事故や契約切れでプ ロジェクトの途中でいなくなる可能性もあるので、もう一人ぐらいはいざというとき にソースを触れるバックアップメンバーを用意しておくのが理想的。 ・DAOは特に他の人と作業ファイルが衝突しやすい層なので要注意。 ・クラス設計から少し話題はそれるが、SubversionやCVS、Gitといったバージョン管 理システムを使うと、他の人と作業ファイルが衝突しても、事故が起きにくくなる。 世間的には、ロック無しで運用して衝突が発生するとマージする使い方が主流の ようだが、私としてはより安全なロックをかける運用をおすすめしたい。 ただし、ロックをしたまま放ったらかしにする不届き者には注意が必要。 67 ライブラリ・フレームワークを作成する上での心得 ・設計思想を統一させるため、できるだけ一人で作る方が良い。 ・あくまでも開発を楽にするためのものだということを忘れないこと。 使用者に「こんな変なの使わせずに普通に作らせろよ、ったく」と思われてしまうよ うなものを作ってはいけない。 ・クラス設計に限ったことではないが、プロジェクトの共通部分やライブラリ・フレーム ワークは、あくまでも裏方だということを忘れないで欲しい。 滅私奉公とまではいかずとも、あまり表にでないで他の開発者の補助に徹する という姿勢が重要。 ・何でもかんでも機能を詰め込むよりも、ワンポイントに絞ったものの方が使い勝手 がいいことが多い。 ・何もないところからいきなり作り始めるのではなく、実際のプロジェクトの中で作っ て便利だと思ったものを洗練・汎用化してライブラリ化する方がうまくいく。 68 結局のところ良いクラス設計とは何か? ・自分だけが使うものであればどれだけ趣味に走ろうが誰も文句を言わないが、他 の人にも使ってもらうとなればそうはいかない。 そのため、私の考える良いクラス設計とは、「他の人に正しく、スムーズに使っても らえること」である。 ・他の人に使ってもらうためには、単に正しく動くということだけでは足りない。 以下のような点も重視する必要がある。 1. 使いやすい 2. 使うための準備が面倒くさくない 3. 機能の存在をわかりやすくする 4. 間違った使い方をされないようにする 5. エラーが発生した場合に原因が特定しやすいようにする 6. 余計なことをしない 7. 改造しやすい 8. メリットだけでなくデメリットも明確にする 69 結局のところ良いクラス設計とは何か? 1.使いやすい ・当たり前すぎてあまり言うことはないが、徹底するのはなかなか難しい。 ・「他人にとってはどうなのか?」という視点を持ち続けることが重要。 機能や内部の挙動を熟知している自分を基準に考えないこと。 ・クラスの場合は必要に応じて標準的なインターフェースを実装するようにして おくと、用途が広がり便利になる。 例えば、データを格納するクラスであれば、独自の規格にするよりも、Listや Mapインターフェースを実装することにより、互換性を持たせることができる。 ・やたらと色んな種類の例外を投げるメソッドは、本来は正しいあり方なのだろ うが、使う側としてはうっとうしくてたまらない。 RuntimeException系の例外だけを投げるメソッドの方が、いちいちtry-catchで 例外処理をしなくても済むので、結局は使い勝手がいい。 70 結局のところ良いクラス設計とは何か? 2.使うための準備が面倒くさくない ・下準備がやたらと大変なものは、それだけで使う気を無くす。 ・専用の設定ファイルが大量に必要となるものは特に嫌われる。 (というか私が嫌い) ・独自のルールや文法を強要するものに関しても同様。 ・リフレクションやアノテーションをうまく活用することにより、設定ファイルへの 依存を減らすことができる。 71 結局のところ良いクラス設計とは何か? 3.機能の存在をわかりやすくする ・どれだけ良い機能があっても、使用者に存在自体を気付いてもらえなければ、 宝の持ち腐れになってしまう。 「そんな機能があったのなら先に言ってよ」と言われるようでは負け。 ・あらかじめ用意している機能が使われず、個々人に独自に実装されてしまうと、 統制が取れなくなり、プロジェクトの崩壊にもつながりかねない。 ・機能に気付いてもらうにはドキュメントを整備するのが一見良さそうだが、どう せみんなまともに見やしないので、あまり期待してはいけない。 ・クラス名やメソッド名を工夫したり、既存のよくある仕組みに似せるなどして、 殊更に主張しなくても自然と気付いてもらえるようにするのが理想。 ・プロトタイプやサンプルソースで、機能をさりげなく網羅するのも効果的。 72 結局のところ良いクラス設計とは何か? 4.間違った使い方をされないようにする ・直接呼ばれたくないクラスやメソッドは隠して、物理的に呼べなくする。 ・誤解を招くようなクラス名やメソッド名は避ける。 ・渡していい引数や、どういう戻り値を返すのかを明確にする。 特にリストや配列を戻り値として返す場合は、nullを返すのか、空を返すのか をはっきりさせた方が良い。 ・設計思想を一貫させる。 場所によってコロコロとやり方が変わると、使う側も混乱してしまい、間違いが 起きやすくなる。 悪い実例としては、AndroidでDBアクセスする際に使用するSQLiteStatement クラスとCursorクラスの関係が挙げられる。 SQLiteStatementクラスではインデックスが1から開始されるのに対して、 Cursorクラスでは0から開始されるのだが、ここまで悪質だと、もはや使用者 を罠にはめようとしているとしか思えない。 73 結局のところ良いクラス設計とは何か? 5.エラーが発生した場合に原因が特定しやすいようにする ・例外をこっそりともみ消すようなことは絶対にしてはいけない。 最低でもログに出力するようにすること。 ・エラー発生時に独自の例外を投げること自体は悪くないやり方だが、その際 は必ず発生元の例外をラッピングした例外を投げるようにすること。 元の例外の情報が失われてしまうと、原因が特定できなくなってしまう。 ・エラー発生時には単に例外を投げるだけでなく、どうしてエラーになったのか という理由もメッセージに添えておくと親切。 ・ログ出力も原因の特定には大いに役立つ。 重要な部分では「★ ★」や「#####」といった目立ちやすい文字をログに含め るのも効果的。 ただし、あまりにも大量にログを出力しすぎると、必要なログが埋もれてしまう のでやり過ぎには注意。 74 結局のところ良いクラス設計とは何か? 6.余計なことをしない ・1つ前のエラーの話でも触れている例外のもみ消しは、余計なことの筆頭。 ・必要とする機能以外の余計な処理が混ざっていると、使えるものも使えなく なってしまう。 ・個々の機能はできるだけシンプルにしておき、複雑な機能はその組み合わせ で実現するような仕組みにしておくと、使用者側としては必要な機能だけを 抜き出しやすくなるので都合が良い。 ・普通のやり方であればできることをかえってできなくするような、機能を制限す る仕組みも良くない。 ・コネクションや入出力ストリームを扱う場合などは、勝手に閉じたりしないこと。 閉じるのであれば必ずその旨を明記しておくこと。 75 結局のところ良いクラス設計とは何か? 7.改造しやすい ・誰にとっても万能なものを作るのはなかなか難しく、使用者側にとっては「あと ちょっとで何とかなるのに」となるケースも多い。 ・そんなとき、改造できる手段さえ提供されていれば、使用者側も素人ではなく プログラマーなので、自分で微調整して使ってくれることがある。 ・改造のしやすさは、自分で後から発展させていくときにも役立つ。 ・具体的に言えば、privateではなくprotectedにしておく、クラスやメソッドにあえ てfinalはつけないでおく、といったテクニックがこれに該当する。 ・プラグイン形式など、後から作られたクラスを動的に取り込める仕組みになっ ていると、より発展性が高まる。 76 結局のところ良いクラス設計とは何か? 8.メリットだけでなくデメリットも明確にする ・これは私の持論であり、クラス設計だけに限った話ではない。 世の中のどんなものにも必ずメリットとデメリットは存在する。 ・機能の多さとシンプルさ、汎用性とパフォーマンス、設計の堅固さと柔軟性な どは両立しづらいので、どちらかを犠牲にすることになりやすい。 ・デメリットを隠して、メリットだけを主張するようなやり方は誠実ではないし、 誤った使い方や過剰な期待を招き、結果としてトラブルの元になる。 ・デメリットも包み隠さず明確になっていると、使用者側の安心感にもつながる。 ・デメリットを考えることにより、おのずと弱点が見えてくるので、それを改善して いくことにより良い出来栄えにもつながる。 77 最後に ■最初にも言ったように、クラス設計に絶対的な正解など無い。 自分にとっては納得のいくクラス設計でも、他人から見ればよろしくないこともある。 また、時間が経つと陳腐化したり、修正が入って当初の思惑が崩れ去ってしまうこ ともよくある。 あえて言えば、「臨機応変」が極意だが、そんなもの誰にも究められない。 ■クラス設計の練習をするには、新しいものを作るよりも、既存のものを一から再設 計して作りなおすのがいい。 この際、必ず机上の設計だけで済まさずに、コーディングまで行って実際に動作 する段階にまで仕上げること。 コーディングがスムーズに行なえないようならば、まだまだ実践的なクラス設計 になっているとはいえないので、さらなる修行が必要。 ■自分の作ったクラス設計の美しさに惚れ惚れできるようになると、何物にも代えが たい喜びにひたることができるが、病みつきになりすぎないように注意。 それでは、楽しいクラス設計を! 78
© Copyright 2024 ExpyDoc