Java Beans の大罪 〜 getter/setter を捨てて美しきオブジェクトの世界で生きよ〜


まえがき

以下のコードを見てください。

Car car = new Car();
car.getEngine().getFuelContainer.setFuel(Fuel.litreOf(30));

もしあなたが、このコードに違和感を感じたのであれば、以下の記事を読む必要はありあません。


はじめに

開発の現場では今なお、疑問を挟む余地なく Bean という言葉が使われ、それに付随する getter/setter の呪縛に囚われ続けた状態が続いています。

この慣習は広く蔓延し、多くの開発者の思考を停止させ、悪しき文化を助長しつづけています。


本記事では、Java の悪しき習慣となっている getter/setter 文化について歴史背景とともに振り返り、検討していきます。


Java Beans の誕生

getter/setter 文化の形成の発祥は 1997年の Java Beans の誕生に遡ります。


Java Beans は GUI 開発時のビジュアル・プログラミング環境をターゲットに、ソフトウェア・コンポーネントの再利用性を向上させる目的で登場しました。

当時は Visual Basic により GUI を構築し、バックエンドにデータベースが存在する クライアント-サーバ型のアーキテクチャが広く使われていました。 これに合わせるように Java でも Visual Basic のように開発環境上でコンポーネントをデザインし、コンポーネントのプロパティを GUI 上で設定していく手法が採用され始めました。

JBuilder とか Visual Cafe といった統合開発環境により、Visual Basic のような操作感で、クライント-サーバ型のアプリケーション開発が可能になりました。

GUI コンポーネント(ダイアログボックスやボタン、ラジオボックスといったコンポーネント)を、サードパーティー製のコンポーネントも含めてGUIデザイナ画面上で統一的に扱えるようにするためには何かしらの規約が必要になります。

そこで誕生したのが Java Beans 仕様であり、これに従うことでリフレクションやイントロスペクションなどによりコンポーネントのプロパティーやメソッドなどを調べ、ビジュアルツール上でコンポーネントのプロパティを編集することで GUI 構築を直感的に行うことができるようになりました。


Java Beans は以下の仕様に従うものとして定義されました。

  • publicでかつ引数なしのコンストラクタを持つこと
  • java.io.Serializableインターフェイスを実装すること
  • プロパティなどを操作するメソッドの命名規則に従うこと(getter/setterの名前やイベントリスナ用メソッドの名前など)


これらの規約に従ったコンポーネントを作成することで、java.beans パッケージで提供されるクラスを経由して扱うことができるようになり、サードパーティー製のコンポーネントも合わせて統合開発環境で統一的に扱えるようになりました。

しかしこの後台頭してくる Web化の波により、クライアント-サーバ型のアーキテクチャは衰退していくことになります。

ここで押さえておきたいのは、Java Beans 仕様は GUI構築の際のコンポーネントのプロパティを統一的に扱うための規約であった という点です。


サーバサイドの台頭

Java はクライアントアプリケーションとしてだけでなく、ブラウザ上で動くアプレットにて、動的な Web ページを作成する用途で利用が広まっていました。 そして 1998 年にリリースされたサーブレットが成功し、Java は徐々にサーバサイドの基盤としての地位を獲得していくことになります。

当時の動的 Web ページと言えば CGI を使ったものが主流でしたが、CGI ではリクエスト毎にOSのプロセスが起動するため、大量のリクエストを扱うには多くのサーバリソースが必要でした。

この点で、Java サーブレットは同一プロセス内のスレッドにてリクエストを処理するため、CGIと比較してリソース効率に優位性があったことで利用が広がっていくことになります(その後、スレッドの利用で C10K問題が謳われ、Nginx などのイベントドリブンな非同期型のアーキテクチャが台頭してきたのは興味深い所です)。


当時の Java サーブレットといえば HttpServletResponse から得た writer にレスポンスとなる HTML を、まさに書き出すといった方法で利用されていましたが、続いて登場する JSP により HTML コードの中にスクリプトレットで値を書き出すといったことができるようになっていきました。

