読者です 読者をやめる 読者になる 読者になる

OOコード養成ギブスとやらが人気らしい。

Java

人気らしいので見てみました。

OOコード養成ギブス - rants

エクストリームプログラミング(XP)同様、ある種の正論を極論に展開するとジョークになるということ?XPはソフトウェア開発の中で、人について考えるきっかけを与えてくれたよね。これも、アンチがいればいるほど、OOP神話について考えるきっかけになるもの?

個々の要素単位で議論しにくいXPと違って、これはひとつづつ突っ込めるので、やってみよう。多くの感想が、「現実のヨゴレ仕事にはフィットしない」って感じだけど、原理的にもビミョーだし、いじりやすいね。

まず、大前提となっているのが、リソースが無尽蔵にあると仮定され、かつ、型宣言が強制され、かつ、プリミティブ型をオブジェクトと区別する言語、つまり、Javaだけについての話なんですね。これは問題ありです。タイトルは「OOコード〜」ではなく、「Javaコード〜」でないと誤解を招く。

1. インデントは1メソッド1レベルのみ。2つ以上必要ならもう1つ別のメソッドを定義してそれを呼ぶようにする。これ重要。

public void forAll(Iterable iter) throws InvalidCollectionException {
    if(iter == null) {
        throw new InvalidCollectionException();
    }
    try {
        for(; obj = iter.hasNext(); iter.next()) {
            forOne(obj); //throws ElementException
        }
    }
    catch(ElementException e) {
        throw new InvalidCollectionException();
    }
}

これを、こう書けと?

public void forAll(Iterable iter) throws InvalidCollectionException {
    if(iter == null) {
        throw new InvalidCollectionException();
    }
    try {
        forAllButNotCatchRawException(iter);
    }
    catch(ElementException e) {
        throw new InvalidCollectionException();
    }
}

private void forAllButNotCatchRawException(Iterable iter) throws ElementException {
    for(; obj = iter.hasNext(); iter.next()) {
        forOne(obj); //throws ElementException
    }
}

ButNotCatchRawExceptionってww こんなのが山ほどできるんですが、これどういうこと?

この規約の大問題は、無意味なメソッド抽出を強制されてしまうこと。本質的にやりたいことが不可分なものの場合、その意図を表現するためにひとつのメソッドに閉じ込めたほうがいい。これができないと、セットになるはずのコードが、簡単に離れた場所(別の行やオブジェクト)に移動されてしまう。メソッドの抽出よりも、記述場所の移動のほうが圧倒的に発生しやすく、アルゴリズム部品の分散(メソッドスパゲティ化)を止めるのはより困難。単体試験では問題出せず、コードレビューになって初めてスパゲティだと気づく。
だったらまだ、メソッド内の二重ネストぐらいは読むように努力し、逆に、意味のないメソッド抽出を避けたほうがいい。メソッド抽出の動機は、あくまで意味の構造化にあって、それがDRY原則に貢献しうるかを考え、もっと慎重になるべき。単なるネストコードの回避のためにやるのは安易すぎ。構文の字面だけ見て、意味を考えずに最適化するのに等しい。

逆に、この規約が通じる部分は、知性あるプログラムというよりは、コンポーネント結合のための労力がほとんどを占める可能性が高い。OOP仕様まみれになるあまり、やりたいこととコード量とのバランスが悪い警鐘にはなるかもしれない。

2. 'else'は禁止。条件文はifだけ。出来なければそのルーチンをexitすること。これでif-elseの連鎖を避けられて、1つのルーチンは1つのことだけをやるという状態を保てる。わかってきたね。

public void init(bool option1,bool option2) {
  String label1,label2;
  if(option1) {
    label1 = "有効";
  }
  if(!option1) {
    label1 = "無効";
  }
  panel.add(new Label(label1));
  if(option2) {
    label2 = "あり";
  }
  if(!option2) {
    label2 = "なし";
  }
  panel.add(new Label(label2));
}

