privateメソッドを作りたくなった時は存在するべきクラスを見逃している#
このページの見出しを見て「えっ?!」と思う人は多いでしょう。でも、私がマネージメントする開発では「privateメソッド禁止」は当たり前なんです。例を使った下の説明で詳しい内容は理解して欲しいのですが、要点だけを言うと、
「privateメソッドに渡すパラメータをひとかたまり(データ構造)とするクラスを見逃している。」#
ということです。顧客会社クラスで考えてみる#
商取引を扱うシステムで良く出てくる「顧客会社」クラスを使って考えてみます。![]() |
public String getTantoushaFullName() { return this.tantoushaSei + " " + this.tantoushaMei; }
次に、このクラスが仕様変更となり、「代表者姓」と「代表者名」という属性が追加されたとします。「代表者氏名を返す」というメソッドもこの時必要になったとすると、以下のようなコードを追加することでしょう。
![]() |
public String getDaihyoushaFullName() { return this.daihyoushaSei + " " + this.daihyoushaMei; }
そして次に、「いずれの氏名を返す時も、姓と名の間のスペースを2つに替えて欲しい。」という要望が挙がったとします。よく見てみると上記二つのメソッドは非常に良く似ています。「スペースを挟んで姓と名を連結する」という仕様だからです。こういう時、privateメソッドを作っておくと変更が1ヶ所で済んだはずです。次のようなコードです。
private String getFullName(String sei, String mei) { return sei + " " + mei; } public String getTantoushaFullName() { return getFullName(this.tantoushaSei, this.tantoushaMei); } public String getDaihyoushaFullName() { return getFullName(this.daihyoushaSei, this.daihyoushaMei); }
が、果たして本当にそうでしょうか?
![]() |
データ構造を見逃すな!#
上記の例で解るように、「privateメソッドを作りたくなった時はクラスを見逃している」のです。privateメソッドはクラス内の共通関数に当たるのですが、共通関数を作ろうとすれば、その関数に渡すパラメータをそもそもまず共通化する必要があります。そのパラメータをひとかたまり(データ構造)とするクラスを作るべきなのです。
この場合は、属性として「姓」と「名」を持つ「氏名クラス」を作るべきです。そしてそのクラスに委譲するのです。
![]() |
パラメータを持たないprivateメソッドはもっとだめ#
「privateメソッド禁止」の話をする際によくある反論が、「パラメータを持たないprivateメソッドだってあるじゃないか!」というものです。パラメータの無い、つまり共通関数ですらないprivateメソッドがなぜ必要なのでしょうか? 彼らの答えは「可読性をあげるため長いコードを分割するんだ。」なのですが、私に言わせると「ちゃんちゃら可笑しいw」です。
なぜならコードが長くなるのは、上記のような小物クラスを見逃しまくっていて、結果的に手続き的な記述になっているからだと断言できるからです。クラスという単位で分かれているべきものが一つになってしまっていれば、その中の記述が長くなってしまうのは当然です。
そもそも一連の処理ならばprivateメソッドなどに分割せず、処理の流れの通りに1ヶ所に書いてある方が、第三者が見た時の可読性は高いはずです。下の例を比べてみて下さい。
privateメソッドに分割した例#
public String getSomething() { doSomething1(); doSomething2(); doSomething3(); } public String getAnotherthing() { 他の処理が書いてある; } private void doSomething1() { 何かの処理1-1が書いてある; 何かの処理1-2が書いてある; } private void doSomething2() { 何かの処理2-1が書いてある; 何かの処理2-2が書いてある; } private void doSomething3() { 何かの処理3-1が書いてある; 何かの処理3-2が書いてある; }
一連の処理が1ヶ所に書いてある例#
public String getSomething () { 何かの処理1-1が書いてある; 何かの処理1-2が書いてある; 何かの処理2-1が書いてある; 何かの処理2-2が書いてある; 何かの処理3-1が書いてある; 何かの処理3-2が書いてある; } public String getAnotherthing() { 他の処理が書いてある; }
オブジェクト指向の原則を考えるとそもそもprivateメソッドは不要#
クラスとはデータ構造で説明した次の図をもう一度見て下さい。出典: What Is an Object?
![]() |
ユーティリティクラスだとどうだ?#
「『担当者氏名を返す』メソッドはprivateではなくユーティリティクラスでいいじゃないか?」と思った人もいるでしょう。次のようなコードです。
public class StringUtility { public String getFullName(String sei, String mei) { return sei + " " + mei; } } public class KokyakuKaisha { : : public String getTantoushaFullName() { return StringUtility.getFullName(this.tantoushaSei, this.tantoushaMei); } public String getDaihyoushaFullName() { return StringUtility.getFullName(this.daihyoushaSei, this.daihyoushaMei); } }
上記のStringUtility#getFullName()メソッドは単なる関数なので、姓名の連結のみに使われる保証はありません。極端な場合は次のような使われ方をするかもしれません。
public class Jimusho { private String yuubinBangou = ""; private String jyuusho = ""; : : public String getAtesaki() { return StringUtility.getFullName(this.yuubinBangou, this.jyuusho); } }
この時、システムを国際化対応する必要がもしも出てきて、「担当者ミドルネーム」「代表者ミドルネーム」という属性が追加になったとします。そして、「氏名を返すメソッドは全て『姓 ミドルネーム 名』とする(間のスペースは1つずつ)」という要件に替わったとします。
この場合の変更箇所はStringUtility.getFullName()メソッドのみではなく、それを呼び出している全メソッドが対象となります(当然ですが)。その際、「従業員クラスの宛先を返すメソッド」も該当しますが、事務所クラスは「ミドルネーム」を持たないため破綻してしまいます。別の関数を新たに作る必要が出てきます。
一方で、「氏名クラス」を利用している場合はどうでしょう?
氏名クラスを利用する場合、そのデータは「姓」と「名」として使うことでしょう。「郵便番号」と「住所」を無理矢理入れようと思えば入れられますが、上記のユーティリティクラスの使われ方ほどの可能性はありません。 なぜならば、ユーティリティクラスの目的は「処理」、つまり「1つめのパラメータと2つめのパラメータを2個のスペースを挟んで連結する」という機能を提供することですが、氏名クラスの目的は氏名となるデータを保持することだからです。誤用の可能性は低いでしょう。
ひとかたまりとなるデータ構造をクラスに閉じ込め、そのデータに関する処理も併せてその中に閉じ込めてしまうことで修正箇所を最小限にしようというオブジェクト指向の目的がこの例からも解ります。
![]() |
例外的にprivateメソッドを許す場合#
以下の場合のみprivateメソッドが必要になります。- 再帰処理を実装する場合
- 汎用的なデータ構造を持つクラスを継承せずに利用する場合
再帰処理#
再帰処理は、メソッドの中からそのメソッド自身を再度呼び出すというアルゴリズムです。これを利用する際、
- 外部から呼び出されるメソッド(再帰処理の入り口)
- 再帰的に呼び出されるメソッド(メソッド内部で何回も呼び出される)
例えば二分探索木を使う場合、走査の起点をルートノードに固定化したいのですが、その処理は外部から呼ばれたpublicなメソッドで行い、再帰的な走査はprivateで行うという使い分けをします。次のようなコードです。
/* 走査 */ public void traverse() { ascendingTraverse(this.rootNode); } /* 昇順走査 */ private void ascendingTraverse(Node current) { if (current.getLeftNode() != null) { // 再帰呼び出し ascendingTraverse(current.getLeftNode()); } currentに対する何かの処理; if (current.getRightNode() != null) { // 再帰呼び出し ascendingTraverse(current.getRightNode()); } }
汎用的なデータ構造を持つクラスを継承せずに利用する#
これは、JavaのArrayListクラスのように汎用的なデータ構造を持つクラスを継承せずに委譲の形で利用する時、その汎用的なクラスに対する特定の扱いを共通化したい場合です。public class Someclass { private String name = "XX"; private List<Integer> someList = new ArrayList<Integer>(); : : public String firstMethod() { // リストの初期化 clearList(); 何かの処理; : } public String secondMethod() { // リストの初期化 clearList(); 別の何かの処理; : } /** リストの要素をゼロで置き換える **/ private void clearList() { for (Integer el : someList) { el = new Integer(0); } }
コラム#
2000年頃の話ですが、私が初めて仕事でJavaを使ったプロジェクトにて、私は帳票用の自動集計フレームワークを設計・実装していました。そのプロジェクトの本番稼働後に仕様追加の依頼があってフレームワークのコードを直そうとしたところ、同じ修正を2ヶ所にしなければならない事が判明しました。そのフレームワークの実装に当たっては、「1ヶ所直せば皆直る」を心掛けていたのにもかかわらずです。
私は必死になって理由を考えました。そしてこのページで説明した「privateメソッドが癌だった」ことに気付いたのです。その時は新しいクラスを作ってリファクタリングし、仕様変更に無事対応できました。
オブジェクト指向の出発点は「1ヶ所直せば皆直る」です。難しいことは置いておいても、これを目指して皆さんも設計・実装してみてください。そうすれば、オブジェクト指向の核心がきっと見えてきます。
添付ファイルの追加
ログイン済のユーザのみが添付ファイルをアップロード出来ます。
添付ファイル一覧
Kind | Attachment Name | Size | Version | Date Modified | Author | Change note |
---|---|---|---|---|---|---|
png |
custocompany1.png | 3.3 kB | 1 | 25-6-2011 03:28 | ytp | |
png |
custocompany2.png | 4.1 kB | 1 | 25-6-2011 03:28 | ytp | |
png |
name.png | 8.2 kB | 4 | 31-12-2011 21:46 | ytp | |
png |
name2.png | 8.3 kB | 3 | 31-12-2011 21:43 | ytp | |
png |
supplyer.png | 3.4 kB | 1 | 25-6-2011 03:28 | ytp |
«
This particular version was published on 17-7-2011 23:59 by ytp.