3
0

もうGetter/Setterを使うのはやめよう(feat. cleanCode)

Last updated at Posted at 2023-11-12

会社で開発をしていた途中、ふとsetterメソッドについて疑問が湧きました。
変数を宣言する際、外部からのアクセスを防ぐため、修飾子をpriaveteで設定したのに
その変数のsetterメソッドを定義するのは変な書き方ではないか?という疑問でした。

また、setterメソッドを使用しないのであれば、世の中のエンジニアはどうやって値を操作しているのかが気になり始めました。

なので、今回はオブジェクト指向の原則を守りながら、プログラミングができるようになるため、getterとsetterメソッドについて調べていきたいと思います。

getter, setterメソッドを使う理由

オブジェクト指向の原則の一つは情報隠蔽(Information Hiding)です。オブジェクトのプロパティを外部からアクセスできないようにするためです。
上記の理由でJavaはすべてのプロパティをprivateで隠し、publicメソッド(getter,setter)を通じて間接的にプロパティを操作する。
これらはセキュリティを強化し、メンテナビリティの高いコードが書ける長所があります。

なぜ、getterとsetterを使うな!というのか

せっかくプロパティをprivateで外部からのアクセスを防いだのに、getterとsetterをpublicにするのは、情報隠蔽の効果が期待できません。
プロパティをは隠してもgetterとsetterでそのプロパティの値を自由に参照または修正できるのは、
果たして良いソースコードだと言えるのでしょうか?

setterを使ってはいけない理由

setterを使うと、必要に応じてプロパティの値を自由に修正でき、楽そうに見えますが、実際にこういう操作は良いソースコードではありません。

理由① setterはその意図・目的を把握しずらい

class Member {
    private String userId;
    private String password;
    private String email;

    public void setPassword(String password) {
        this.password = password;
    }
    public void setEmail(Long email) {
        this.email = email;
    }
}

Member member = MemberRepository.findById(userId).orElseThrow();
member.setPassword("123123")
member.setEmail("xx@gmail.com")

このコードは既存会員のパスワードとEmailを更新する処理です。

member.setPassword("123123")
member.setEmail("xx@gmail.com")

しかし、このコードだけ見ると、新規会員を登録する処理か既存会員の情報を更新する処理か前処理を追って確認しなければ分からないはずです。

このようにsetterを使うとプロパティの値を変更した理由が分からなくなります。

オブジェクトの一貫性の維持ができない

class Address {
    private String city;

    public Address(String city) {
        this.city = city;
    }

    public void city(String city) {
        this.city = city;
    }
}

Address address = New Address();
address.setCity("東京");

User user1 = new User("田中", address);

// addressのcityを"東京"=>"大阪"に変更
address.setCity("大阪");
User user2 = new User("佐藤", address);

System.out.println("user1の都市:" + user1.getAddress().getCity()); // user1の都市:大阪
System.out.println("user2の都市:" + user2.getAddress().getCity()); // user2の都市:大阪

このコードは各ユーザを生成し、そのユーザが住んでいる都市を出力しています。user1は東京・user2は大阪でセットしたはずですが、実際はとどっちも大阪と出力されます。
class Address{...}のsetterによってprivateであるcityを外部から修正できるようになります。
このため、user1の値も一緒に修正され、Side Effectが発生してしまいました。

getterを使ってはいけない理由

setterを使ってはいけない理由ははっきり分かると思います。
しかし、単純に値を参照するgetterは、なぜ、使ってはいけないでしょうか?

getterの戻り値を参照するだけではない

単純にプロパティの参照が目的でgetterを使う場面もありますが、
そのプロパティの値を用いて、分岐などのビジネスロジックを行い場面も多いかと思います。

public void withdraw(long accountId, Long amount) {
    Account account = accountRepository.findById(accountId).orElseThrow();
    Long amount = account.getAmount() - amount;
 
    if (newBalance < 0) {
        throw new IllegalArgumentException("残高不足です。");
    }
 
    account.setBalance(newBalance);
}

このコードではaccountオブジェクトから現在高を参照し、引出した後の残高が0円以上なのかを確認しています。
普段私が書くコードと似た感じです。

しかし、getterで現在高を取得し、外部でビジネスロジックを行うのは少し変な書き方だと思います。

コードを変更した際の脆弱性

@Getter
class Customer {
    private long charge;
}
 
@Service
public class CustomerService {
    ...
    
    public void printMembershipGrade(Customer customer) {
        if (customer.getCharge() >= 100000) {
            System.out.println("Gold");
        } else if (customer.getCharge() >= 50000) {
            System.out.println("Silver");
        } else {
            System.out.println("Bronze");
        }
    }
    
    ...
}

このコードは顧客のガス代によって会員のランクを出力しています。

コードには問題がなさそうですが、以降仕様がガス代の代わりに、使用量と単価で変わるとどうなるでしょうか?

