共通関数継承のデメリットを説明せよ

共通関数継承とは、あるクラスで共通的に使うだろう関数やメンバを、親クラスのメソッドやメンバとして定義するパターンだ。Constant interfaceパターンimport staticあたりが関係するアンチパターンとされるモノの一つで、神様ルートクラスを嫌い、POJOを好むのあたりの、ごく初歩な議論になるだろうと思う。

package spike;

public abstract class SuperUtil {

  /**
   * 時刻形式の文字列から、日付部分を抜き出す
   */
  protected String timestamp2Date(String timestamp) {
    if (validTimestamp(timestamp)) return timestamp.substring(0, 10);
    return "";
  }

  /**
   * 時刻形式の文字列から、時間部分を抜き出す
   */
  protected String timestamp2Time(String timestamp) {
    if (validTimestamp(timestamp)) return timestamp.substring(11, 8);
    return "";
  }
  
  /**
   * 時刻形式の文字列かチェックする
   */
  protected boolean validTimestamp(String str) {
    // ...
    return true;
  }
}
package spike;

public interface CommonMsg {
  public static final String ERROR_MSG_01 = "時刻形式が正しくありません";
}
package spike;

public class SubClass extends SuperUtil implements CommonMsg {
  private String timestamp;

  public void setTimestamp(String timestamp) {
    if (validTimestamp(timestamp)) this.timestamp = timestamp;
    throw new RuntimeException(ERROR_MSG_01);
  }
	
  public String getDate() {
    return timestamp2Date(timestamp);
  }
	
  public String getTime() {
    return timestamp2Time(timestamp);
  }
}

これは、とりあえず下のようにすべきだろう。とりあえず、だよ。

package spike;

public class SuperUtil {

  /**
   * 時刻形式の文字列から、日付部分を抜き出す
   */
  public static String timestamp2Date(String timestamp) {
    if (validTimestamp(timestamp)) return timestamp.substring(0, 10);
    return "";
  }

  /**
   * 時刻形式の文字列から、時間部分を抜き出す
   */
  public static String timestamp2Time(String timestamp) {
    if (validTimestamp(timestamp)) return timestamp.substring(11, 8);
    return "";
  }
  
  /**
   * 時刻形式の文字列かチェックする
   */
  public static boolean validTimestamp(String str) {
    // ...
    return true;
  }
}
package spike;

public class CommonMsg {
  public static String ERROR_MSG_01 = "時刻形式が正しくありません";
}
package spike;

import static spike.CommonMsg.*;
import static spike.SuperUtil.*;

public class SubClass {
  private String timestamp;

  public void setTimestamp(String timestamp) {
    if (validTimestamp(timestamp)) this.timestamp = timestamp;
    throw new RuntimeException(ERROR_MSG_01);
  }
  
  public String getDate() {
    return timestamp2Date(timestamp);
  }

  public String getTime() {
    return timestamp2Time(timestamp);
 }
}

さて。これを隣の同僚にどう説明したらいいものか悩み、先のエントリのアンチパターンからは外してみた。

リスコフの置換原則が破られる、試験性が落ちる、必要性がないなど、いろいろな理由は付けられるわけだけど、他のアンチパターンに見られるようなインパクトが見いだせないんだよなぁ。「どっちでもいいことにこだわりやがって」という印象を残さないくらい、強烈に説得したいんだが…。

ということで分けてみたよ、という言い訳エントリでした。

Javaプログラマが知るべき9のこと

はじめに

ソースコードは設計であり、コードの記述は品質に直結するのは言うまでもない。ちなみに、プログラマにとって特に重要なのは保守性だ。コードは書いた直後から保守対象となるからだ。コードは要求文書の範囲で動けばいいと思っている人がいれば今すぐ、ソースコードをコピペして100klに増えるプラグインがいつの間にかインストールされる呪いをかけてあげよう。幸い、ここを読んでいる人にはそんな人はいないだろうと思うけれども。

