Javaでしばしば議論となる、getter/setterの必要性について考えてみました。
getter/setterとは?
いわゆるアクセサというもので、オブジェクト(インスタンス)中のプライベート変数にアクセスするためのメソッドです。
public class User {
// 外部からアクセスしたいフィールド
private String name;
// getter
public String getName() {
return name;
}
// setter
public void setName(String name) {
this.name = name;
}
}
User user = new User();
System.out.println(user.getName()); // -> null
user.setName("hello!");
System.out.println(user.getName()); // -> hello!
使いどころ
読み取り専用/書き込み専用にしたい場合
例えば、IDの払い出しは別クラスから受けるので、その値を変更されたくない場合。
public class User {
// 外部からアクセスしたいフィールド
private long id;
// インスタンス生成時にIDを自動生成する
public User() {
this.id = UserManager.allocateId();
}
// getter
public long getId() {
return id;
}
// 値の変更はされたくないので、setterは定義しない
}
例では、インスタンス生成時にもれなくUserManager.allocateId()
という仮想関数によって、重複のないIDの払い出しを受けています。これを変更されては、IDの一意性を保てなくなりますので、setterを記述しないことでReadOnly属性を表現しているのです。
ところで、JavaではReadOnlyにしたいときに使えるfinal修飾子があります。これを使うのはどうでしょうか?
public class User {
// publicにする
public final long id;
// インスタンス生成時にIDを自動生成する
public User() {
this.id = UserManager.allocateId();
}
}
idの値を外部から変更しようとした場合にコンパイルエラーにはなるものの、こうすれば外部から値を書き換えられる心配はありません。(リフレクションなどは除く)
どちらを好むかは人それぞれです。
setterに処理を付加するとき
setterにバリデータなどを付加したいときがあります。
public class User {
private int age;
public setId(int age) {
if(age < 0) {
throw new IllegalArgumentException("invalid age");
}
this.age = age;
}
}
年齢の範囲を規定します。マイナス歳という年齢はありませんから、バリデーションで予め弾くように設定します。
フレームワークやライブラリの仕様に拘束される場合
フレームワークやライブラリには、フィールドにアクセサを付けることが必要なものもあります。
C#の例
C#では、getter/setterを簡単に記述できます。
public class User {
public string name { get; set; }
}
User user = new User();
user.name = "abc";
System.Console.WriteLine(user.name); // -> abc
非常にスマートです。
getter/setterは必要なのか
これまで述べた利点を考えると、非常に便利な代物に見えます。いったい何が問題なのでしょうか。
むやみにgetter/setterを付与してはならない
「後々使うから」getter/setterをどのフィールドに対しても付けることが習慣化されてしまっているプログラマは少なくありません。
しかし、オブジェクト指向的に考えると、フィールドに無制限にアクセスを許すgetter/setterメソッドが優れているとは思えないのです。
カプセル化の誤解
フィールドがprivateだからカプセル化できているというのは大変な誤解です。publicなアクセサメソッドを通じて直接の値変更が可能なわけですから、それはただのpublic変数と同じです。
とはいえ、MVCモデルを通じてWebアプリケーションの開発を行っていると、DTOのような構造体に似たクラスが必要になってくるかもしれません。
そのとき、フィールドだからprivateにしないといけない!privateだからgetter/setterが必要だ!と思考停止しないでほしいのです。
Seaser2、PlayFrameworkのアプローチ
さて、ここでフレームワークの話が出てきます。Seaser2やPlayは、他に比べても比較的モダンなフレームワークです。
これらは、O/Rマッパーに対応するModelクラスのフィールドをpublicで定義することを仕様としています。なぜでしょうか?
実際の話、getter/setterの定義は、煩雑かつ冗長であり、読みづらいのです。
また、前述したように、DTOやModel、Entityなどの「入れもの」クラスは、全てのフィールドが外部からアクセスされることが前提ですから、アクセサを定義する必要がないということもポイントです。
「どうせ、外部からしか触らないし、アクセス制限もしない。それなら、全てpublicで開いてしまえば、コード量が1/3以下になるし、断然楽だ!」
…というのが、現代的な考え方ですね。
必要なときを見極める
本当に必要なときを見極めるのが大切です。
しかし、オブジェクト指向的に設計を行うとき、純粋なgetter/setterが必要になるケースはそこまで多くありません。商品の情報を格納するオブジェクトを作ってみましょう。
仕様
- オブジェクトはimmutableである。
- 税込価格を取得できる。
public class Product {
// 税率
private static final double TAX_RATE = 1.05;
// 単価
private final double price;
// 価格を設定するコンストラクタ
public Product(int price) {
this.price = price;
}
// 税込価格を返すメソッド
public int priceWithTax() {
return (int)(price * TAX_RATE);
}
public int priceWithoutTax() {
return (int)price;
}
}
このクラスに単純なgetter/setterは必要でしょうか?
まず、オブジェクトはimmutableなので、setterは必要ありません。残るはpriceを生の値で返すgetterですが、これもget○○という命名では不適切です。priceは、税込と税抜が考えられますので、priceWithTaxまたはpriceWithoutTaxとしました。
さて、こうなるとJavaBeansなどの命名規約では不適切ということが分かります。
オブジェクト指向に則ると、単なるgetter/setterは不要なケースが意外と多いことに気付くでしょう。
まとめ
- 必要を見極め、極力getter/setterは付与しない方法を考える。
- 単なるDTOは、public変数で宣言する。
- フレームワークやライブラリ他で必要に迫られた場合は、遠慮なく使う。
プログラムは柔軟な考え方で設計しましょう!