うわ〜、変数が初期化されない可能性がある潜在バグだよ。これ、コンパイラ警告もんだよね。普通はこうでしょ。

  if(option1) {
    label1 = "有効";
  }
  else {
    label1 = "無効";
  }

まあ、ブロック内がこのぐらいの処理なら、三項演算子が普通だろうけどね。これもelse等価なので禁止なんだよね。きっと。

  label1 = option1 ? "有効" :"無効";

それとも、こうせよと?

public void init(bool option1,bool option2) {
  String label1 = "無効";
  if(option1) {
    label1 = "有効";
  }
  panel.add(new Label(label1));
  String label2 = "なし";
  if(option2) {
    label2 = "あり";
  }
  panel.add(new Label(label2));
}

こんな、手続きを追わないといけないコードは、宣言的コードに劣ります。「〜を条件としてAかBいずれかを行う」というのは思考ステップ1回で済み、しかも、直感的にAが肯定でBが否定だとわかる、かなり宣言に近い表現です。ところが、「初期値はBで開始せよ。〜の条件にマッチした場合はAとする」だと、AとBのどっちが肯定でどっちが否定なのか、順を追って考えないといけません。「初期値はAで開始せよ。〜の条件にマッチしない場合はBとする」と、簡単に言い換えても不自然に感じないからです。

当たり前ですが、elseそのものが罪なのではありません。そうじゃなくて、else以降の例外的処理コードが自然言語で説明できないぐらい複雑化してるのに、メソッド/オブジェクト/例外として抽出できる意味を見つけようとせず、else-ifの試行錯誤中にたまたま単体テストがグリーンになって、そのままそーっとしておこうという、そんなプログラマーの精神状態に罪があるのでは?

メソッド中のネストレベル0のif〜elseブロックの全体に対して、ひとつのコメントで説明できなければ、その場所には、直交成分のある二つ以上の問題が混在している証拠です。直交成分ごとに問題を切り分け、個々の問題に対して、メソッド/オブジェクト/例外をデザインする必要があるでしょうね。

単に字面だけelseをなくすように考えるのでは、

public void execute() {
  if (testOnly) {
    doNothing();
  }
  else {
    Result result = doSomething();
    if (result.isError()) {
      doRecoverry();
    }
  }
}

というようなコードから、多体なExecuterを考えついたり、エラーを例外として再設計することにつながる考えに至りません。
きちんと自然言語で、「試験の時は模擬実行にすり替え」「失敗なら回復処理を行う」と説明してやると、

public execute() {
  Executer executer = ExecuterFactory.createRealOrFake(testOnly);
  try {
    executer.doSomething();
  }
  catch(Exception e) {
    executer.doRecoverry();
  }
}

となります。「処理をすり替えるなら、同インターフェースの別のコンクリートにスイッチするといい」「失敗はいちいちifでフックせずに例外を使うのが定石だ」のような、事例-パターン対応の訓練に結びつくと思うんですけどね。このほうが、経験値がダイレクトに反映されて嬉しい。

3. 全部のプリミティブとstringをラップせよ。"primitive obsession."だ。整数型が使いたかったら、はじめにクラス(インナークラスでもいい)を作ってそれの本当の役割をはっきりさせる。たとえば郵便番号はオブジェクトで整数じゃないという具合に。これですっきりしてて、なおかつテストしやすいコードになる。

ええと、new Age(32)とかSex.MALEとか? 「年齢30歳以上」という条件書くときは、やっぱ、

if new Age(32).equalsOrGreaterThan(new Age(30)) { ... }

ということ?この調子ですべての演算をサポートするのかな?年齢は年齢同士の比較演算だけじゃなくて、時間間隔との加減算で別の年齢を返す必要もあるよ。たいへんだ。数値を使う箇所全部に、この調子でいちいちクラスを設け、可能性のある演算をすべて実装するのかな。

えーと、