ということで、コードの品質を下げる要因、すなわちシステム全体の品質を下げる要因となり、かつ使われやすいアンチパターンを挙げ、対策を検討していくことにする。対象は以下:

  1. 出力パラメータ
  2. 処理状態返却
  3. 意味のある配列
  4. 無意味な初期化
  5. 多すぎるtry-catch
  6. 暗黙の順序
  7. コンパイラ警告の無視
  8. 過剰なコメント
  9. e.printStackTrace()

出力パラメータ

メソッドの引数にオブジェクトを渡しメソッドの処理結果をそのオブジェクトに格納させる手法は、本当によく見ると思う。例えばこんな感じ:

HogeBean hoge = new HogeBean();
someFile.read(hoge);
out.println(hoge.getFuga());
out.println(hoge.getPiyo());

これの駄目なところは、someFile.readによってhogeオブジェクトの状態がどう変わるのかがreadメソッドを読まない限りわからないことだ。そういう箇所が一つであればまだマシだが、数箇所あったりifやwhileが関わったりするともう人の手には負えないコードになってしまう。「hogeの状態管理がsomeFile.readにまとまった」と思ったら大間違いだ。委託と丸投げくらいの違いがある。

そこはこう変えよう。

HogeBean hoge = new HogeBean();
hoge.setFuga(someFile.getFuga());
hoge.setPiyo(someFile.getPiyo());
out.println(hoge.getFuga());
out.println(hoge.getPiyo());

もう一歩進むとしたら、HogeBeanのコンストラクタにsomeFileを渡して、HogeBean内でfugaとpiyoをセットさせる方を選ぶ。が、とりあえず現時点では「hogeに対して外部からsetしてる時点で…」等という議論は避けることにしよう。一歩一歩いきましょうね!

処理状態返却

メソッドの返却値として正常終了したかどうかを返すパターンだ。

public int select(List<Hoge> list, String strDate) {
  try {
    List<Hoge> l = hogeDao.selectByDate(strDate);
    list.addAll(l);
    return 0;
  } catch (SQLException e) {
    log.error(e);
    return -1;
  }
}

これもよく見るだろう。intではなく独自のエラー処理用オブジェクトを使っていることも多い(今まで見た最も素敵なコードは"public Exception exec()"だ)。そして大抵、出力パラメータと仲がいい。というか、メソッドの処理状態を返すために貴重な返却値を使われてしまっているので、メソッドの処理結果はパラメータ上で渡す以外にない。

なんと言っても最大の欠点は、その異常処理状態がメソッド呼び出し側で無視される可能性が高いことだ。開発中のデバッグが難しくなるだけならまだマシだが、テストの網を潜り抜けてリリースされてしまう恐れがある。

ここは素直に例外処理を使うべきだ。ついでに出力パラメータも直してしまおう。

public List<Hoge> select(String strDate) {
  try {
    return hogeDao.selectByDate(strDate);
  } catch (SQLException e) {
    throw new RuntimeException(e);
  }
}

SQLExceptionをそのままthrowしてもいいが、ここではとりあえずRuntimeExceptionでラップした。よほど小規模のアプリケーションじゃない限り、SQLExceptionやIOExceptionはどこかの段階で何か別のExceptionにラップされるべきであり、十分な設計がされているとは思えないアプリケーションでも既に何らかの形でラップされているだろう。throws定義を伝播させるのが面倒すぎるからだ。どの段階でどんな例外として扱うかは考慮が必要となる。*1

メソッド呼び出し側が定義したlistにaddAllするかどうかは、呼び出し側に託すことにした。

繰り返すが、これはゴールでも模範解答でもなんでもない。一歩前より幾分マシにするだけの、小さなステップだ。

意味のある配列

配列の要素に意味を持たせたパターンだ。もちろん、Listでも同じ。「全てがListだ!」なんていうのはLisperにでも言わせておこう。

String[] destination= new String[3];
destination[0] = "920-8577";
destination[1] = "金沢市広坂1-1-1";
destination[2] = "金沢市役所";
return destination;

全てが暗黙の仕様となっている。

ここはクラスとして定義すべきだ。

public class Destination {
  public String zipcode;
  public String address;
  public String name;
}

