はじめに
社内で抽象化についての勉強会があり、どうしても勉強会の時間だけでは伝えきれない部分があるなと感じました。
少し自分なりに抽象化について思うところを整理して記事にできればいいなと考えて今回の記事を書きました。
抽象化について勉強したいという人が実際に勉強してみたけれども具体的にどういう意味があるのか分からないという方の助けになればと思います。
抽象化のない世界
一例として帳票を出力する処理について考えます。
この処理の中では見積書や請求書や領収書を出力したいですが、見積書の出力に必要な項目と請求書に必要な項目は違います。
実際に実行はしてないので実際に実行するとエラーはあるかもしれません…
あくまでもイメージです
抽象化を行わない前提で処理を実現しようとすると下記のようなコードになるかと思います。
--------------------帳票のクラス群------------------------------
class Mitumori {
public createMitumori(Map<String, String> printData) {
// 見積用のなにがしかの処理
}
public print(Map<String, String> printData) {
this.createMitumori(printData);
// 出力するためのなにがしかの処理
}
}
class Seikyusyo {
public createSeikyu(Map<String, String>printData) {
// 請求用のなにがしかの処理
}
public print(Map<String, String> printData) {
this.createSeikyu(printData);
// 出力するためのなにがしかの処理
}
}
class Ryousyuusyo {
public createRyousyu(Map<String, String>printData) {
// 領収用のなにがしかの処理
}
public print(Map<String, String> printData) {
this.createRyousyu(printData);
// 出力するためのなにがしかの処理
}
}
--------------------帳票出力のクラスから抜粋------------------------------
// 帳票出力処理
public print(String printType, Map<String, String> printData) {
if ("見積書".equals(printType)) {
Mistumori mitumori = new Mitumori();
mitumori.print(printData);
} else if ("請求書".equals(printType)) {
Seikyusyo seikyusyo = new Seikyusyo();
seikyusyo.print(printData);
} else if ("領収書".equals(printType)) {
Ryousyuusyo ryousyuusyo = new Ryousyuusyo();
ryousyuusyo.print(printData);
}
}
帳票出力の処理はMitumori,Seikyusyo,Ryousyuusyoの3クラスに依存していると考えます。
そのため、どれかのクラスに修正が入った時、帳票出力処理に影響を与える可能性が出てきます。
そんなことないだろうと思うかもしれませんがそもそもこの後処理を書く人もこの書き方を守ってくれる保証はないうえ、他の処理に影響を出すように帳票出力処理を書く人も現れるかもしれません。
そもそもクラス作ってくれないこともありえます
抽象化したい理由
本来は帳票出力処理の都合に合わせて各帳票出力の処理を作ってほしいところです。各帳票の出力処理が帳票出力処理に依存して作られていれば帳票出力処理に影響を与えないように処理を書くことができます。
抽象化が存在する世界
最初に上げた例の中に抽象化を行うクラスを追加します。そのうえで、各帳票の出力処理は抽象化したクラスを継承して作るようにします。
--------------------帳票のクラス群------------------------------
// 帳票出力処理を抽象化したクラス
abstract class Report {
private abstract create(Map<String, String> printData) {}
public print(Map<String, String> printData) {
this.create(printData);
// 出力するためのなにがしかの処理
}
}
class Mitumori extends Report {
private create(Map<String, String> printData) {
// 見積用のなにがしかの処理
}
}
class Seikyusyo extends Report {
private create(Map<String, String>printData) {
// 請求用のなにがしかの処理
}
}
class Ryousyuusyo extends Report {
private create(Map<String, String>printData) {
// 領収用のなにがしかの処理
}
}
--------------------帳票出力のクラスから抜粋------------------------------
// 帳票出力処理
public print(String printType, Map<String, String> printData) {
Report report;
if ("見積書".equals(printType)) {
report = new Mitumori();
} else if ("請求書".equals(printType)) {
report = new Seikyusyo();
} else if ("領収書".equals(printType)) {
report = new Ryousyuusyo();
}
report.print(printData);
}
抽象化を行うことで変わった点として。各帳票の出力処理が帳票出力処理に依存して作られているため帳票出力処理そのものへの影響が少なくなっています。
また、各帳票間で同じように使用しているprintメソッドは統一して使用されている部分を共通化して使用することが可能です。抽象化を使ってない場合だとその共通部分が帳票出力処理に漏れ出したりしていろんな場所にいろんなコードが書かれるようになってしまいます…
本来はこの振り分けの部分をFactoryクラスとして切り出すことで帳票出力処理のみに専念させる方がいいと思っています。このクラスは帳票の振り分けと出力の2つの仕事をこなしていますが、振り分け処理を別クラスに任せて単純に帳票を出力することだけをになうクラスとして存在していれば帳票出力の処理は完全に影響を受けない形になります
終わりに
抽象化については動物を例に出したりするものをよく見ますが、具体的に仕事でどう活かすのかが分かりづらいなと考えていたため今回は帳票出力を例にとって書いてみました。
ただ、抽象化は理解するまではそこまで積極的に使用しなくても良いのかなとは思っています。経験上、抽象化していてもそれを見た後続の人が意思をくみ取ってくれないこともあり完全無視のコードを書き始めてより訳の分からないコードを生んでしまうこともあります。
抽象化を見たときに意思をくみ取ってコーディングしていくことは必要だと思いますので考え方だけでも伝わればと思います。