はじめに
現在転職活動中です。面接にて
「オブジェクト指向を意識して開発できますか?エピソードがあれば教えてください」の質問に答えることができませんでした。
原則や概念は研修や本で学びました。読み返せば理解はできます。
しかしながら、説明、言語化できなかったです。(=わかっていない)
自戒をこめて書きます。
オブジェクト指向とは
Googleで検索した結果より引用
現実世界の「モノ(オブジェクト)」のように、データ(属性)とそれを操作する処理(メソッド)をひとまとめにして扱うプログラミングの考え方
実際にやっていたこと
- クラスは作ってた
- クラス内にはメンバ変数やそれに伴うメソッドも定義した
- 作ったクラスをもとにnewでインスタンスを生成したり、フレームワークによっては@AutowiredでDIしたりもした
オブジェクト指向っぽいことはしていたはず。
オブジェクト指向の原則
1.カプセル化
Wikipediaより引用
カプセル化はオブジェクト指向での使用が最も有名であり、そこではフィールドとそれを操作するメソッドをまとめたオブジェクトの内部要素への直接アクセスを制限するためのアクセスコントロールを設けている。
カプセル化は内部要素への直接アクセスを制限し、隠蔽することによって、意図しないデータの設定や、バグを防ぐことができる認識です。
カプセル化なしの場合
public class BankAccount {
public int balance = 1000; // publicなので、どこからも参照できる
}
// 使う側
account.balance = -500; // おかしな値も入れられる
カプセル化ありの場合
public class BankAccount {
private int balance = 1000; // privateで隠す
public void setBalance(int newBalance) {
if (newBalance < 0) {
throw new Exception("マイナス残高は設定できません"); // ここで意図しない設定を防ぐ
}
this.balance = newBalance;
}
}
// 使う側
account.setBalance(-500); // エラーになる
実際にやっていたこと、気づいたこと
私は形だけのカプセル化をしており、使い分けができていませんでした。
全てのクラスを「入れ物」として捉えていました。
-
DTO:Controller から Service へデータを渡す入れ物 -
Form:画面の入出力を扱う入れ物 -
Entity:DB操作をするための入れ物
各メンバ変数はprivateで宣言し、外部から直接参照できないようにしていました。
getter, setter も自動生成で作っていました。
これはカプセル化できていた?と一瞬思いました。
調べていくと役割によってカプセル化の使い分けができていなかったことに気づきました。
| クラス | 「入れ物」 | 理由 |
|---|---|---|
| DTOやForm | OK | データを運ぶことが目的のため |
| Entity | NG | ビジネスルールを適用するため |
DTOやFormは入れ物として使用してOKで、カプセル化は気にしなくていい。
Formはアノテーションでバリデーションを定義することもありました。
同様に、Entityに関しても、DBからの結果などをテーブル形式で格納する入れ物として扱っており、Service側で判定するということをしていました。
本来は、Entity自身で判定する必要がありました。
私が書いていたコード:
// Entity(入れ物)
public class Policy {
private String status;
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}
// Service(判定)
public class PolicyService {
public void cancel(Policy policy) {
if (!"ACTIVE".equals(policy.getStatus())) {
throw new Exception("解約できません");
}
policy.setStatus("CANCELLED");
}
}
ビジネスルールを持つコード:
// Entity(ビジネスルールを持つ)
public class Policy {
private String status;
public void cancel() {
if (!"ACTIVE".equals(this.status)) {
throw new IllegalStateException("アクティブな契約のみ解約可能");
}
this.status = "CANCELLED";
}
}
// Service
public class PolicyService {
public void cancel(Long policyId) {
Policy policy = policyRepository.findById(policyId);
policy.cancel(); // Policyに任せる
}
}
私はこの使い分けができていませんでした。
個人開発でTypeScript使っていましたが、ここでもclassを「入れ物」としてのみ扱っていました。
振る舞いを持たせる発想がなかったです。
2.継承
wikibooksより引用
継承は、既存のクラス(親クラスまたは基底クラス)から新しいクラス(子クラスまたは派生クラス)を作成するメカニズムです。子クラスは親クラスの属性やメソッドを引き継ぎ、それを拡張または変更することができます。これにより、コードの再利用性が向上し、階層構造を形成することができます。
例えば、CarクラスはVehicleクラス(乗物クラス)を継承することで、乗物の基本的な属性とメソッドを再利用あるいは再定義することで差分プログラミングを実現します。
実際にやっていたこと
継承の仕組みは理解して、使っていました。
実際に親クラスでは共通処理を定義し、子クラスでは固有の処理を実装していました。
abstract class Channel {
// 共通処理
public void commonProcess() {
System.out.println("共通処理");
}
// 抽象メソッド
abstract boolean validate();
}
// Channelクラスを継承
class WebChannel extends Channel {
// Web用の判定処理
boolean validate() {
return true;
}
}
継承自体は使えていました。しかし、継承を活かしきれていない箇所もありました。
3.ポリモーフィズム
wikibooksより引用
ポリモーフィズムは、同じ名前のメソッドが異なるクラスで異なる振る舞いをすることを可能にする概念です。ポリモーフィズムは静的なもの(コンパイル時ポリモーフィズム)と動的なもの(ランタイムポリモーフィズム)があり、それぞれメソッドのオーバーロードやオーバーライドを通じて実現されます。
実際にやっていたこと、気づいたこと
直近携わったプロジェクトでは、ポリモーフィズムを使ったコードがありました。
同じメソッド名で、継承先のクラスごとに違う処理を作っていました。
abstract class Channel {
// 抽象メソッド
abstract boolean validate();
}
// Channelクラスを継承
class WebChannel extends Channel {
// Web用の判定処理
boolean validate() { return true; }
}
class MobileChannel extends Channel {
// Mobile用の判定処理
boolean validate() { return false; }
}
しかしながら、全体をみると、ポリモーフィズムで判定してたり、ifで判定していたり、混在していました。
if ("WEB".equals(type)) {
// Web用の処理
} else if ("MOBILE".equals(type)) {
// Mobile用の処理
}
そして、私自身も責務を分けずにif文で判定して実装していたところがありました。
おわりに
オブジェクト指向を意識して開発していますか?と聞かれた場合、
あらためて各原則を理解しなおしたことで、自分が意識できていなかったことが明確になりました。
Entityを作る際はビジネスルールを適用し、サービス側で判定しないようにする。
ポリモーフィズムの使いどころを意識した上で、判定処理を作る際に活かした実装をしたいと思います。
転職活動、引き続き頑張ります!
参考資料
ビジネスルールについて