// ...
Destination destination= new Destination();
destination.zipcode = "920-8577";
destination.address = "金沢市広坂1-1-1";
destination.name = "金沢市役所";
return destination;

情報隠蔽されていないクソクラスが増えたが、さっきよりマシだ(もっとも、setter/getterを使ったらこれよりマシになる、とは思わないが)。前者に比べて記述が増えてるが、その暗黙の仕様を書いたExcel方眼紙が要らなくなる。

大事なことなので繰り返すが、これは小さなステップでしかない。全ての問題が解決する例示なんて期待するほうが間違っている。

無意味な初期化

どこからの文化なのだろうか、変数の初期値としてリテラルやnewされたオブジェクトを代入しなければならないと思い込んでいる人は意外と多いようだ。

List<Hoge> hogeList = new ArrayList<Hoge>();
hogeList = HogeContainer.getList();

String fuga = "";
fuga = hoge.getFuga();

確かに、closeが必要なオブジェクトを扱う場面などで初期値の扱いが難しいことはある。が、それ以上に無駄に初期化しているコードは本当によく見る。

これは、1行にまとめよう。

List<Hoge> hogeList = HogeContainer.getList();

String fuga = hoge.getFuga();

まとめにくいと感じた場合は、変数のスコープが人の頭で管理できる限界を超えているかもしれない。どの変数がどこで必要になるのか、整理が必要だ。

似たようなパターンとして、スコープの最後でnullを代入しているパターンがある。オブジェクトのデストラクタのつもりだろうか。

多すぎるtry-catch

例外処理が、例外発生ごとに書かれているパターンだ。

public ErrorInfo exec(String param) {
  List<Model> models = null;
  BigDecimal dec = null;
  try {
    models = logic.readFromDB(param);
  } catch (SQLException e) {
    return new ErrorInfo("データベース接続でエラーが発生しました。");
  }

  try {
    dec = logic.sumUp(models);
  } catch (BussinessLogicException e) {
    return new ErrorInfo("集計処理でエラーが発生しました。");
  }

  try {
    out.write("合計:" + dec + "単位");
  } catch (IOException e) {
    return new ErrorInfo("出力部分でエラーが発生しました。");
  }

  return null;
}

通常の処理と様々なレベル(プログラム由来やデータ由来、設定由来など)のエラー処理とが混在し、ここで何をしたいのか不明確だ。また、それぞれの処理がtry-catchブロックで分かれるため、ブロック間で情報を伝達させるためのローカル変数を冒頭で宣言しなければならず、必然的に無意味な初期値を与えざるを得ない。そもそも、各メソッドで非チェック例外(NullPointerExceptionなど)が起こったとして、適切にエラー処理がされるのだろうか?

処理状態返却パターンでの構造を例外処理構造にそのまま適用したのだろう。黄金のアンチパターンとも呼べるようなコードが仕上がった。

try-catchブロックは一つにまとめてしまおう。

public ErrorInfo exec(String param) {
  try {
    List<Model> models = logic.readFromDB(param);
    BigDecimal dec = logic.sumUp(models);
    out.write("合計:" + dec + "単位");
  } catch (SQLException e) {
    return new ErrorInfo("データベース接続でエラーが発生しました。");
  } catch (BussinessLogicException e) {
    return new ErrorInfo("集計処理でエラーが発生しました。");
  } catch (IOException e) {
    return new ErrorInfo("出力部分でエラーが発生しました。");
  }
  return null;
}

ただし、投げられるExceptionクラスが混在している場合はcatchで分離できないため、少し厄介になる。上記コードでは、最低限としてスタックトレース情報をErrorInfoに管理させる必要があるだろう。

また、それぞれのcatchブロックで何か特別な処理をしている場合、例えばBussinessLogicExceptionをcatchした中でreturnせずにdecの初期値を設定している場合は、try-catchをまとめることはできない。例外処理構造に本来の(「例外」ではない)処理を混在させてしまっているせいだ。try-catchをネストさせるか、もしくはメソッド内部に収まらない変更が必要だろう。

例外処理について考えるための、いいきっかけになるかもしれない。

