なぜ「不正な setter」が問題になるのか
setter 自体は悪ではありません。
ただ「何でもかんでも public な setter を生やす」「ルールも何もなく値を変えられるようにする」と、
オブジェクト指向の根っこである「オブジェクトが自分の一貫性を守る」という前提が崩れます。
不正な setter とは、ざっくり言うと
「そのオブジェクトにとってありえない状態にも、平気で変えてしまえる setter」
のことです。
結果として、
オブジェクトの状態が壊れやすい
どこで壊れたか追いにくい
ドメインルールがどこにも集約されない
というツラさを呼び込みます。
ここから、具体例ベースで噛み砕いて話していきます。
例題で見る「不正な setter」が生む壊れ方
何でも変えられる貧血ユーザ
よくある User クラスを見てみます。
final class User {
private Long id;
private String name;
private String email;
private int point;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public int getPoint() { return point; }
public void setPoint(int point) { this.point = point; }
}
Java一見「ちゃんとカプセル化している」ように見えますが、
この setter たちは何のチェックもせずに、どんな値でも受け入れます。
呼び出し側は簡単にこうできます。
User u = new User();
u.setName(null); // 名前が null
u.setEmail("aaa"); // メールアドレスっぽくない文字列
u.setPoint(-100); // マイナスポイント
Java本来、業務的には
名前は必須
メールアドレスは一定の形式
ポイントは 0 以上、最大 10,000 まで
みたいなルールがあるかもしれません。
でも setter はそのルールをまったく知らないので「何でもどうぞ」と通してしまいます。
つまり、オブジェクトの中に「不正な状態」が簡単に入り込める構造になっています。
これが「不正な setter」の一番分かりやすい問題です。
いつ壊れたか分からない地獄
さらに厄介なのは、どこからでも好き放題 setter が呼べることです。
u.setPoint(100); // Aクラス
...
u.setPoint(-50); // Bクラス
...
u.setPoint(300); // Cクラス
Java「いつ」「どこで」「なぜ」この値になったのかが、
クラスの外のいろんな場所に散ってしまいます。
バグが出たときに、
どこが勝手に setter を叩いたのか
どの順番で値が変わったのか
を追いかけるのは、かなりしんどい作業になります。
不正な setter が壊しているもの(重要な深掘り)
一貫性(不変条件)を守る責任が消える
良いオブジェクトは、「自分が常に正しい状態でいる責任」を持ちます。
例えば、注文クラスなら
明細が 1 件以上ある
合計金額と明細の合計が矛盾しない
状態(NEW, SHIPPED, CANCELED)が整合している
などの「不変条件」があります。
ところが不正な setter があると、こう書けてしまいます。
final class Order {
private List<OrderLine> lines;
private int totalPrice;
public void setLines(List<OrderLine> lines) { this.lines = lines; }
public void setTotalPrice(int totalPrice) { this.totalPrice = totalPrice; }
}
Java呼び出し側で
order.setLines(List.of()); // 明細 0 件
order.setTotalPrice(10_000); // でも totalPrice は適当に 10000 に
Javaといった「矛盾した注文」を平気で作れてしまいます。
本来、Order クラスが「そんな状態は存在してはいけない」と言うべきなのに、
何でも受け入れてしまう setter のせいで、その責任が放棄されている状態です。
ルールの「居場所」が分からなくなる
本来、
ポイント計算のルールは User に
注文の整合性ルールは Order に
のように、「どこを見れば何が分かるか」が明確であるべきです。
しかし不正な setter がある設計では、
データ構造はドメインクラス(User, Order)
ルールはどこかの Service クラスや Util クラス
というふうに分裂してしまいます。
「ポイント仕様が変わったら、どこを直せばいい?」
「キャンセルルールが変わったら、どこを見るべき?」
に対して、コードを見ても答えが返ってきません。
いちいち grep して if を探しに行くことになります。
どう直すかの基本方針:setter をやめて「メソッドにルールを持たせる」
単純に「setter を消せ」と言っているわけではない
大事なのは「値を変えちゃダメ」ではなく
「値の変更を、そのオブジェクトの文脈に沿った操作にする」ことです。
先ほどの User を、ルールを持ったメソッドに書き換えてみます。
final class User {
private final Long id;
private String name;
private String email;
private int point;
User(Long id, String name, String email, int point) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("名前は必須");
}
if (!email.contains("@")) {
throw new IllegalArgumentException("メール形式がおかしい");
}
if (point < 0) {
throw new IllegalArgumentException("ポイントは0以上");
}
this.id = id;
this.name = name;
this.email = email;
this.point = point;
}
void changeName(String newName) {
if (newName == null || newName.isBlank()) {
throw new IllegalArgumentException("名前は必須");
}
this.name = newName;
}
void changeEmail(String newEmail) {
if (!newEmail.contains("@")) {
throw new IllegalArgumentException("メール形式がおかしい");
}
this.email = newEmail;
}
void addPoint(int added) {
if (added < 0) {
throw new IllegalArgumentException("マイナスは不可");
}
int newPoint = this.point + added;
if (newPoint > 10_000) {
newPoint = 10_000;
}
this.point = newPoint;
}
Long id() { return id; }
String name() { return name; }
String email() { return email; }
int point() { return point; }
}
Javaここでは、
名前を変えたい
メールを変えたい
ポイントを増やしたい
という “意味のある操作” ごとに専用メソッドを用意して、その中でルールをチェックしています。
呼び出し側は
user.changeName("山田太郎");
user.changeEmail("yamada@example.com");
user.addPoint(100);
Javaのように、「何をしたいか」がメソッド名として見える形になります。
これが「setter をやめて、意味のある操作メソッドにする」という基本方針です。
setter があっても許されるケースと、その注意点
DTO や外部 I/O 専用のクラス
画面とのやりとり、API のリクエスト/レスポンスだけに使う DTO などは、
「ただのデータ入れ物」と割り切って setter を持たせても構いません。
final class UserRequestDto {
public String name;
public String email;
}
Javaあるいは Jackson などのライブラリでシリアライズ/デシリアライズするために、
引数なしコンストラクタ+setter を要求されることもあります。
ただし、その DTO のままドメインの中心に持ち込まず、
必ず「ルールを持ったドメインオブジェクト」に変換することが大事です。
User toDomain() {
return new User(null, this.name, this.email, 0);
}
JavaDTO に setter があるのはいいとして、
ドメインモデル(User, Order, Account など)側に安易な setter を生やし始めると、一気に不正な状態の温床になります。
ORM の都合で必要になる場合
JPA エンティティなどでは、ライブラリの制約上、
引数なしコンストラクタや setter が必要になることがあります。
その場合でも、
プロテクト/パッケージプライベートにして外部から触らせない
ビジネスロジックの中では極力使わない
実際の操作用には別のメソッド(changeEmail, addPoint など)を用意する
といった工夫で、「ドメイン的に不正な状態になれるルート」を塞いでいくことができます。
まとめ:不正な setter を見たときに自分に問いかけてほしいこと
不正な setter の問題は、
「何でも自由に書き換えられるドア」を開けっぱなしにした結果、
オブジェクトの一貫性も、ルールの居場所も、デバッグのしやすさも失うことにあります。
setter を書く前に、こんなふうに自分に問いかけてみてください。
このフィールドは、どんな値でも本当に入っていいのか
本当はルールがあるのに、それをすっ飛ばしていないか
「こういう操作」という名前付きメソッドにしたほうが自然ではないか
このオブジェクトは、どういう状態なら“必ず正しい”と言えるのか
それに「確かにルールあるな」と思ったら、
そのルールをクラスの中に閉じ込める方向で設計し直してみてください。