if age >= 30 { ... }

age=32で何かもんくある?もし、年齢はちゃんと誕生日から出さないとだめ、って仕様に変更された場合、

if Utils.getAgeFromDate(birthday) >= 30 { ... }

に書き換えるのは自然だよね。整数比較は一目瞭然で、単体テスト不要。いっぽう、

age = Age.getFrom(birthday);
if age.equalsOrGreaterThan(new Age(30)) { ... }

って、これ。equalsOrGreaterThanの単体テスト増えるよね、たぶん。Age(30)ってコンストラクタも微妙だしなぁ。処理中に誕生日またがったらどうしよう?などなど、気が重い。

4. 1行に1つのドットのみ使うこと。これで他のオブジェクトの深くまでいってフィールドやメソッドにアクセスするってことを避ける。こうしちゃうとカプセル化を破ることになってしまうから。

それは、返されたオブジェクトがカプセル化を必要としているのかどうかによるよね。あと、1行づつ書いているプログラムがオブジェクトツリーを深く探索しない保証はない。

5. 名前は省略しない。ある種の余計なものによって手続き的な冗長さできることを避けるためだ。もしメソッドや変数のフルネームをタイプしないといけないんだったら、きっともっと時間をかけて名前について考えるだろう。それによってshipOrder()というメソッドを持ったOrderなんてオブジェクトを作らなくなって代わりにOrder.ship()みたいな呼び出しが増えるはずだ。

これは、クライアントコードのクラス名/メソッドコールを、可能な限り冗長に書いていると、API側の名前がナンセンスだということに気づきやすいということ?

6. エンティティを小さく保つこと。クラスは50行まで、パッケージごとに10クラスまで。1クラス50行までというのは極めて重要だ。これは簡潔さを強いてクラスをフォーカスさせるだけではなく、どんなエディタ/IDEでも1画面にクラスが収まることにもなる。

1クラスあたりのメソッド数は、1メソッドに3行使うとして、自然と15個が上限になるのかな。てことは、コンストラクタをぬいて、get/setペアが7ペアで限界か?まだJavadocなしで、importブロックも0行だという前提です。

画面での視認性は、IDEのコード折りたたみ機能をあてにしたらいいんじゃないの?というか、現実的に、他に1画面で一瞥する方法なんてなさそうだけど。

7. 3つ以上のインスタンス変数を持ったクラスは使わないこと。きっとこれは一番難しいだろう。ここでの著者の考えは、2つのインスタンス変数があると、ほぼ必ずと言っていいほどそのいくつかの変数を別のクラスにサブグループかする道理があるってことだ。

Point3D(x,,) セーフ Quoternion(x,y,z,w) あ、だめなのか、これ。4次元数以上はすべて、任意次元数のサブセットじゃないとだめなのですね。

FileFolder(name,security,timestamp,childEntries) あれ?
FileFolder(name,attribtes=Attrs(security,timestamp),childEntries) めんどくさ。

User(login=Login(name,password),profile=Profile(name=Name(firstName,lastName),contacts=[Email(email),Address(zip,address,country),Phone(number),..],...))
まじですか。Lispですかこれw

そんでさらに、規約4によると、これを、
user.profile.name.firstName
ってアクセスしちゃだめなんだってさ。user.getFirstName()を作れってことらしい。それするには、profile.getFirstName()が必要で、それには、name.getFirstName()が必要。えーと、ユーザオブジェクトから下の名前取り出すのに、いったいどれだけかかるのさ。もし、1クラスに2個しかインスタンス変数がない、というのを正直にやると、「フィールド数-1個のクラス」が必要です。最上位で扱いたい変数が20個あるなら、クラス数は19個要ります。

8. ファーストクラスコレクションを使うこと。換言すればコレクションを持つクラスは他のメンバー変数を持つべきではない。primitive obsessionの拡張だ。コレクションを含んだクラスが必要なら、そういう風に書くんだ。