暗黙の順序

例えば、インスタンス変数の初期値を設定するメソッドと、それを使って何かをするメソッドが分かれているようなパターンだ。

public class Hoge {
  private Props props;

  /**
   * インスタンスを初期化する
   */
  public void init() {
    this.props = new Props();
  }

  /**
   * プロパティから設定を読み込む
   */
  public void setProps(Properties p) {
    props.setCompanyName(p.get("company.name"));
  }

  /**
   * ようこそメッセージを返す
   */
  public String welcomeMessage() {
    return props.getCompanyName() + "へようこそ";
  }
}

知らずに使うと、welcomeMessageを使ってぬるぽが起こり、初めてinitする必要があることを知るだろう。initしてwelcomeMessageを使ってみたが、得られるのは「へようこそ」「へようこそ」「へようこそ」。「へ」って何じゃ!!進捗は1日遅れです。

上のコードのような小さいクラスだとsetPropsに気付くだろうが、現場にコミットされているクラスはそんなもんじゃなかろう。2000行の中から探し出さなければならないとしたら、人智を超越した何かへの挑戦だ。そして、ベタ書き派は喜んで言う。「ほら、処理を分けると読み難くなるだろっ?」

インスタンスの前提条件があるとしたら、インスタンス変数の初期値としたり、コンストラクタにまとめよう。

public class Hoge {
  private Props props = new Props();

  /**
   * プロパティから値を読み込む
   */
  public Hoge(Properties p) {
    props.setCompanyName(p.get("company.name"));
  }

  /**
   * ようこそメッセージを返す
   */
  public String welcomeMessage() {
    return props.getCompanyName() + "へようこそ";
  }
}

これで、このインスタンスは必ずpropsを保持していることが保証できた。インスタンスが扱える状態であれば常に、それっぽいwelcomeMessageを出せるだろう。

もう一歩進めるとしたら、propsをfinalとするのもいい。さらにもう一歩であれば、Propsクラスが保持しているだろうCampanyNameもfinalとしたい(そのためにはsetterアクセスをちょっと変える必要はある)。そう、不変クラスとかイミュータブルと呼ばれるヤツだ。インスタンスの状態遷移を排除すると、順序関係を逆戻りさせるような操作がないこともわかるようになる。*2

DIコンテナでセッターインジェクションを使ってるとしたら、…まぁそれもいいだろう。その場合はメソッドの先頭で前提条件に合わない場合に例外投げたり(メッセージは分かりやすくしておこう)、最低限Javadocに何か書いてくれると助かるかもしれない。もっとも、DIコンテナは詳しくないので、最近は解決できる何かがあるのかもしれないが。

コンパイラ警告の無視

EclipseでのWarning(黄色い「!」)のことを、ここではコンパイラ警告と呼ぶことにしよう。不要なimport、未使用のprivateフィールドやメソッド、未使用の引数やローカル変数、型パラメータの省略、インスタンスを通じたstaticアクセス、非推奨APIの使用…、現場のコードでよく見るのはこれくらいだろう。

import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import org.junit.Test;

public class ALotOfWarnings {
  private List alist;
  private List anotherlist;
  private String hoge = "hoge";

  public ALotOfWarnings() {
    alist = new ArrayList();
    alist.add("hoge");
    alist.add(Date.UTC(2011, 2, 4, 11, 22, 33));
  }

  public Date Foo() {
    return (Date) alist.get(1);
  }

  private void makeAnotherlistSomething() {
    anotherlist = new ArrayList();
  }
}

このコードが正常に動くだろうか?JVMの立場からは、正常に動くだろう。それ以上は何ともいえない。

直せるところから直してみよう。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class ALotOfWarnings {
  private List alist;
  private String hoge = "hoge";
  private SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

  public ALotOfWarnings() {
    alist = new ArrayList();
    alist.add(hoge);
    try {
      alist.add(df.parse("2011-03-04 11:22:33"));
    } catch (ParseException e) {
      throw new RuntimeException(e);
    }
  }

  public Date Foo() {
    return (Date) alist.get(1);
  }
}

