オブジェクト指向って結局何が嬉しいかわかりづらくない?
新人研修のjava研修とかでもオブジェクト指向は習った気がしたが、当時はあまりピンときていなかった。
というのも、カプセル化、継承、ポリモフィズムなどの用語と概念、実装法はなんとなくわかっても、なんのためにそれらが存在するのかに関しては全く理解できていなかった。(メリットに関する説明は研修でも少なかった気がする)
いまでも全てを理解してるとは全く思えてないが、少なくともこんな嬉しさがあるんじゃないか?という学びを少しずつ共有していくシリーズにしたい。
あらためて、オブジェクト(クラス)とは
オブジェクト指向におけるクラスは、
・インスタンス変数(属性)
・メソッド(ふるまい)
から構成されるのが基本である。
これの何が嬉しいの??
”インスタンス変数のルールをメソッド内に記述する”ことでメリットを享受できる
オブジェクト指向において(というかメリットを享受するために)重要なポイントはインスタンス変数のルールをメソッド内に記述することである。
※ここでいうルールとは、「age(年齢)は0以上の整数である」みたいなやつ
こうするとなぜ嬉しいのか??
結論
1. 不正な値の混入を防ぐことができる(保守性を高める)
2. ロジックの散在を防ぐことができる(凝集度を上げ、変更容易性を高める)
からである。
抽象的な表現になってしまったので、具体例をふまえてより詳細にみていく↓↓
1. 不正な値の混入を防ぐことができる
あるシステムのUserクラスがあるとする。
属性と、コンストラクタ、setter/getterがあるとして、こんな感じになる。
class User{
//属性
private int age;
private String name;
//コンストラクタ
public User(int age, String name){
this.age = age;
this.name = name;
}
//getter
public int getAge(){
return this.age;
}
public String getName(){
return this.name;
}
//setter
public void setAge(int age){
this.age = age;
}
public void setName(String name){
this.name = name;
}
}
こんな感じになる。
しかし、このクラスはインスタンス変数のルールをメソッド内に記述することができていない。
この状況の何がまずいか??例えばこんな実装ができてしまう↓
User user = new User(-100, null);
System.out.println(user.getAge()); //出力結果:-100
System.out.println(user.getName()); //出力結果:null
年齢が-100歳で名前がないUserが誕生してしまう。。
このことにきづかずDBにおかしなデータを入れ込んでしまったり、思わぬところでバグを発生させてしまうリスクを埋め込んでしまっている。。。
こういったことを防ぐためにコンストラクタにインスタンス変数のルールを記述する!
今回は
age → 0以上の整数
name → null以外の文字列
というルールであるとする。
class User{
//~~省略
public User(int age, String name){
if(age < 0){
throw new IllegalArgumentException("年齢が0以上でありません");
}
if(name == null){
throw new NullPointerException("名前を設定してください");
}
this.age = age;
this.name = name;
}
//~~省略
}
引数でわたってきた年齢(age)の値がマイナスの時や、名前(name)の値がnullの時は例外を投げるような処理を追加している。
このことにより、不正な値をもったUserクラスの存在を防ぎ、DBにおかしなデータを入れ込んでしまうリスクや、思わぬところでバグを発生させてしまうリスクを回避している。
コンストラクタだけでなく、setterも修正
public void setAge(int age){
if(age < 0){
throw new IllegalArgumentException("年齢が0以上でありません");
}
this.age = age;
}
public void setName(String name){
if(name == null){
throw new NullPointerException("名前を設定してください");
}
this.name = name;
}
値を変更する際につかうメソッドにおいても入力チェックをかますことで、不正値の混入を防ぐことができる。
2. ロジックの散在を防ぐことができる
Userクラスが属性と、コンストラクタ、setter/getterだけのデータクラス(つまりインスタンス変数のルールをメソッド内に記述されていない状態)だったとしても、Userクラスの外で値を設定する前に入力チェックを行えばいいのでないか?と思われた方もいるかもしれない。
例えばUserクラスをつかって、ユーザーを新規作成処理があるとする
//ユーザー新規作成処理
public void CreateUser(int age, String name){
if(age < 0){
throw new IllegalArgumentException("年齢が0以上でありません");
}
if(name == null){
throw new NullPointerException("名前を設定してください");
}
User user = new User(age, name);
//略
}
Userクラスの外で、name,ageのルールのチェックをしている。
たしかに局所的にはこのコードは全く問題ない。
しかし次に、ユーザー情報を更新する処理も追加でつくることになったとする。
//ユーザー情報更新処理
public void UpdateUser(int age, String name){
if(age < 0){
throw new IllegalArgumentException("年齢が0以上でありません");
}
if(name == null){
throw new NullPointerException("名前を設定してください");
}
user.setAge(age);
user.setName(name);
}
更新処理の方にも、Userクラスに値を設定する前の入力チェックが必要となるので、新規追加処理と同じ処理がかかれ、コードは重複することになる。
これのなにがまずいか??
このようなコードの重複が引き起こす弊害は、仕様変更が必要となった時に表れる。
たとえば名前(name)のルールとして、nullを不許可にするだけでなく、”3文字以上の文字列”というルールを追加するという仕様変更があったとする。
nameのルールをUserクラスのメソッド内に記述せず、新規作成処理、更新処理に書いていた場合、書き換える箇所が新規作成処理、更新処理の最低2箇所。
//ユーザー新規追加処理
public void CreateUser(int age, String name){
if(age < 0){
throw new IllegalArgumentException("年齢が0以上でありません");
}
if(name == null){
throw new NullPointerException("名前を設定してください");
}
//New!
if(name.length() < 3){
throw new IllegalArgumentException("名前は3文字以上で設定してください");
}
User user = new User(age, name);
//略
}
//ユーザー情報更新処理
public void UpdateUser(int age, String name){
if(age < 0){
throw new IllegalArgumentException("年齢が0以上でありません");
}
if(name == null){
throw new NullPointerException("名前を設定してください");
}
//New!
if(name.length() < 3){
throw new IllegalArgumentException("名前は3文字以上で設定してください");
}
user.setAge(age);
user.setName(name);
}
今後の拡張によってはより多くの箇所(Userクラスを更新するすべての箇所)を修正しなければならない。
つまり仕様変更の際に修正しなければならない箇所の数が跳ね上がり、変更コストが高くなる。(このようなソースコードを変更容易性が低いという。)
一方、Userクラスのメソッド(コンストラクタやセッター)内にルールを記述している場合は、ルールの変更に対するコードの変更箇所がUserクラス内で閉じるようになる。
class User{
//略
public User(int age, String name){
if(age < 0){
throw new IllegalArgumentException("年齢が0以上でありません");
}
//ここを追加するだけで良い
if(name.length() < 3){
throw new IllegalArgumentException("名前は3文字以上で設定してください");
}
if(name == null){
throw new NullPointerException("名前を設定してください");
}
this.age = age;
this.name = name;
}
新規作成処理や更新処理は以下のような処理となり、nameのルールの変更による修正は発生しない。
//ユーザー新規追加処理
public void CreateUser(int age, String name){
User user = new User(age, name);
//略
}
//ユーザー情報更新処理
public void UpdateUser(int age, String name){
user.setAge(age);
user.setName(name);
}
このように属性と属性に対するロジックがクラス内にまとめられているコードを凝集度が高いといい、変更容易性を確保できる。
【発展】”インスタンス変数のルールをメソッド内に記述する”ことで、業務ルールをオブジェクトに埋め込む
というところまで書きたかったんですが時間切れで書けませんでした。また次回以降やります。
過去記事も参照してみてください。
https://qiita.com/shuma_horii/private/c450c9dc5db616789690
また今回の話を言い換えると、各々のオブジェクトは「自身の属性を正常に制御する」という単一の責任を負っているともみることができる。
※以下記事参照
https://qiita.com/MinoDriven/items/76307b1b066467cbfd6a
この辺りはまた続編で、、
まとめ
結論、「インスタンス変数のルールをメソッド内に記述する」つまりそのクラスのルールはクラス内に書く、ことで高凝集なクラスとなり、保守性、変更容易性を確保できる。
「データとそのデータに関するロジックをひとつのクラスにまとめておける」というオブジェクト指向ならではの嬉しさである。
参考記事&書籍