オブジェクト指向エクササイズ(英 : Object Calisthenics)という訓練法がある。The ThoughtWorks Anthologyにて、Jeff Bayさんが提案したエクササイズだ。このトレーニングは実際のところ単純に難しい。ルール自体がきつめに設定されているのが原因だが、2005年に発表されたこともあり、残念ながら現代の利用に則さない部分があるのも問題である。そこで、よりモダンな環境に対応しつつ、誰でもほどほどの難易度でできて、成長できそうなハードル設定に変えてみることにした。
なお、使用言語は筆者が業務で使っているC#とする。大抵のことは原著で使われたJavaなどの他の言語にも応用できるだろう。
オブジェクト指向エクササイズ2.0
C#の機能に特化しているため、お使いの言語に存在しないものは無視すること。
- 1つのメソッドにつきインデントは1段階までにすること
1.1.try
句、using
ステートメントを使う場合、そのインデントを起点として考えること
1.2. 特段の理由が無い限りusing
宣言を利用すること -
else
句を利用しないこと - すべての基本型(値型と文字型、文字列型)をラップすること
- 直接のインスタンス変数クラスのメソッドのみ読み出すこと
- 名前は省略せず、使う単語は2つまで、かつ20文字以内で終わるようにすること
5.1.var
は使わないこと
5.2.new()
は使用しても構わない - すべてのエンティティを小さくすること
6.1. 10ファイルを超えるフォルダを作らないこと。ただしフォルダ内のフォルダ数は特に制限しない
6.2. クラスの行数が50100以上になった場合、クラスの機能分割を検討すること - 1つのクラスにつきインスタンス変数は2つまでにすること
7.1. ただしDBの1行などを表すデータクラスを除く - ファーストクラスコレクションを使用すること
8.1. LINQなどのリスト包括表記機能はファーストクラスコレクション内部のみで使うこと - インスタンスメソッドは基本型を引数に取らないこと
9.1. Setterは絶対に使用しないこと
9.2. 余裕があればGetterやプロパティを使用しないこと - ファクトリメソッド・
Main
・演算子オーバーロード以外のstatic
メソッドは作らないこと - 匿名型は使用しないこと
- 拡張メソッドを作成しないこと
サブ項目を増やすことで、特に6番は具体的になった。
原著の各事項を検討する
以下の引用はすべて日本語版を基とする。ただ項目名を見ているだけではジェフさんの真意が伝わらない(そのために誤った解釈がされかねない項目もいくつかある)ので、要点にまとめておいた。
また、訳注で触れられていた没案「ファクトリメソッド以外のスタティックメソッドは作らないこと」も復活した。static
を避けることもオブジェクト指向の理解につながるからだ。
なお、原著の当該(5)章では継承やオーバーロード、インターフェイスに関する言及はほぼ無い。ジェフさんの現状も不明なのでどう考えているかは想像するしかないが、Javaを使っていてList
インターフェイスにしか触れていないということは、自作インターフェイスやインターフェイス実装クラスは不要と考えているようだ…少なくとも「必然的にオブジェクト指向にする」には。
1つのメソッドにつきインデントは1段階までにすること
要点
- 「厳密に1つの仕事を行うクラスで、厳密に一つの仕事を行うメソッドを書く」
- メソッドの抽出を繰り返し適用する
評価
このルールを守ろうとするとき障害になるのはtry
句である。ジェフさんはtry
句内部の処理をいちいちメソッドを切っていたのだろうか。また、C# 7以前だとusing
ステートメントでインデントを消費するのでインデントの天井に届いてしまう。この2つは例外とした方がやりやすい。早期にdispose
するためにはクラスをnew
して、using
句内で設定しなければならない(2回初期化するので効率が悪くなるし、コンストラクタを余分に用意する必要もある)ので、使えるならusing
宣言にしよう。
else
句を利用しないこと
要点
こういうものを使うとelse
を使わなくてもOKだよ。
- メソッドからの早期リターン
- Strategyパターン
- ガード節
- ポリモフィズム
- NullObjectパターン
評価
これは特に変える必要はないだろう。このまま頑張ろう!
すべてのプリミティブ型と文字列型をラップすること
要点
「時間」オブジェクトをパラメータに取るメソッドに「年」オブジェクトを渡すことは不可能です。
評価
これもそのままで行った方が勉強になるので採用。今のところ順調である(フラグ)。
一行につきドットは1つにすること
要点
ドットがつながっているということは、オブジェクトが他のオブジェクトの中を掘り進んでいるということです。
評価
初めて明らかに赤信号なルールが出てきた。この指摘はFluent Interfaceの登場で的外れになってしまった。よりによって、この用語が生まれたのがオブジェクト指向エクササイズが誕生した2005年だ1。これを順守してFluent Interfaceを使うメソッドでいちいち変数を切るか、メソッドを例外(スローするやつではなく、メソッドチェインを有効にするという意味)にするかはあなたの匙加減だ。
自分としてはデバッグが楽になるので変数を使い捨てる方がよいと思うが、「すべてのエンティティを小さくすること」と相性が悪いため難しい。だがこの二者択一はジェフさんの考えるところとずれているのが上から読み取れるだろう。ここは要点を取り入れる形に変更した。
名前を省略しないこと
要点
省略は紛らわしくなりますし、もっと重大な問題を隠してしまいがちです。
なぜ省略したくなるのか考えてください。同じ単語を何度も何度も入力しているせいではありませんか。(中略)もしくは、メソッド名が長くなってきているせいではありませんか。
(中略)
クラスやメソッドの名前は、1つか2つの単語だけを使うように気を付け、文脈が重複する名前は避けてください。
評価
例えばStringBuilder
インスタンスをsb
と命名することがあるので、それは避けるというのは当然のことだ。しかし、このルールを守るのは最も簡単だ―IDEが同じ単語も長い名前も自動入力してくれるからだ。とどのつまり、技術革新のせいでこのルールの役目は終わってしまっている(当時のIDEがどこまでの力があったかわからないが、自分がEclipseを使っていた2014年ごろにはもうそういう機能があったはず)。しかし、最後の文章はいまだに効力を発揮し続けているのでこれは汲み取りたいということでルールを変更した。
エクササイズということを考慮し、型名の省略を制限することとした。これもエクササイズ発表当時に存在しないことだった。型宣言を省かないというのは当然だが、扱いに困るのはコンストラクタの型省略である。
SomeClass someClass = new(); // new SomeClass() と書くのと同義
IList<string> strings = new List<string>(); // インタフェイスを実装でインスタンス化するのはよくあること。
Base base = new Derived(); // 派生クラスでのインスタンス化。抽象クラス変数定義に必要。
これに関しては「文脈が重複する」ものと考え、new()
による省略を認めるものとした。var
に関しては省略そのものであるので、ここでは認めないことにする。
すべてのエンティティを小さくすること
要点
50行を超えるクラス、10ファイルを超えるパッケージを作らないという意味です。
評価
具体的な基準があるにもかかわらず、ルールに盛り込まれていないのは問題なので、サブ項目とした。しかし、初心者は行数制限を律義に守るのは疲れるのでやめた方がいいと思う。だからと言って1000行のクラスを作るのはやり過ぎなので、努力義務として項目を分けることにした。C#では演算子オーバーロードや、インターフェイス実装、LINQ to EntitiesのSelectの指定やXMLコメントなどを考えると、50行はかなりかつかつである。そのため原著の倍である100行を目安にすることにした。原著にはプログラムコメントは当然ないが、当時はJavaDocは存在していたのだろうか? WikiPediaにも歴史情報が載っていないので覚えている方は教えてください。
この行数制限を考えるに、継承は使っていかないと厳しかろう(先程もお伝えした通り原著では継承を利用していないが)。しかし行数制限のために抽象クラスを作り続けるのはおかしな話で元も子もない。継承のデメリットは無視できないので、それを実際に学びながらプログラミングをしてみてはいかがだろうか。この辺りは個人的な目標設定と兼ね合いになるので、明文化は避けた。
また、C#ではJavaにおけるパッケージに相当する名前空間の縛りも緩いので、自由に設定しても問題無いように読み替えよう。インターフェイスを作る場合は10ファイルはきついように思うが、インターフェイス用のサブフォルダを作るということも考えられる。この辺りは柔軟にやろう。
1つのクラスにつきインスタンス変数は2つまでにすること
要点
ほとんどのクラスはただ1つの状態変数を扱うことだけに責任を持つべきですが、2つの変数を必要とするクラスも少しはあります。
(中略)
このエクササイズの9つのルールに従ってコーディングしていると、2種類のクラスがあることに気がつくでしょう。一方は、1つのインスタンス変数の状態を管理するクラス、もう一方は、2つの独立した変数を調整するクラスです。
(強調筆者)
評価
冒頭の発言を大事にするなら、「可能な限りインスタンス変数は1つで作る。ダメな場合のみもう1つだけ増やす」と考えよう。また、後者の発言はこのエクササイズの重要なヒントになる。何をどうすればいいかわからないときはここに立ち戻るのがよい。原則を「可能な限り…」と言い換えることも考えたが、やはり原点を大事にしたいと考えて据え置きにした。
ただし、POJO(C#ならPOCO)やrecord
型など、まとまったデータであることが重要であるクラスは例外だと考える。そうすると行数制限が重たくなるので、エクササイズ自体がデータベースの読み取りなどを考えてはいないように思われる。
ファーストクラスコレクションを使用すること
要点
コレクションを持つクラスには、他のメンバ変数を持たせないようにしてください。
(中略)
コレクションは多くの振る舞いを持っていますが、後任のプログラマや保守担当者にプログラム上の意図やヒントをほとんど示せないのです。
評価
この項目もエクササイズが生み出された後に風向きが変わった。LINQをはじめとしたリスト包括表記の台頭2だ。それまではただのデータ集合だったList<T>
も、その中身を基に処理を書けるようになった。これらをフル活用するには直接コレクションを扱うのが簡単だ。また、LINQはFluent Interfaceで成り立っているので「一行につきドットは1つにすること」を守株すると途端に難しくなる。
しかし、ファーストクラスコレクションの長所が無くなったわけではない。処理の影響範囲がクラス内に限定されるため安全性が非常に高くなるし、余計なアクセスメソッドを準備しなくて済むというのが利点だ。オブジェクト指向らしい考え方でもあるので、ファーストクラスコレクションの利用は継続することにする。自分が作るならファーストクラスコレクションにも別の変数を設けたりするだろうが、それは匙加減だろう。
このエクササイズにおけるLINQの扱いには2つの方法が考えられる。
まずは、IEnumerable<T>
をファーストクラスコレクションに設定することだ。ファーストクラスコレクションのカスタマイズ性とLINQの利便性を両立できるため実践的だ。また、クラスを使うものにこれはT
型を多数保持するためのクラスであることを効率的に伝えることができるという利点もある。IList<T>
ならデータを順繰り検索・追加・削除する、ISet<T>
なら中身は一意になるという具合だ。従って「コレクションは…プログラム上の意図やヒントをほとんど示せない」というジェフさんの指摘には首肯できない。
もう1つは、LINQの使用をファーストクラスコレクション内部にとどめることだ。LINQの特性である汎用性が失われてしまうという短所はあるし、現場ではIEnumerable<T>
インターフェイスをファーストクラスコレクションに実装する方が採用されるだろう。しかし、逆張りかつエクササイズとしてはより考えさせられるだろうし、よりオブジェクト指向の考え方に近いこちらをルールとすることにした。
Getter、Setter、プロパティを使用しないこと
要点
振る舞いがその場で簡単に値を求められるようになっていると、その振る舞いはインスタンス変数の後をついてきません。
評価
これはかなり意見が分かれるところだろう。セッターを使わないことは当然賛成できる。勝手口だからだ。しかしゲッターは有用だし、基本型を使わないとままならない場面は結構ある。Enumerable.Sum<T>
メソッドは組み込み数値型の使用を前提としており、カスタム型では恩恵に与れない。
上級者を目指すならこの原則を律義に守り、クラスで完結させることを追求すべきだが、これからオブジェクト指向を学ぼうとする者には激務だ。そこで、ここは上記の通り差し替えた。つまりコンストラクタの引数での使用は当然許可されるし、オブジェクト指向に慣れるまではメソッド内部でゲッター経由で基本型を使うことは目をつむりたい。
その他検討
匿名型をどうすべきか
オブジェクト指向エクササイズの目的を考えると、匿名型は使用すべきでない。どうせエクササイズなのだから、使い捨てになるとしても、LINQクエリはいちいち新しい型を利用すべきだ。
演算子オーバーロードはどうすべきか
C#はJavaと違い、演算子をオーバーロードできる。a.Add(b)
よりもa + b
とした方が分かりやすいのは確定的に明らかだし、文字数も少なくなる。これは必要とあらば積極的に活用すべきだ。
拡張メソッドの問題点
拡張メソッドを使えば、基本型や外部APIに疑似的に機能を追加できる。例えばstring.IsNullOrEmpty(value)
をvalue.IsNullOrEmpty()
と書けるようにラップすることが可能だ(調べればすぐに分かるはずなので詳細は割愛)。また、インターフェースをthis
引数にすれば多重継承っぽいことも可能だ。しかし拡張メソッドを持てるのはstatic
クラスのみという、オブジェクト指向らしからぬ欠点がある。当然10番のルール違反でもあるし、どのクラスにどんな機能を持たせるかというオブジェクト指向エクササイズの本旨とずれてしまう。そのため、涙を呑んで拡張メソッドの使用は諦めよう。現実的な課題解決で存分利用してください。
終わりに
項目を細分化したり、C#に特有な事項などを追加したために、元のエクササイズルールの倍の項目数となってしまったが、概ね現在のC#の進化を反映したルールになっていると思う。
確かに元のエクササイズをそのまま適用するのは現在厳しいし、void
メソッドでStringBuffer
に値を注入するというメソッドの切り出し方などは疑問に思わなくないが、原著の指摘は記事の初版作成時点で17年(日本語版発行から14年)たった現在でも一読の価値がある。紙の本は新品が無いので(個人的には定価以上の中古でも買う価値は大ありだったが)この機会に冒頭のリンクからEBookを購入してみてはいかがだろうか。
修正事項
- 2023/6/5:ルールに「7.1. ただしDBの1行などを表すデータクラスを除く」を追加。「1つのメソッドにつきインデントは1段階までにすること」の記述を詳細化。
- 2023/6/21:早期disposeの説明が不十分だったので修正。「6.2.」の条件を100行に緩和。「ファーストクラスコレクションを使用すること」の評価を追記修正。