この JSPでは setProperty getProperty タグで getter/setter が定義された Bean のプロパティに名前でアクセスできました。さらに JSP 2.0 では EL式が定義され、プロパティ名を . で連結したEL式として書くことで、どんなに深いオブジェクトのプロパティでも簡単にアクセスできるようになりました。

画面とやり取りするオブジェクトは getter/setter が定義された Bean を前提にするものとしてアプリケーションフレームワークが整備されていったのです。


Struts と BeanUtilsの興起

Java サーブレットでは、サーブレットAPI を使い、ブラウザから POST されたデータは、HttpServletRequest で受け、そこから request.getParameter("name") のようにパラメータを取得していました。

2000年台に入ると、Struts などのフレームワークが現れ、アクションフォームと呼ばれる Java Beans 仕様に即した DTO を用意しておくことで、リクエストパラメータを自動的でマッピングしたオブジェクトが簡単に取得できるようになりました。


以下のような フォームBean を定義し、

public class LoginForm extends ActionForm {

    private String email;
    private String password;

    // setter/getter

    public ActionErrors validate(
        ActionMapping mapping, HttpServletRequest req) {...}

    public void reset(
        ActionMapping mapping, HttpServletRequest req) {...}
}

アクションで以下のようにリクエストパラメータにアクセスすることができました。

public class LoginAction extends Action {
    public ActionForward execute(
            ActionMapping mapping, 
            ActionForm form, 
            HttpServletRequest req, 
            HttpServletResponse res) throws Exception {
        String email = ((LoginForm)form).getEmail();
        // ...
    }
}


フォームBean を用意しておくだけで HttpServletResponse からパラメータを一つずつ取り出すといった操作が不要になり Bean という考え方が広く一般化していくことになりました。

HTTP リクエストからフォームBean へパラメータを設定する過程で作成されたコードをライブラリとして抜き出したものが Commons BeanUtils になり、今も getter/setter が定義されたオブジェクトのプロパティを操作する場面で使われることあります。


Java の外側にある HTTP の世界とのインターフェース相互変換において、文字列名称をキーにして Java Beans の命名規則として結びつける形で自動化することで開発効率を高めました。JSP と Struts により Bean という言葉はサーバーサイドWebにおける慣例として一般化していきました。


Enterprise JavaBeans

話しは前後しますが、エンタープライズの世界でもコンポーネントを再利用可能な Beans として扱う考えから、Enterprise JavaBeans(EJB) が登場します。

最初の登場は 1998年頃です。

EJB は分散オブジェクトとしてリモートアクセスできるコンポーネントとして再利用を目指したものでした。


Enterprise JavaBeans のコンポーネントは、Stateful Session BeanStateless Session BeanMessage Driven BeanEntity Bean などがあります。 GUI コンポーネントのための Java Beans は、エンタープライズの世界で、ネットワーク越しにやり取りする再利用可能なコンポーネント = Bean という意味合いで使われはじめました。


EJB は複雑なだけで再利用なんてされることはほとんど無く、遅くて使い物にならないという結論に達するまでには少し時間がかかりました。アプリケーションサーバを売りたいベンダの過大広告に押されて導入するも、実際の開発にはボイラープレートコードが多く必要であり、重厚超厚な EJB 仕様には多くの苦痛が伴うものでした。

コンテナがなければ動作しないという煩わしさもあり、アンチEJB として Spring や Hibernate などのいわゆる POJO を基礎とした軽量フレームワークが謳歌するきっかけとなり、さらにはその教訓が EOD(Easy Of Debelopment) として EJB 側に取り込まれることになるのはさらに後の話になります。


さて、EJBのコンポーネントとして特に悪評高きは Entity Bean でした(現在は既に廃止されており JPA が取って代わっています)。

エンティティBean は「データベースなどの永続記憶域に格納されたエンティティ、または既存のエンタープライズアプリケーションによって実装されるエンティティの、オブジェクト指向のビューを表現するオブジェクト」であり、永続化などの低レベルな操作からセッションBeanを開放することを目的としたものです。

