はじめに
こんにちは!TooMeです!抽象化って難しいですよね.
大きいプロジェクトになると抽象化したものに対して抽象化を行なったものの抽象化をしたりしてもうよくわかりません.
この記事は新卒エンジニアの自分が抽象化を超理解するために作成したものです.
抽象化とは??
Wikipediaでは『対象から注目すべき要素を重点的に抜き出して他は捨て去る方法である.』と述べられています.
対義語は具象化ですね.具象化について考えると,抽象化についても理解しやすいと思います.
例えば,英雄太郎くんがいたとして,具象化を進めていくと,東京都新宿区出身で2000年10月2日に生まれた英雄太郎くんになると思います.
まだまだ性別や身体的特徴を追加すればもっと具象化できますよね.
具象化の逆をすると,英雄太郎くんは人間とか脊椎動物になりますね.これが抽象化です.
プログラミングにおける抽象化も同様です.
なぜ抽象化が大事なのか
車やバイクの運転も抽象化されています.
私たちはエンジンの仕組みや燃料噴射のタイミングといった複雑な操作を知らなくても,アクセルやブレーキ,ハンドルといった抽象化されたインターフェースを操作するだけでこれらの運転ができます.
プログラミングもこれと同じで,抽象化をすることによって以下のようなメリットを享受できます.なので抽象化は大事です.
- 複雑さの軽減
大規模なソフトウェアにおいて,抽象化を行うことによって細かい処理のことを知らなくてもプログラムの全体像を把握しやすくなる. - 再利用性の向上
一度作成した関数やクラスを,抽象化された「部品」として様々な場所で再利用することができる.車輪の再発明を避け,開発スピードを向上させることができる. - 保守性の向上
仕様変更や機能追加があった場合,修正箇所を特定しやすくなる.抽象化された部品の内部だけを修正すればいいため,プログラム全体への影響を最小限に抑えることができる. - 生産性の向上
既存の抽象化されたライブラリやフレームワークを使用することで,ネットワーク通信やデータベース操作などの複雑な機能を,その詳細な仕組みを知らなくても簡単に実装することができる.これによって開発者は本来の目的であるアプリケーションのロジック開発に集中することができる.
抽象化の危険
なんでもかんでも抽象化しようとすると,とんでもないくそコードが誕生することがあります.自分が思う抽象化のデメリットを以下に示しました.
1. 可読性と理解の困難
抽象化の層を増やしていった結果,「このコードは具体的に何をしているのか」に到達するのに時間がかかってしまう.
2. 不要な複雑さの追加(YAGNI原則の違反)
You Aren't Gonna Need It(あなたはそれを必要としないだろう)
将来必要になるかもしれない.という予測に基づいて過剰な抽象化を行うと,実際には使用されないインターフェースやクラスが作られてしまい,ただただシステムの構造を不必要に複雑にしただけの負債となってしまう.
3. 間違った抽象化
一度作った抽象化のルール(インターフェース)は,それを実装しているすべての具象クラスに影響を与えてしまうため,変更が困難になってしまう.例えば以下の例があります.
- 例: 動物の抽象クラスとして「4本足で歩く」というルールを定義した.しかし,後から「ヘビ」や「魚」を追加したくなったため,このルールが邪魔になり,無理やりな例外処理や設計の見直しが必要になった.
- 結果: 間違った抽象化によって,新しい機能を追加する際に足かせとなりm柔軟性を著しく低下させた.抽象化しない方がよっぽどマシだったという状況に陥った.
間違った抽象化をしないために
1. まずは具体的に書く
最初から抽象化しようとせず,まずは具体的なコードをそのまま書く.
2. DRY原則
Don't Repeat Yourself(同じコードは避けるべき)
同じようなコードが複数回出てきたら抽象化をしてみる.2回程度なら,まだコピペの方がシンプルな場合もあるのでそこは自分で考えないといけない.
3. YAGNI原則
「今,本当に必要なものか」を考え,将来の予測で設計を複雑にしないようにする.
4. リファクタリングをする
抽象化は最初から完璧にすることは難しいので,コードが育っていく過程で,必要なタイミングで徐々にリファクタリングを挟み,抽象化をしていく.
おわりに
今回は,自分自身の理解を深めるために,プログラミングにおける抽象化というテーマの掘り下げを行いました.付録ではPythonを用いた具体的な抽象化のコードを2つ載せています.ぜひ見ていってください.
付録
抽象化のコード例(Python)
このコードでは,乗り物の共通操作を抽象化し,車とバイクという具体的なも乗り物を作成します.
from abc import ABC, abstractmethod
import math
# ----------------------------------------------------
# 1. 抽象的な「乗り物」を定義する (インターフェース)
# ----------------------------------------------------
# 「乗り物」という抽象的な概念を定義します。
# これから作成する具体的な乗り物(車、バイクなど)が
# 最低限持つべき機能(インターフェース)をここで決めます。
class Vehicle(ABC):
def __init__(self, name):
self.name = name
@abstractmethod
def start_engine(self):
"""エンジンをかける(どうやってかけるかは具体的に決めない)"""
pass
@abstractmethod
def accelerate(self):
"""加速する(どうやって加速するかは具体的に決めない)"""
pass
@abstractmethod
def brake(self):
"""ブレーキをかける(どうやってブレーキをかけるかは具体的に決めない)"""
pass
# ----------------------------------------------------
# 2. 具体的な「車」と「バイク」を定義する (実装)
# ----------------------------------------------------
# Vehicleという抽象的なクラスを継承して、具体的な乗り物を作ります。
# ここで初めて、それぞれの乗り物が「どのように」動くのかを実装します。
class Car(Vehicle):
"""車クラス"""
def start_engine(self):
print(f"{self.name}のエンジンをキーで回して始動させました。")
def accelerate(self):
print(f"{self.name}のアクセルペダルを踏んで加速します。")
def brake(self):
print(f"{self.name}のブレーキペダルを踏んで減速します。")
class Motorcycle(Vehicle):
"""バイククラス"""
def start_engine(self):
print(f"{self.name}のキックスターターを踏んでエンジンを始動させました。")
def accelerate(self):
print(f"{self.name}の右ハンドルのスロットルを回して加速します。")
def brake(self):
print(f"{self.name}のブレーキレバーを握って減速します。")
# ----------------------------------------------------
# 3. 抽象化されたインターフェースを利用する
# ----------------------------------------------------
# この関数は、引数が「車」なのか「バイク」なのかを気にしません。
# Vehicleという「乗り物」の抽象的な型として扱います。
# これにより、この関数は乗り物の種類に依存せず、再利用可能になります。
def drive_vehicle(vehicle: Vehicle):
print(f"\n--- {vehicle.name}を運転します ---")
vehicle.start_engine()
vehicle.accelerate()
vehicle.brake()
print("--------------------")
# 具体的なオブジェクトを作成
my_car = Car("セダン")
my_motorcycle = Motorcycle("アメリカンバイク")
# 同じ関数に、異なる具体的なオブジェクトを渡して実行
# 関数側は中身が車かバイクかを意識する必要がない
drive_vehicle(my_car)
drive_vehicle(my_motorcycle)
実行結果
--- セダンを運転します ---
セダンのエンジンをキーで回して始動させました。
セダンのアクセルペダルを踏んで加速します。
セダンのブレーキペダルを踏んで減速します。
--------------------
--- アメリカンバイクを運転します ---
アメリカンバイクのキックスターターを踏んでエンジンを始動させました。
アメリカンバイクの右ハンドルのスロットルを回して加速します。
アメリカンバイクのブレーキレバーを握って減速します。
--------------------
抽象化の抽象化のコード例(Python)
ここまで来るとさすがにコード量が多いです.抽象化の層ができたことにより,同じオブジェクトをメンテナンスの人はそれをMachineryと認識し,運転する人はVeicleと認識することができるようになりました.
from abc import ABC, abstractmethod
# ----------------------------------------------------
# 1. 最も抽象的な層:「機械 (Machinery)」
# ----------------------------------------------------
# 「エンジンを持ち、メンテナンスが必要」という非常に大きな概念を定義します。
# これが今回の「抽象化の抽象化」の土台となります。
class Machinery(ABC):
def __init__(self, model_name):
self.model_name = model_name
@abstractmethod
def start_engine(self):
"""エンジンを始動する"""
pass
@abstractmethod
def perform_maintenance(self):
"""メンテナンスを実施する"""
pass
# ----------------------------------------------------
# 2. 中間の抽象層:「乗り物 (Vehicle)」
# ----------------------------------------------------
# 次に、「機械(Machinery)」という抽象的な概念をさらに具体的にした、
# 「乗り物(Vehicle)」という抽象クラスを定義します。
# VehicleはMachineryの一種なので、そのルールをすべて引き継ぎます。
class Vehicle(Machinery, ABC): # Machineryを継承している点がポイント
@abstractmethod
def move(self):
"""移動する"""
pass
# ----------------------------------------------------
# 3. 具体的な層:個々の製品
# ----------------------------------------------------
# 「乗り物」のルールに従う、具体的な製品を作ります。
class Car(Vehicle):
def start_engine(self):
print(f"[{self.model_name}] 乗用車のエンジンを静かに始動しました。")
def perform_maintenance(self):
print(f"[{self.model_name}] オイル交換とタイヤチェックを行いました。")
def move(self):
print(f"[{self.model_name}] 乗客を乗せてスムーズに走行します。")
# 同じく「乗り物」のルールに従う、別の製品です。
class Truck(Vehicle):
def start_engine(self):
print(f"[{self.model_name}] トラックのエンジンが轟音とともに始動しました。")
def perform_maintenance(self):
print(f"[{self.model_name}] エンジンと荷台の総点検を行いました。")
def move(self):
print(f"[{self.model_name}] 大量の荷物を積んで力強く走行します。")
# 「乗り物」ではないが「機械」ではある、という製品も作れます。
class Generator(Machinery): # VehicleではなくMachineryを直接継承
def start_engine(self):
print(f"[{self.model_name}] 発電機のエンジンを起動し、電力を供給します。")
def perform_maintenance(self):
print(f"[{self.model_name}] 燃料フィルターの清掃を行いました。")
# ----------------------------------------------------
# 4. 抽象化の層を使い分ける
# ----------------------------------------------------
# 「機械」として一括で管理する関数
# メンテナンス部門の人は、それが車か発電機かを意識する必要はありません。
def manage_asset(asset: Machinery):
print(f"\n--- 資産管理: {asset.model_name} ---")
asset.start_engine()
asset.perform_maintenance()
print("--------------------")
# 「乗り物」として操作する関数
# 運送部門の人は、移動できない発電機のことは考慮する必要がありません。
def dispatch_vehicle(vehicle: Vehicle):
print(f"\n--- 車両配車: {vehicle.model_name} ---")
vehicle.move()
print("--------------------")
# 具体的なオブジェクトを作成
car = Car("クラウン")
truck = Truck("ギガ")
generator = Generator("EG2000")
# ▼「機械」として一括管理できる
# 車、トラック、発電機のすべてを同じ関数で扱える
print("====== メンテナンス部門の業務 ======")
manage_asset(car)
manage_asset(truck)
manage_asset(generator)
# ▼「乗り物」として操作できる
# 車とトラックは配車できるが、発電機は配車できない
print("\n====== 運送部門の業務 ======")
dispatch_vehicle(car)
dispatch_vehicle(truck)
# dispatch_vehicle(generator) #これを実行するとエラーになる(moveメソッドがないため)
出力例
====== メンテナンス部門の業務 ======
--- 資産管理: クラウン ---
[クラウン] 乗用車のエンジンを静かに始動しました。
[クラウン] オイル交換とタイヤチェックを行いました。
--------------------
--- 資産管理: ギガ ---
[ギガ] トラックのエンジンが轟音とともに始動しました。
[ギガ] エンジンと荷台の総点検を行いました。
--------------------
--- 資産管理: EG2000 ---
[EG2000] 発電機のエンジンを起動し、電力を供給します。
[EG2000] 燃料フィルターの清掃を行いました。
--------------------
====== 運送部門の業務 ======
--- 車両配車: クラウン ---
[クラウン] 乗客を乗せてスムーズに走行します。
--------------------
--- 車両配車: ギガ ---
[ギガ] 大量の荷物を積んで力強く走行します。
--------------------