はじめに
抽象化とは、細かい違いをいったん無視して、共通する本質だけを取り出すこと。
今回次のような関係を例に抽象化について学んだことをまとめる
社員
├─ エンジニア
└─ 営業
エンジニアも営業も、職種は違う。
でも、どちらも会社に所属する「社員」。
つまり、共通部分だけを見るとこうなる。
社員番号を持っている
勤怠を入力する
この共通部分を 社員 としてまとめるのが抽象化。
目的
処理の目的はこれ。
エンジニア、営業などの具体的な職種に共通する要素を「社員」としてまとめる。
職種ごとの差分は個別クラスに書き、共通処理は親クラスやinterfaceに集約する。
悪い例:共通部分を各クラスに重複して書く
class Engineer {
private employeeNumber: string;
constructor(employeeNumber: string) {
this.employeeNumber = employeeNumber;
}
inputAttendance(): void {
console.log(`${this.employeeNumber} が勤怠を入力しました`);
}
programming(): void {
console.log("プログラミングをします");
}
}
class Sales {
private employeeNumber: string;
constructor(employeeNumber: string) {
this.employeeNumber = employeeNumber;
}
inputAttendance(): void {
console.log(`${this.employeeNumber} が勤怠を入力しました`);
}
salesActivity(): void {
console.log("営業活動をします");
}
}
何が悪いのか
Engineer と Sales の両方に、同じものがある。
private employeeNumber: string;
inputAttendance(): void {
console.log(`${this.employeeNumber} が勤怠を入力しました`);
}
これは重複。
現場で困るのは、勤怠入力の仕様が変わったとき。
例えば、勤怠入力時にログを追加したい場合、両方を修正する必要がある。
Engineerを修正
Salesを修正
今後Managerが増えたらManagerも修正
クラスが増えるほど修正漏れが起きる。
良い例:共通部分を Employee に抽象化する
// 抽象化:エンジニア・営業に共通する「社員」という概念を作る
abstract class Employee {
// 抽象化:全社員に共通する属性
protected readonly employeeNumber: string;
constructor(employeeNumber: string) {
if (!employeeNumber) {
throw new Error("社員番号は必須");
}
this.employeeNumber = employeeNumber;
}
// 抽象化:全社員に共通する振る舞い
inputAttendance(): void {
console.log(`${this.employeeNumber} が勤怠を入力しました`);
}
// 抽象化:職種ごとの仕事内容は共通化できないので、子クラスに任せる
abstract work(): void;
}
// 具体化:エンジニア固有の振る舞いを書く
class Engineer extends Employee {
work(): void {
this.programming();
}
private programming(): void {
console.log("プログラミングをします");
}
}
// 具体化:営業固有の振る舞いを書く
class Sales extends Employee {
work(): void {
this.salesActivity();
}
private salesActivity(): void {
console.log("営業活動をします");
}
}
抽象化しているコードはどこか
ここ。
これは、Engineer と Sales の共通部分を Employee としてまとめている。
abstract class Employee {
さらにここ。
protected readonly employeeNumber: string;
これは、全社員に共通する「社員番号」を親クラスに持たせている。
さらにここ。
inputAttendance(): void {
console.log(`${this.employeeNumber} が勤怠を入力しました`);
}
これは、全社員に共通する「勤怠を入力する」という処理を親クラスにまとめている。
さらにここ。
abstract work(): void;
これは、全社員は何かしら働くが、具体的な仕事内容は職種ごとに違うため、子クラスに任せている。
どこに何を書くべきか
Employee に書くもの
Employee には、全社員に共通するものを書く。
abstract class Employee {
protected readonly employeeNumber: string;
inputAttendance(): void {
console.log(`${this.employeeNumber} が勤怠を入力しました`);
}
abstract work(): void;
}
書いてよいもの。
社員番号
氏名
勤怠入力
社員として共通するチェック
理由は、エンジニアでも営業でも共通して使うから。
Engineer に書くもの
Engineer には、エンジニア固有のものを書く。
class Engineer extends Employee {
work(): void {
this.programming();
}
private programming(): void {
console.log("プログラミングをします");
}
}
書いてよいもの。
プログラミングする
コードレビューする
設計する
技術調査する
理由は、営業には関係ないから。
Sales に書くもの
Sales には、営業固有のものを書く。
class Sales extends Employee {
work(): void {
this.salesActivity();
}
private salesActivity(): void {
console.log("営業活動をします");
}
}
書いてよいもの。
営業活動をする
顧客訪問する
見積もりを提案する
理由は、エンジニアには関係ないから。
良し悪しの判断基準
良い抽象化
良い抽象化は、共通点だけを親に置く。
abstract class Employee {
protected readonly employeeNumber: string;
inputAttendance(): void {
console.log(`${this.employeeNumber} が勤怠を入力しました`);
}
abstract work(): void;
}
これは良い。
理由は、employeeNumber と inputAttendance() は全社員に共通するから。
悪い抽象化
悪い例。
abstract class Employee {
protected readonly employeeNumber: string;
inputAttendance(): void {
console.log(`${this.employeeNumber} が勤怠を入力しました`);
}
programming(): void {
console.log("プログラミングをします");
}
salesActivity(): void {
console.log("営業活動をします");
}
}
これは悪い。
理由は、programming() は営業には不要で、salesActivity() はエンジニアには不要だから。
Employee に何でも詰め込むと、親クラスが不自然になる。
何を良しとするのか
抽象化で良いと判断する基準はこれ。
| 判断ポイント | 良い状態 |
|---|---|
| 共通する属性か | 親に置く |
| 共通する処理か | 親に置く |
| 一部の職種だけの処理か | 子クラスに置く |
| 親クラス名が自然か | 自然 |
| 子クラスが不要なメソッドを持たされていないか | 持たされていない |
Javaで書く場合
// 抽象化:エンジニア・営業に共通する「社員」という概念
public abstract class Employee {
// 抽象化:全社員に共通する属性
protected final String employeeNumber;
public Employee(String employeeNumber) {
if (employeeNumber == null || employeeNumber.isBlank()) {
throw new IllegalArgumentException("社員番号は必須");
}
this.employeeNumber = employeeNumber;
}
// 抽象化:全社員に共通する振る舞い
public void inputAttendance() {
System.out.println(employeeNumber + " が勤怠を入力しました");
}
// 抽象化:働くこと自体は共通。ただし具体的な仕事内容は子クラスに任せる
public abstract void work();
}
// 具体化:エンジニア固有の処理
public class Engineer extends Employee {
public Engineer(String employeeNumber) {
super(employeeNumber);
}
@Override
public void work() {
programming();
}
private void programming() {
System.out.println("プログラミングをします");
}
}
// 具体化:営業固有の処理
public class Sales extends Employee {
public Sales(String employeeNumber) {
super(employeeNumber);
}
@Override
public void work() {
salesActivity();
}
private void salesActivity() {
System.out.println("営業活動をします");
}
}
現場で意識すること
現場では、抽象化は「きれいに見せるため」にやるものではない。
目的はこれ。
重複を減らす
変更箇所を減らす
共通ルールを1か所に集める
差分だけを子クラスに書く
例えば勤怠入力の仕様が変わったら、ここだけ直せばよい。
inputAttendance(): void {
console.log(`${this.employeeNumber} が勤怠を入力しました`);
}
Engineer や Sales を直さなくてよい。
これが抽象化のメリット。
やってはいけないこと
1. 共通していないものを親に置く
abstract class Employee {
programming(): void {
console.log("プログラミングをします");
}
}
これはダメ。
営業も Employee なので、営業まで programming() を持ってしまう。
2. 何でも親クラスに集める
abstract class Employee {
inputAttendance(): void {}
programming(): void {}
salesActivity(): void {}
accounting(): void {}
manageTeam(): void {}
}
これはダメ。
親クラスが肥大化して、何のクラスなのかわからなくなる。
3. 名前だけ抽象的で中身がバラバラ
abstract class BaseClass {
}
BaseClass のような名前は避けた方がよい。
何を表しているクラスなのかわからない。
良い名前はこれ。
Employee
User
Order
Payment
Notification
業務上の意味がある名前にする。
まとめ
抽象化とは、共通点を抜き出して、違いを隠すこと。
今回の例なら、
エンジニア、営業
↓
共通点を見る
↓
社員番号を持つ
勤怠を入力する
↓
Employee にまとめる
同じコードを書くときは、こう考える。
これは全員に共通するか?
一部だけの処理ではないか?
親クラスに置くことで修正箇所は減るか?
子クラスが不要な機能を持たされていないか?
結論としては、以下。
共通するものは親クラスへ
違うものは子クラスへ
共通していないものを無理に親へ入れない
これが抽象化の基本だと学んだ