エンティティBean の例を、忌まわしき Pet Store App から抜粋してみましょう。


Address クラスがあり、

public class Address {
  private String streetName1;
  private String streetName2;
  private String city;
  private String state;
  private String zipCode;
  private String country;

  // Constructor to be used when creating PO from data
  public Address() {}

  public Address(String streetName1, String streetName2, String city,
                 String state, String zipCode, String country) {
    this.streetName1 = streetName1;
    this.streetName2 = streetName2;
    this.city = city;
    this.state = state;
    this.zipCode = zipCode;
    this.country = country;
    return;
  }

  // getter methods
  public String getStreetName1() { return streetName1; }
  public String getStreetName2() { return streetName2; }
  public String getCity() { return city; }
  public String getState() { return state; }
  public String getCountry() { return country; }
  public String getZipCode() { return zipCode; }

  // setter methods
  public void setStreetName1(String streetName) { this.streetName1 = streetName; }
  public void setStreetName2(String streetName) { this.streetName2 = streetName; }
  public void setCity(String city) { this.city = city; }
  public void setState(String state) { this.state = state; }
  public void setCountry(String country) { this.country = country; }
  public void setZipCode(String zipCode) { this.zipCode = zipCode; }

  // XML (de)serialization methods
  public Node toDOM(Document document) {
    // ...
  }

  public static Address fromDOM(Node node) throws XMLDocumentException {
    // ...
  }
}


EntityBean は以下のようになります。

public abstract class AddressEJB implements EntityBean {
  private EntityContext context = null;

  // getters and setters for CMP fields
  public abstract String getStreetName1();
  public abstract void setStreetName1(String streetName1);

  public abstract String getStreetName2();
  public abstract void setStreetName2(String streetName2);

  public abstract String getCity();
  public abstract void setCity(String city);

  public abstract String getState();
  public abstract void setState(String state);

  public abstract String getZipCode();
  public abstract void setZipCode(String zipCode);

  public abstract String getCountry();
  public abstract void setCountry(String country);

  // EJB create method
  public Object ejbCreate(String streetName1, String streetName2, String city,
         String state, String zipCode, String country) throws CreateException {
    setStreetName1(streetName1);
    setStreetName2(streetName2);
    setCity(city);
    setState(state);
    setZipCode(zipCode);
    setCountry(country);
    return null;
  }

  public void ejbPostCreate(String streetName1, String streetName2, String city,
      String state, String zipCode, String country) throws CreateException { }

  public Object ejbCreate(Address address) throws CreateException {
    setStreetName1(address.getStreetName1());
    setStreetName2(address.getStreetName2());
    setCity(address.getCity());
    setState(address.getState());
    setZipCode(address.getZipCode());
    setCountry(address.getCountry());
    return null;
  }

  public void ejbPostCreate(Address address) throws CreateException { }

  public Object ejbCreate() throws CreateException {
    return null;
  }

  public void ejbPostCreate() throws CreateException { }

  public Address getData() {
    Address address = new Address();
    address.setStreetName1(getStreetName1());
    address.setStreetName2(getStreetName2());
    address.setCity(getCity());
    address.setState(getState());
    address.setZipCode(getZipCode());
    address.setCountry(getCountry());
    return address;
  }

  // Misc Method
  public void setEntityContext(EntityContext c) {
    context = c;
  }

  public void unsetEntityContext() {
    context = null;
  }

  public void ejbRemove() throws RemoveException { }
  public void ejbActivate() { }
  public void ejbPassivate() { }
  public void ejbStore() { }
  public void ejbLoad() { }
}


コンテナに管理を任せれば、上記のような エンティティBean を使うことで永続化操作を強く意識しなくても済むという代物ですが、これは幻想に終わりました。ほとんど誰にも使われないまま、エンティティBean は現在 EJB 仕様からも外されています。

