第22章: OO から FP への移行 — 6言語統合ガイド
1. はじめに
オブジェクト指向(OO)から関数型プログラミング(FP)への移行は、一夜にして完了するものではありません。段階的に、安全に、既存のコードベースを壊さずに移行する戦略が必要です。本章では、6 言語それぞれのマルチパラダイム度合いに応じた移行戦略を比較します。
2. 共通の本質
OO と FP の対応関係
| OO 概念 |
FP 等価物 |
| クラス |
データ型(struct / record / case class) + 関数 |
| メソッド |
関数 |
| 継承 |
ADT / 判別共用体 + パターンマッチ |
| インターフェース |
型クラス / trait / プロトコル |
| 可変状態 |
不変データ + 状態変換関数 |
| デザインパターン |
高階関数 / パターンマッチ / 関数合成 |
移行の 4 フェーズ
Phase 1: 新規コードを FP で書く(Strangler Fig の初期段階)
Phase 2: 純粋関数を抽出し、副作用を境界に集約
Phase 3: 可変状態を不変データ構造に置き換え
Phase 4: OO パターンを FP パターンに置き換え
3. 言語別実装比較
3.1 マルチパラダイム度合い
| 言語 |
OO サポート |
FP 度合い |
移行の容易さ |
| Scala |
完全(class / trait / 継承) |
高い(case class / match) |
最も容易 |
| F# |
.NET のクラス利用可能 |
高い(判別共用体) |
容易 |
| Rust |
trait ベース(継承なし) |
中〜高(enum / match) |
中程度 |
| Clojure |
Java 相互運用 |
高い(データ中心) |
データ変換が容易 |
| Elixir |
なし(モジュールベース) |
高い(パイプライン) |
概念の転換が必要 |
| Haskell |
なし(純粋関数型) |
最高 |
全面的な書き換え |
3.2 クラスからデータ + 関数へ
OO スタイル → FP スタイルの変換(全言語)
**OO(Java 風):**
class BankAccount {
private double balance;
public void deposit(double amount) { balance += amount; }
public void withdraw(double amount) { balance -= amount; }
public double getBalance() { return balance; }
}
**Clojure:**
(defn make-account [balance] {:balance balance})
(defn deposit [account amount]
(update account :balance + amount))
(defn withdraw [account amount]
(update account :balance - amount))
**Scala:**
case class BankAccount(balance: Double):
def deposit(amount: Double): BankAccount = copy(balance = balance + amount)
def withdraw(amount: Double): BankAccount = copy(balance = balance - amount)
**Elixir:**
defmodule BankAccount do
defstruct [:balance]
def new(balance), do: %__MODULE__{balance: balance}
def deposit(account, amount), do: %{account | balance: account.balance + amount}
def withdraw(account, amount), do: %{account | balance: account.balance - amount}
end
**F#:**
type BankAccount = { Balance: float }
let deposit amount account = { account with Balance = account.Balance + amount }
let withdraw amount account = { account with Balance = account.Balance - amount }
**Haskell:**
data BankAccount = BankAccount { balance :: Double }
deposit :: Double -> BankAccount -> BankAccount
deposit amount account = account { balance = balance account + amount }
withdraw :: Double -> BankAccount -> BankAccount
withdraw amount account = account { balance = balance account - amount }
**Rust:**
pub struct BankAccount { pub balance: f64 }
impl BankAccount {
pub fn deposit(&self, amount: f64) -> Self {
BankAccount { balance: self.balance + amount }
}
pub fn withdraw(&self, amount: f64) -> Self {
BankAccount { balance: self.balance - amount }
}
}
3.3 継承からパターンマッチへ
継承の置き換え
**OO(継承):**
abstract class Shape { abstract double area(); }
class Circle extends Shape { double radius; double area() { return PI * r * r; } }
class Square extends Shape { double side; double area() { return side * side; } }
**Scala(ADT):**
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Square(side: Double) extends Shape
def area(shape: Shape): Double = shape match
case Circle(r) => math.Pi * r * r
case Square(s) => s * s
**Haskell(ADT):**
data Shape = Circle Double | Square Double
area :: Shape -> Double
area (Circle r) = pi * r * r
area (Square s) = s * s
**Rust(enum):**
pub enum Shape {
Circle(f64),
Square(f64),
}
pub fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Square(s) => s * s,
}
}
3.4 Strangler Fig パターン
段階的に古いコードを新しいコードで置き換える移行戦略です。
Strangler Fig の実装
// Scala: フィーチャーフラグで切り替え
class OrderService(useNewImpl: Boolean = false):
private val legacyService = new LegacyOrderService()
private val fpService = new FPOrderService()
def processOrder(order: Order): Result =
if useNewImpl then fpService.process(order)
else legacyService.process(order)
;; Clojure: 動的切り替え
(def use-new-impl (atom false))
(defn process-order [order]
(if @use-new-impl
(fp-process-order order)
(legacy-process-order order)))
// Rust: trait による差し替え
pub trait OrderProcessor {
fn process(&self, order: &Order) -> Result<ProcessedOrder, String>;
}
pub struct LegacyProcessor;
pub struct FPProcessor;
impl OrderProcessor for FPProcessor {
fn process(&self, order: &Order) -> Result<ProcessedOrder, String> {
validate(order)
.and_then(calculate_total)
.and_then(apply_discount)
}
}
3.5 GoF パターンの FP 変換
| OO パターン |
FP 変換 |
関数型での表現 |
| Strategy |
高階関数 |
関数を引数として渡す |
| Observer |
イベントバス / コールバック |
関数のリスト |
| Command |
データ構造 |
コマンドを ADT で表現 |
| Decorator |
関数合成 |
高階関数でラップ |
| Visitor |
パターンマッチ |
case / match で分岐 |
| Factory |
ファクトリ関数 |
関数を返す関数 |
| Composite |
再帰的 ADT |
ADT の再帰的定義 |
| Adapter |
変換関数 |
from / into / 関数 |
Observer パターンの FP 変換例
;; OO スタイル
;; subject.addObserver(observer)
;; subject.notifyAll()
;; FP スタイル: イベントバス
(def listeners (atom {}))
(defn subscribe [event-type handler]
(swap! listeners update event-type (fnil conj []) handler))
(defn publish [event-type data]
(doseq [handler (get @listeners event-type)]
(handler data)))
# Elixir: プロセスベースの Observer
defmodule EventBus do
use GenServer
def subscribe(event_type, pid) do
GenServer.cast(__MODULE__, {:subscribe, event_type, pid})
end
def publish(event_type, data) do
GenServer.cast(__MODULE__, {:publish, event_type, data})
end
def handle_cast({:publish, event_type, data}, subscribers) do
subscribers
|> Map.get(event_type, [])
|> Enum.each(&send(&1, {:event, event_type, data}))
{:noreply, subscribers}
end
end
4. 比較分析
4.1 移行難易度の評価
| 言語 |
移行元からの距離 |
難易度 |
理由 |
| Scala |
最も近い |
低 |
OO + FP のハイブリッド、Java との相互運用 |
| F# |
近い |
低〜中 |
.NET の OO と自然に共存 |
| Rust |
中程度 |
中 |
継承なし、trait ベースで FP に近い |
| Clojure |
やや遠い |
中 |
データ中心への概念転換が必要 |
| Elixir |
遠い |
中〜高 |
クラスの概念がなく、アクターモデルへの転換 |
| Haskell |
最も遠い |
高 |
純粋関数型への完全な転換が必要 |
4.2 段階的移行 vs 全面書き換え
| 戦略 |
適した言語 |
理由 |
| 段階的移行(推奨) |
Scala, F#, Clojure |
既存 OO コードとの共存が容易 |
| 全面書き換え |
Haskell, Elixir |
パラダイムが根本的に異なるため部分移行が困難 |
| ハイブリッド |
Rust |
trait ベースで OO 的コードも FP 的コードも書ける |
4.3 イベントソーシングによる移行
OO の可変状態を FP の不変イベント列に置き換える強力な移行パターンです:
OO: object.setState(newState) ← 状態を直接変更
FP: events = [..., event] ← イベントを蓄積し、状態を再構築
;; Clojure: イベントソーシング
(defn apply-event [state event]
(case (:type event)
:deposited (update state :balance + (:amount event))
:withdrawn (update state :balance - (:amount event))))
(defn rebuild-state [events]
(reduce apply-event {:balance 0} events))
5. 実践的な選択指針
| 移行シナリオ |
推奨言語 |
理由 |
| Java コードベースからの移行 |
Scala |
JVM 互換、段階的移行が最も容易 |
| C# コードベースからの移行 |
F# |
.NET 互換、段階的移行が容易 |
| 新規プロジェクトで FP 採用 |
Haskell, Elixir |
パラダイムの純粋性 |
| パフォーマンス要件が高い |
Rust |
ゼロコスト抽象化 + FP スタイル |
| 動的型付けからの移行 |
Clojure |
データ中心で概念が近い |
| 大規模チームでの移行 |
Scala, F# |
OO 経験者の学習曲線が緩やか |
6. まとめ
OO から FP への移行は、考え方の転換です:
- データと振る舞いの分離: クラス → データ型 + 関数
- 不変性の採用: 可変状態 → 状態変換関数
- パターンの簡素化: GoF パターン → 高階関数 + パターンマッチ
- 段階的な移行: Strangler Fig パターンで安全に移行
最も重要なのは、移行は一度にすべてを変える必要はないということです。新しいコードを FP で書き始め、徐々に既存コードを置き換えていくアプローチが、リスクを最小化しながら FP の恩恵を得る最善の方法です。
言語別個別記事