@Getter
class Customer {
    private long usages;
    private long unitPrice;
}

CustomerServiceではCusutomerの変更を分かりません。存在しないプロパティを参照しているため、コンパイルエラーが発生してしまします。

問題はこれだけではありません。既存ソースでgetCharge()を参照しているコードを全部修正しなければいけないです。
もし、参照箇所が100を超える場合なら、大変なことになりでしょう。

上記の2つの理由はすべて外部からgetterを使ってビジネスロジック行っているからです。

では、Getter・Setterなしに、参照・修正はどうば良い?

今まではGetter・Setterの脆弱性について調べました。
しかし、開発をする上で、参照・修正は欠かせないものです。

では、Getter・Setterを使用せずに、参照・修正ができる良いコードについて調べていきましょう。

Setter

明確な意図を持つメソッドを使うこと

class Member {
    private String userId;
    private String password;
    private String email;

-    public void setPassword(String password) {
-        this.password = password;
-    }
-    public void setEmail(Long email) {
-        this.email = email;
-    }
+    public void updateMember(String password, String email) {
+        this.password = password;
+        this.email = email;
+    }
}

Member member = MemberRepository.findById(userId).orElseThrow();
- member.setPassword("123123")
- member.setEmail("xx@gmail.com")
+ member.updateMember("123123", "xx@gmail.com")

↑のコードはもともとsetterだけ使ってプロパティを変更していました。
setterを削除し、updateMemberメソッドを追加することでメソッドの役割は明確になりました。
簡単なコードを例としましたが、このように

コンストラクタを使うこと

class Address {
    private String city;

-    public Address(String city) {
-        this.city = city;
-    }
-    public void city(String city) {
-        this.city = city;
-    }
+    public Address(Striing city) {
+    this.city = city;
+    }
}

- Address address = New Address();
- address.setCity("東京");
+ Address address1 = New Address("東京");

- User user1 = new User("田中", address);
+ User user1 = new User("田中", address1);

// addressのcityを"東京"=>"大阪"に変更
- address.setCity("大阪");
+ Address address2 = New Address("大阪");

- User user2 = new User("佐藤", address);
+ User user2 = new User("佐藤", address2);

System.out.println("user1の都市:" + user1.getAddress().getCity()); // user1の都市:東京
System.out.println("user2の都市:" + user2.getAddress().getCity()); // user2の都市:大阪

↑コードはsetterを使ってプロパティを変更していました。同じオブジェクトを共有することでuser1のプロパティに影響を与え、side Effectが発生しました。
setterを削除することで、オブジェクトの変更を防ぎ、意図通りソースコードが実行できます。

Builderパターン
この記事ではないですが、Builderパターンを使えば、コンストラクタより可読性が高いコードが書けます。

Getter

オブジェクトにメッセージを送り、結果を返すようにすること

「すべてのプロパティにgetterを生成し、その戻り値を使って外部でビジネスロジック行う。」
こういうコードはオブジェクトの中でプロパティを変更するのではなく、外部から意図していない変更ができる脆弱性があります。

↓のコードのビジネスロジックをオブジェクトが処理するように変更します。

@Getter
class Customer {
    private long usages;
    private long unitPrice;

+    public String getMembershipGrade() {
+        long charge = usages * unitPrice;
+        
+        if (charge >= 100000) {
+            return "Gold";
+        } else if (charge >= 50000) {
+            return "Silver";
+        } else {
+            return "Bronze";
+        }
+    }
}
 
@Service
public class CustomerService {
    ...
    
-    public void printMembershipGrade(Customer customer) {
-        if (customer.getCharge() >= 100000) {
-            System.out.println("Gold");
-        } else if (customer.getCharge() >= 50000) {
-            System.out.println("Silver");
-        } else {
-            System.out.println("Bronze");
-        }
-    }
+    public String getMembershipGrade(Customer customer) {
+        return cosutomer.getMembershipGrade();
+    }
    ...
}

会員のランクを計算し、返す処理をCustomerの中で行われるように修正しました。
この修正で、Customerの仕様の変更があっても、Customerを修正するだけ、
クライアントであるCustomerServiceには影響はなくなりました。

Getterを絶対使うな!!というわけではありません
出力のための値などにプロパティを使うのならある程度Getterを使うのは大丈夫です。

まとめ

Setter

  • Setterを使うよりは意図が分かるメソッドを作成し、その中で処理を行うこと。

Getter

  • 出力などの単純参照ならGetterを使ってもOK
  • Getterの戻り値を使って結果を出す際は、外部ではなく、そのオブジェクトでロジックを行って結果を返すこと。

以上で、GetterとSetterを使ってはいけない理由とその対策について調べてみました。

今回の記事を通じて、疑問がかなり解消されました。
Setterについてはある程度知っていましたが、Getterについては初めての分かることもたくさんあっていい勉強になったと思います。

最後まで読んでいただき、ありがとうございました。

参考サイト

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0