Address クラスを見ると getter/setter だらけなのが分かると思います(EntityBeanも同様です)。 これらはオブジェクトというよりは永続化のための単なるデータホルダーに過ぎず、型が定義された単なるハッシュのように見えます。

この当時は、取得したデータホルダーを セッションBean で手続き的にビジネスロジックを処理していく というスタイルでの開発が一般的でした。


さらに エンタープライズの世界ではレイヤードアーキテクチャが一般化し、各層の依存を無くす目的でレイヤーの跨ぎは DTO を介するべきといった考え方や、ステートフルセッションBeanのリモートコールを最小化するために DTO の利用がパターン化されるといった背景もあり、オブジェクトと呼ぶにはためらいのある getter/setter を備えた単なるデータホルダーが DTO という名を呈して蔓延していくことになります。


ORM の台頭

エンティティBean の失敗により、代替となる永続化ライブラリが多く出てきました。 JDBC の単純なラッパだったり、クエリビルダーのような軽いものから、後の JPA に繋がる Hibernate のようなオブジェクト指向に舵をきったものまで色々ありました。


Hibernate などの ORM は RDB とのインピーダンスミスマッチを吸収し、オブジェクトの世界に留まったまま透過的に永続化操作を行います。EJB コンテナの上でしか動かなかった エンティティBean とは対照的に、POJO をベースとした軽量ライブラリとして広まっていきました。

それでもやはり透過的に扱うことが難しい問題は残り、特有の知識をもった職人が設定をいじくり回すなどして無理に使うという場面が少なくなく、アンチ ORM 派が生まれたりもしています。


ORM で利用するエンティティも、エンティティBean で見たように getter/setter を前提としたものでした。 この規約を元に、リフレクションやバイトコードマニピュレートなどの黒魔術を使うことで、永続化操作をオブジェクトの世界で意識せずに済むように、なんとか頑張るという方向で進化していったのです。


HTTP から Java の世界への入口だけではなく、出口となるデータベースへの永続化時にも getter/setter を備えた Bean が必要となり、流通するフレームワークを利用した開発では、それが普通の事になって行きました。


再利用可能なコンポーネントとしての Java Beans は、より粒度の細かい、コンポーネントと呼ぶにはためらいのある場面にまで getter/setter を強制させるような環境が整っていったのです。


getter/setter は良くない習慣です

以上 見てきたように、getter/setter を備えた Bean は開発の利便性のために導入され、Java の外側の世界との橋渡しのためのエコシステムとして利用が拡大してきました。

Dependency injection による セッターインジェクションなどの影響もあり、 getter/setter を備えた Bean はさらに強大な市民権を得ることになりました。フィールドのアクセスは直接外部に公開するのではなく getter/setter を経由して公開すべしという教えの影響や、IDE によるコード生成であったり、Lombok による自動生成なども、クラスには必ず getter/setter を設けるものだといった風潮が蔓延する原因となっています。


もう一度考えてください。getter/setter はオブジェクトがビジネスロジックを実現するために必要なものかどうかを。これらは開発利便性のためにフレームワークやライブラリにより強制されたものであることを。

現状のエコシステムの上では getter/setter がいわば必然になっていますが、使わざる負えないとしても、getter/setter は良くない習慣だという認識を持った上で、やむ追えず使うという意識が必要なのです。getter/setter が当たり前の世界で暮らしていることが、原始的な手続き的な世界に留まる足かせであることを理解すべきなのです。


なぜ getter/setter は悪なのか

旧来の手続き型の問題を克服するためにオブジェクト指向が出てきたという点は前提とします。

大きく複雑なものを人間が理解するためには、分割して構造化するか、抽象化して扱うかのどちらかになります。細かな市街地地図を見ても九州から北陸までの行路は把握できないのです。テレビのチャンネルを変える時には赤外線パルスの波形や発光ダイオードに流す必要のある電流量を把握している必要はないのです。


オブジェクトに getter/setter があった場合、以下のようなコードが自然だと捉える人がいます。

Car car = new Car();
car.getEngine().getFuelContainer.setFuel(Fuel.litreOf(30));