これでも、alistに総称型パラメータを指定していないとう警告は残る。上記「意味のある配列」パターンが潜んでいることが明確になった。これ以上は、この短いコードなら答えを出すのは簡単だが、実際の製品中のコードで短時間で答えを出せるとは限らないだろう。

コンパイラ警告はアンチパターン集だ。それ自体大きな問題にはならないことも多いが、設計パターンに関して重要なヒントを与える。活用しない手はない。

過剰なコメント

Subversion等でバージョン管理がなされている環境であれば、改変履歴コメントや改変元コードのコメントアウトは害悪でしかない。diffが取りづらくなるからだ。他にも単純なsetter/getterのJavadoc等は管理すべき資産を無駄に増やすので、付けるべきではない。

/**
 * hogeをセットする。<br/>
 * #456 2011-02-04 katzchang 0以下の場合は0をセットするように変更
 * @param hoge
 */
public void setHoge(int hoge) {
//del start #456 2011-02-04 katzchang
//  this.hoge = hoge;
//del end #456 2011-02-04 katzchang
//add start #456 2011-02-04 katzchang
  if(hoge > 0) {
    this.hoge = hoge;
  } else {
    this.hoge = 0;
  }
//add end #456 2011-02-04 katzchang
}

/**
 * hogeを返却する。
 * @return hoge
 */
public int getHoge() {
  return hoge;
}

ここはシンプルにいこう:

/**
 * hogeをセットする。<br/>
 * ただし、0以下の場合には0をセットする。
 */
public void setHoge(int hoge) {
  if(hoge > 0) {
    this.hoge = hoge;
  } else {
    this.hoge = 0;
  }
}

public int getHoge() {
  return hoge;
}

適切に管理されたJavadocコメントは本当に役に立つ。しかしコピペ駆動開発現場では、コピペ元の@paramが残っているなどのワナが残されることが多い。機械的にチェックできるプラグインがあるはず(たぶん。なければ誰か作るべき)なので、Javadocを残さなければならないとしたら、導入を検討しよう。*3

e.printStackTrace()

catch句で、なぜかprintStackTrace()が呼ばれているパターンだ。*4

try {
  //...
} catch (SQLException e) {
  e.printStackTrace();
  throw new HogeException("データベース接続でエラーが発生しました");
}

さて、ここまで「うんうん」と頷きながら読み進めた人であれば、これの何がダメでどうすべきかは知っているだろう。解決する道筋はいくつかあるが、そう難しい話はない。問題はここから。

職場でこういったコードを見たことがある人は、同僚に対して潜在する問題点を説明し、改善案を提示するまでのストーリーを考えてほしい。納得してもらい、円滑にコードを修正する。これができなければ、今までの解決策も含め、すべてがただのこだわりプログラマのウンコ主張に終わってしまう。本当に難しい問題はここにある。

まとめ

小さな一歩を繰り返してクソコードをマシにしていくことで、ようやくコードは設計書と呼べるレベルに近づいていくだろう。クソコードを放置することはコード自体の管理の手間が増えるだけではなく別文書の複雑な設計書の管理も必要とさせ、保守コストに二重に跳ね返る。デバッグで終わらせる人生なんて馬鹿げていると思ったら、一歩ずつでいい、コードを整理していこう。完璧である必要なんてない。

そして、ほとんどのことはたぶんEffective Javaに書いてある(読んだことないけど)。それでなくても幾度となく繰り返されている議論であるはずだが、やはり届いていない現場は多い。さてこの状況、私たちに何ができるだろうか?

Effective Java 第2版 (The Java Series)

Effective Java 第2版 (The Java Series)

おわりに

ブクマで「id:kyon_mm 多分この辺はCleanCodeのほうが近い内容かな。」とあったけど、書評を見る限りそう思います(さっき注文した)。著者アンクル・ボブが、プログラマが知るべき97のことに寄稿した「08:ボーイスカウトルール」で書いていることに似ている…ような気もしないでもないというとアレなんかな。

ということで、あわせて読みたい

プログラマが知るべき97のこと

プログラマが知るべき97のこと