コレクションそのもののインスタンスを、意味のある名前のついた変数に入れればそれでいいんじゃないの?わざわざ専用の操作なんてのを設計して使うより、コレクションAPIそのまま使えばいいじゃん。だめ?そのほうが、共同開発できるし、そもそも名前統一も考える必要ないし。

9. セッター、ゲッター、プロパティを使わない。これはカプセル化を強いる抜本的なアプローチだ。これはDependency Injectionアプローチの実装も必要になるし、"言え、訊くな"という格言の遵守も必要となる。

ありゃまあ、画面にログインユーザの名前を表示したくても、

panel.add(new Label(user.getName()));

は、だめなの?"言え、訊くな"的に設計すると、

user.putNameOnTo(panel);

になるけど、それって、UserがPanelとLabelに依存するよね。このままいくと、データベースサーバにウィンドウマネージャへの参照を永続化?意味不明。

メソッド内の字面のネストは嫌がるくせに、パッケージ依存性のネストは気にしないのかな?「言え、訊くな」だと、クライアントコード->モデル->UIライブラリ、と、依存性がネストするよね。常識で考えたら、モデルとUIライブラリは関係性切れてて、クライアントコードからその2つに、個別に依存矢印引かれるのが普通だと考えるんだけど。

断言する。これらのルールを破らずに1000行のプロジェクト書いたものはOOがうまくなる。

よって望めば制限を和らげることも出来るようになる。でも作者が指摘するのはそうする理由はないということ。

ハノイの塔を解くようなアルゴリズムでも、この方法ならあっという間に1000行ぐらい行くでしょうね。いくらでもメソッドを抽出できるし、いくらでもクラス構造をネストできる。メソッド内ネストとランタイム型を避ければ、層がひとつできあがり。さらに、その層を直接触らないための層を上に作っていけば、何重にも層化できて、無限にOOPで遊べるよね。

オブジェクト指向そのものの訓練にはなるけど、「うまくなる」というのはどうかと思う。うまくなるというのは、厨になるのとはちょっと違う。たとえば、Flash OOP厨は、ものすごい時間と労力をかけて、FlashでJavaFXのクローンを作るかもしれない。そういうのって、「うまい」のかな?

彼のチームはちょうど100,00行のプロジェクトをこの制限のうちに終えたところなんだ。

いったい、プログラムの知性部分は、このうち何行あるだろうか。断言しよう、全行数のうち70%以上が、オブジェクト指向のためだけに消費され、人(開発者、エンドユーザとも)を幸せにするための行は30%に満たないだろう。なーんつってな。

最初にとばしすぎた。失敗。

まとめ

  • 現実には、リソースは無尽蔵じゃない。時間もメモリも複雑度管理の精神力も限られている。
  • 現実には、Javaですら型宣言の強制を回避するリフレクションが人気だし、OOP言語の多数派は、実は、変数の型がもっとあいまいで、インスタンスの型についてもダックタイピングOOPもアリアリ。
  • プリミティブ型にメソッドがないOOP言語は、C++Javaぐらい。他のOOP言語はほとんど、プリミティブが強力なメソッドを持っている。自力設計より、ランタイム型のパワーを使うのが普通。

OOP設計よりも動くプログラムのほうがいい、動くプログラムよりいいのは仕様を物語る端的なコード。端的なコードを書くには、強い言語と強いランタイム型。これ重要。
Javaは性能のためにプリミティブをC++に似せた。性能重視なJavaOOP全体の正義の基準と考えるのは変。むしろ代表はC#。今はむしろ、Javaってのは、ヨゴレ手続きプログラムでもなんでも、速度優先のCみたいなコードを書くための言語なんじゃないかな。GoFデザパタマンセーとか言ってたら、そのうち、機械語チューニングしてなんぼぞ、って言ってた人みたいな空しさを感じるかもね。eval関数があれば、もうインタプリタパターン要らないし。