車がエンジンを持っていて、そこに繋がる燃料容器があり、そこに燃料を設定すれば、まぁ、車は動ようになるかもしれません。しかし考えててください。車を使う人はなぜ車の内部構造まで把握する必要があるのでしょうか?ガス欠に備えてサブの燃料容器を増やしたとしたら、使う側もそれに合わせてどちらのタンクに燃料を入れるのかを把握しておかなければなりません。利用者は車の内部構造をくまなく把握しないと車を利用できないのでしょうか?

これは知識の流出であり、このような考え方はシステムの複雑さを助長する良くない考えです。手続き型の反省から生まれたオブジェクト指向で、手続き型に立ち戻った考えを助長するものです。


車に燃料の残量警告灯があった場合は、以下のように書くべきでしょうか?

Fuel remaining = car.getEngine().getFuelContainer.getFuel();
Fuel total = remaining.plus(Fuel.litreOf(30));
car.getEngine().getFuelContainer.setFuel(total);
if (total.greaterThan(Fuel.litreOf(45))) {
  car.setLowFuelIndicator(false);
}

燃料の追加と残量警告灯の変更を、2つのアクションで外部から制御する必要はあるのでしょうか。残量警告灯を消し忘れたらどうなるのでしょうか。これらを逐次外からの操作で制御すべきなのでしょうか。


あなたが扱うのはオブジェクトなのです。


車に燃料を入れるのであれば以下で良いはずです。

Car car = new Car();
car.refueling(Fuel.litreOf(30));

燃料タンクについて知る必要もなければ、残量警告灯を気にする必要もないのです。それらは車自身の内部の構造で消化すべき問題であり、利用者は意識すべき問題ではないのです。

修理工場で使うアプリケーションの場合は、エンジンや燃料タンクのことを気にするかもしれませんし、どのようなドメインを扱っているのかによって異なる表現になるかもしれません。しかし通常は燃料容器がどこにあるかなどは気にするべきではなく、車に燃料を補給することさえ出来れば良いのです。


getter/setter により内部構造を不用意に露呈することで以下のような問題が生じます。

  • 適切なカプセル化が行われず、変更の影響範囲が拡大する
  • 操作が単一のアクションとして完結せず、不確実な状態のオブジェクトが発生しうる
  • 利用者は、使うオブジェクトの内部構造を熟知しなければならない
  • オブジェクトに対する手続きが、オブジェクトの外側の様々な箇所で行われ、コード(=知識)が分散する

オブジェクトはメソッド付きの単なるデータホルダではありません。オブジェクトはソフトウェアの世界では有機的であり、なんらかのアクションに応じて能動的に振る舞うものであるべきです。絶対的な一人の神が、トランザクションスクリプトの中で、各オブジェクトの詳細を逐一変更して回るべきでは決して無いのです。

getter/setter がある世界で生きていた場合、それを利用してオブジェクトを手続き的に操作することがさも自然であり、疑問を挟む機会を失うことでしょう。getter/setter は、エンジニアにオブジェクト指向的な考え方を行わせないツールであり、最初の開発生産性は向上するかもしれませんが、変更時に歪が露呈する保守性の低いシステムを作る呪縛なのです。


悪の囁きに対する処方箋

getter/setter を前提として栄えてきた現在のエコシステム上では、getter/setter を直ちに抹殺することは難しい部分もあります。純粋世界と現実世界にはまだ隔たりがあるのです。しかしだからと言って流されてはいけません。


今や、ほとんどの ORM には getter/setter は必要ありませんし、public な引数なしのコンストラクタも必要ありません。

getter/setter は定義せず、引数無しのコンストラクタも protected とし、引数ありのコンストラクタで完全な状態のオブジェクトを生成するようにしましょう。

@Entyty
public class Car {
  
  @Id
  private Long id;
  
  @OneToOne(fetch = FetchType.LAZY)
  private Engine engin;
  // ...
  
  protected Car() { }
  
  public Car(
      Engine engin,
      // ...
      ) {
    // ...
  }
}