Clean Code アジャイルソフトウェア達人の技

Clean Code アジャイルソフトウェア達人の技

*1:実際の方法論は諸説ある。議論はhttp://www.ibm.com/developerworks/jp/java/library/j-jtp05254/http://d.hatena.ne.jp/daisuke-m/20081202/1228221927等を参照のこと。

*2:この部分は修正。http://twitter.com/#!/yukihane/status/34999009835749376により。

*3:Javadocを生成する場面で警告される、とのことhttp://twitter.com/#!/bleis/status/34772634373722113。生成までしてないのがバレバレでございます。

*4:目撃したコードに近い形になるよう、ちょっとコードを変えた。

ソースコード改造の発注時に気をつけること

メモメモ。他にあれば、ぜひおしえてください。

  • ソースコード中の改変コメント("// add 2011.02.03 katzchang start"等)は不要です。
  • 元のソースコードコメントアウトは不要です。必要ないコードはそのまま削除してください。
  • ifなどのブロック("{ ... }")の追加や削除の場合、ブロック中のソースコードに改変がないとしても、インデントを適切に増減させてください。
  • Javadoc コメントを記述する場合、定義と矛盾がないようにしてください。メソッドコメントの @param が特に矛盾しやすいようです。setter/getterのように自明であれば、省略しても結構です。
  • Javadoc以外で、ブロックコメント("/* ... */")は使わないでください。

SCMで適切に管理されていることが前提です。

追記

あぁ、大事なことを忘れていた。

  • VSSで管理しないでください。改行コードが崩れています。

VSSのせいかどうかわかんないけど、そういう現象ってありませんか?

幼児に歯を磨かせるためのいくつかの施策

3歳児に歯を磨かせるのは大変だ。

「歯を磨かないと、口腔内に残った糖分が酸に変化してエナメル質を溶かすんだよ」とか、「歯はまだいい、歯肉炎にかかりだしたらヤバいぞ」などの脅しは通じないのが、奴らの欠点だ。

そこで、いくつか試している施策を紹介する。

歯みがきの音楽

長い間、虫歯鉄道を愛用している。iTunesで6分の動画が売ってるので、買って、iPhoneで聞かせている。

おかあさんといっしょ」の「虫歯建設株式会社」は悪くないが、テンポが早く、奴らの集中力を削ぐ。「はみがきじょうずかな」は時間が短すぎるし、何よりも「おかあさんコール」で父親の出番がなくなる。

歯みがきスタンプ

用意するものは台帳として適当な厚紙と好みのシール。台帳はamazonダンボールの底板を豪快に使うのもいい。

で、「12個たまったらおもちゃ1つ」などの条件について合意できたら、台帳に題名と数字を書き、歯みがき1回に対して1枚のシールを貼っていただく。ユーザを囲い込み、リピート率を向上させる作戦だ。

うちではご褒美はめばえとしている。値段的に500円前後なのが丁度いい。

めばえ 2011年 02月号 [雑誌]

めばえ 2011年 02月号 [雑誌]

条件について詰める場面で、奴らは「12こじゃなくて13こにしたらは?」などと課題を増やす方向に交渉されるときがあり、困惑する。

歯みがきセット

歯みがきのあとに5分程度のコンテンツを与える約束を交わす。絵本の読み聞かせも悪くないが、トム&ジェリーなんかが効果は絶大。

トムとジェリー プレミアム DVD BOX (DVD付) (<DVD>)

トムとジェリー プレミアム DVD BOX (DVD付) ()

「早く終わったら2個見てもいいよ」などとプレミアムを与えると、奴らは素早く反応し、行動する(「2個セット」と呼ぶ)。ただし、どんなに時間がかかったとしても「時間がかかったから歯みがきセットはなしね」などと言うと、30分以上の号泣ストライキで対抗されるので注意が必要になる。

一つのコンテンツを繰り返し楽しむという奴らの特性を考慮すれば、かなり低コストな策だ。

おわりに

うちで試している、いくつかの施策について紹介した。基本的に、別の楽しいもので釣るという作戦が有効のようだ。