集約ルートとなるオブジェクトに業務ドメインを色濃く反映した public メソッドを作成し、これを利用者とのインターフェースとして扱います。そしてこの public メソッドは、利用者からの業務的要求を処理するアトミックな操作として定義してください。たいていの場合、この public メソッドの戻り値は void となるはずです。

別の考え方として、ORMから取得するものは単なる DTO として扱い、これを元にしてドメインモデルを構築する方法もありますが、この場合は ORM の存在意義が無いに等しくなり、現実的ではありません。


次に、画面への出力はどうでしょう。ビジネスを実現するために必要な情報を提供する主体がドメインオブジェクトであることは妥当性があります。しかしそう考えた場合、集約ルートになるオブジェクトが、内包するオブジェクトの情報を提供する getter が必要になります。加えて現在の View 構築では EL式などで値を参照するものが大多数であり、この点においても getter の必要性が高まります。

考え方として、業務を実行することと、情報を提供することを、別の概念として分けて扱うことに解決の糸口があります。通常業務としては Car オブジェクトの詳細を知る必要はないが、内包する値の詳細を情報提供しなければならないケースは必ずあります。そして、その詳細の情報は、業務を遂行する立場や役割に応じてそれぞれ必要とする情報が異なるのが通常です。


一つの方法としては、以下のようにオブジェクトの内包する詳細を検査する という概念を導入することです。

public class Car {
  public void inspect(CarInspector inspector) {
    this.engin.inspect(inspector);
    inspector.setSerialNumber(this.serialNumber);
  }
}

これによりオブジェクトの内部構造が変化したとしても、その影響を局所化することができます。さらに、例えば年式に応じてアラームを出す必要があれば、それは CarInspector の責務とすることで柔軟性を得ることもできます。

難点は似たようなフィールドを持つクラスを複数用意する必要がある点で、現実的な割り切りとして getter に限り利用を許容するという考え方もあります。


もう一つの方法としては、業務を実行することと、情報を提供することを、より明確に分離してしまう方法もあります(さらに突き詰めると CQRS の話になりますが、それはここでは触れません)。JPA のコンストラクタ式を使うことで、オブジェクトを直接画面用の VO にマッピングすることができるので、エンティティオブジェクトを介さずに画面用の View オブジェクト(DTO)として扱うことができます。これによりエンティティキャッシュを汚さず、LazyInitializationException の発生を押さえられるという副次的な利点もあります。


最後に、画面からの入力はどうでしょう。Spring MVC では、コンストラクタインジェクションを利用して getter/setter を介することなく、画面からのリクエストをドメインオブジェクトにマッピングすることができます。ちょうど JPA のコンストラクタ式の画面版のような感じになります。

ただし、コレクションとしてネストしたオブジェクトを透過的に扱う場合には getter/setter が要求されるなど、まだまだ完全に透過的に処理することができないため、入力フォームを VO として扱い、ドメインモデルに変更リクエストとして渡すのが妥当な実装になるでしょう(画面別の入力アラートなど、ドメインオブジェクト自体で全てを消化しようとした場合に無理が出てくることが多くあります)。


まとめ

本稿では、 Java Beans により端を発した getter/setter 文化を振り返り、いま一度その是非について検討してきました。いまや意識せずに付いてくる getter/setter は、その存在が手続き型の世界への退行を誘う悪の囁きです。

長く管理するシステムには、開発の初期スピートだけに目を取られることなく、その後の変更を視野に入れた判断が必要であり、もしあなたの扱うシステムに getter/setter があったとしても、それをどのように扱うかは慎重に判断して対応していく必要があります。原始的な手続き型の世界に留まるのではなく、美しいオブジェクトの世界で息をすることを意識すべきなのです。



Object Thinking (Developer Reference) (English Edition)

Object Thinking (Developer Reference) (English Edition)

  • 作者:David West
  • 出版社/メーカー: Microsoft Press
  • 発売日: 2004/02/11
  • メディア: Kindle版