ちなみに、娘ちゃん3歳が昨夜「まっくぶっくえあーじゅういちいんちほしい!」と急に叫んだのでびっくりした。「MacBook Air 11インチ欲しい!」と言うことだろうか。まったく意味が分からないが、なにか良い事があるかも知れないので、ここに記しておこう。

「クラスはオブジェクトの集合である」についての疑問

Aクラスは全てのAクラスのインスタンスの集合である
かつ、
Set<A>のインスタンスは特定のAクラスのインスタンスの集合である

Set<A>のインスタンスはAクラスの部分集合である

…となるが大丈夫か?というのが自分の疑問です。

継承は害悪か。 - Togetterや、TDDBC名古屋で夜にid:rf0444ともお話していた話題ですわね。昨日から今日にかけて、Twitterでいろいろやってたお話です。

「型は集合である」という定義もあるそう*1なので、関連するとは思うけど、型の世界は勉強不足なのでよくわかっていません。が、上記の「Aクラス」を「型A」と読み替えても割といいのかなというイメージではいます。

クラスとそのサブクラスとの関係のみを考えた場合は、「クラスはオブジェクトの集合である」というのは割と自然だったりする…んですかねぇ。

どうでしょうか。

ブコメより

id:hoxo_m なんも難しいことはなくて Set は集合を表現しているだけで集合ではない。
http://b.hatena.ne.jp/entry/d.hatena.ne.jp/katzchang/20110107/p1

それはSet<A>の話?それともSet<A>のインスタンスの話?

Don't Tell, Ask - 求めよ、命じるな

えーと、どんな因果か、レビューされる側よりもする側になることが多くなってしまった。

対象となる生産物の欠点を指摘することは、レビューの大事な役割の一つであるとされている。だが、レビュアーとして一番怖いのは、間違った指摘や無意味な指摘をしてしまうことだったりする。そう、レビュアーもまた間違いを犯すんだよ。恐ろしいことに。

そこで有効なのが、"Don't Tell, Ask."「求めよ、命じるな」の原則。

レビュー対象物の「至らなさそうな点」に対して、なぜそこはそうなっているのかについて答えるよう求める。自分がより良いアイデアを持っていれば、それを伝え、どちらが優れているか判断を求める。「ここは、こうしてよ」のような命令(出す側は「アドバイス」と呼ぶ)は出さない。だって、レビュアーの意見の方が優れている保証なんて、何もないんだからね。

そもそもレビューをするのは、そのアドバイスに黙って従う初級プログラマレミングスをプレイするためではないし、クソ面倒な仕事が増やされたと机を殴る中級プログラマの被害妄想を育てるためでもない。レビュー対象物の質を上げると同時に、レビュアーが直接作る以上にチーム全体の生産性を向上させることが、レビューの大切な役割だ。そのために、選択肢のどちらを選ぶかだけではなく、それを選ぶ理由も納得する必要がある。

最近はレビューの最初に、気にかかっていることはないか尋ねるようにしている。仕様について何となく不明確な点はないか、コードの構造について「嫌な感じ」がないか、など。聞くと何かしら出てくることが多い。「気になってたならもうやっておけよ」と思うかも知れないが、それを求めるのはクソだ。多くの場合は時間的な制約があるし、「嫌な感じはあるが解決策がわからない」というような能力的な制約もある。「気になることなんてない」と答えるヤツは超人プログラマか、素人か、嘘つきか、だろうね。

さて、"Don't Tell, Ask."は、オブジェクト指向でのプラクティス"Tell, Don't Ask."「求めるな、命じよ」から借用した*1。組織構造はオブジェクト指向的なアイデアが参考になることが多いが、たまに、こういう真逆な構造もあるのが面白い。

ペアプログラミングを実践していれば解決する課題はあるかも知れないが、自分たちのチームでは導入できてはいないので、残念ながら語ることはできない。

アジャイルプラクティス 達人プログラマに学ぶ現場開発者の習慣

アジャイルプラクティス 達人プログラマに学ぶ現場開発者の習慣

*1:アジャイルラクティス第6章にある。らしい。未確認。