第22章: OO から FP への移行¶
はじめに¶
本章では、オブジェクト指向(OO)スタイルのコードを関数型(FP)スタイルに移行する方法を学びます。Rust はデフォルトで不変なため、FP スタイルに自然に適合しますが、OO から来た開発者のために移行のガイドラインを示します。
1. OO スタイル vs FP スタイル¶
OO スタイル(可変)¶
/// OO スタイル: 内部状態を持つオブジェクト
pub mod oo_style {
pub struct Account {
id: String,
balance: f64,
transactions: Vec<Transaction>,
}
pub struct Transaction {
pub transaction_type: TransactionType,
pub amount: f64,
}
pub enum TransactionType {
Deposit,
Withdrawal,
}
impl Account {
pub fn new(id: &str, initial_balance: f64) -> Account {
Account {
id: id.to_string(),
balance: initial_balance,
transactions: Vec::new(),
}
}
// 可変メソッド - 内部状態を変更
pub fn deposit(&mut self, amount: f64) -> f64 {
if amount > 0.0 {
self.balance += amount;
self.transactions.push(Transaction {
transaction_type: TransactionType::Deposit,
amount,
});
}
self.balance
}
pub fn withdraw(&mut self, amount: f64) -> f64 {
if amount > 0.0 && self.balance >= amount {
self.balance -= amount;
self.transactions.push(Transaction {
transaction_type: TransactionType::Withdrawal,
amount,
});
}
self.balance
}
}
}
FP スタイル(不変)¶
/// FP スタイル: 不変データと純粋関数
pub mod fp_style {
#[derive(Debug, Clone)]
pub struct Account {
pub id: String,
pub balance: f64,
pub transactions: Vec<Transaction>,
}
#[derive(Debug, Clone)]
pub struct Transaction {
pub transaction_type: TransactionType,
pub amount: f64,
}
#[derive(Debug, Clone)]
pub enum TransactionType {
Deposit,
Withdrawal,
}
/// 口座を作成(純粋関数)
pub fn make_account(id: &str, initial_balance: f64) -> Account {
Account {
id: id.to_string(),
balance: initial_balance,
transactions: Vec::new(),
}
}
/// 入金(新しい口座を返す - 元は不変)
pub fn deposit(account: Account, amount: f64) -> Account {
if amount > 0.0 {
Account {
balance: account.balance + amount,
transactions: {
let mut txns = account.transactions;
txns.push(Transaction {
transaction_type: TransactionType::Deposit,
amount,
});
txns
},
..account
}
} else {
account
}
}
/// 出金(新しい口座を返す - 元は不変)
pub fn withdraw(account: Account, amount: f64) -> Account {
if amount > 0.0 && account.balance >= amount {
Account {
balance: account.balance - amount,
transactions: {
let mut txns = account.transactions;
txns.push(Transaction {
transaction_type: TransactionType::Withdrawal,
amount,
});
txns
},
..account
}
} else {
account
}
}
}
2. 移行戦略¶
Strangler Fig パターン¶
既存のコードを徐々に新しいスタイルに置き換えます。
pub mod migration {
use super::fp_style::*;
pub enum Style {
FP,
OO,
}
pub struct StranglerAccount {
pub style: Style,
pub data: Account,
}
/// フィーチャーフラグによる切り替え
pub fn create_account(id: &str, balance: f64, use_fp: bool) -> StranglerAccount {
let style = if use_fp { Style::FP } else { Style::OO };
StranglerAccount {
style,
data: make_account(id, balance),
}
}
/// 統一インターフェース
pub fn account_deposit(account: StranglerAccount, amount: f64) -> StranglerAccount {
StranglerAccount {
data: deposit(account.data, amount),
..account
}
}
}
アダプターパターン¶
既存のインターフェースを維持しながら、内部をFPに移行します。
pub mod adapter {
use super::fp_style::*;
/// 既存インターフェースを維持するアダプター
pub struct FPAccountAdapter {
account: Account,
}
impl FPAccountAdapter {
pub fn new(id: &str, initial_balance: f64) -> FPAccountAdapter {
FPAccountAdapter {
account: make_account(id, initial_balance),
}
}
pub fn get_balance(&self) -> f64 {
self.account.balance
}
// 既存のインターフェースを維持
pub fn deposit_to_account(&mut self, amount: f64) -> f64 {
self.account = deposit(self.account.clone(), amount);
self.account.balance
}
pub fn withdraw_from_account(&mut self, amount: f64) -> f64 {
self.account = withdraw(self.account.clone(), amount);
self.account.balance
}
}
}
3. 多態性の実現¶
OO: トレイトオブジェクト¶
pub trait Shape {
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
impl Shape for Rectangle {
fn area(&self) -> f64 { self.width * self.height }
}
FP: enum とパターンマッチ¶
pub enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
pub fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle { base, height } => base * height / 2.0,
}
}
pub fn scale(shape: &Shape, factor: f64) -> Shape {
match shape {
Shape::Circle { radius } => Shape::Circle { radius: radius * factor },
Shape::Rectangle { width, height } => Shape::Rectangle {
width: width * factor,
height: height * factor,
},
Shape::Triangle { base, height } => Shape::Triangle {
base: base * factor,
height: height * factor,
},
}
}
4. 段階的な関数抽出¶
/// 純粋関数を抽出
pub fn calculate_interest(balance: f64, rate: f64, days: i32) -> f64 {
balance * rate * (days as f64 / 365.0)
}
/// 手数料計算(純粋関数)
pub struct FeeStructure {
pub minimum_balance: f64,
pub low_balance_fee: f64,
pub premium_threshold: f64,
pub standard_fee: f64,
}
pub fn calculate_fee(balance: f64, fee_structure: &FeeStructure) -> f64 {
if balance < fee_structure.minimum_balance {
fee_structure.low_balance_fee
} else if balance > fee_structure.premium_threshold {
0.0
} else {
fee_structure.standard_fee
}
}
5. OO vs FP 比較表¶
| 側面 | OO | FP |
|---|---|---|
| 基本単位 | オブジェクト | データ + 関数 |
| 状態管理 | 可変(mutable) | 不変(immutable) |
| 多態性 | 継承・トレイト | ADT・パターンマッチ |
| コード再利用 | 継承 | 関数合成 |
| 副作用 | どこでも可能 | 境界に分離 |
| テスト | モックが必要 | 入力→出力のみ |
| デバッグ | 状態追跡が困難 | 値が不変で追跡容易 |
| 並行処理 | ロックが必要 | 不変データで安全 |
6. 移行チェックリスト¶
準備¶
- 現在のコードの状態を把握
- テストカバレッジを確認
- 移行の優先順位を決定
実行¶
- 純粋関数を抽出
- 副作用を分離
- ADT に移行
- テストを追加
完了¶
- 古いコードを削除
- ドキュメントを更新
- パフォーマンスを確認
まとめ¶
本章では、OO から FP への移行について学びました:
- OO vs FP: 可変 vs 不変の違い
- Strangler Fig パターン: 段階的な移行
- アダプターパターン: 既存インターフェースの維持
- ADT への移行: enum とパターンマッチ
- 純粋関数の抽出: ビジネスロジックの分離
Rust は不変性がデフォルトなので、FP スタイルへの移行は自然に行えます。
参考コード¶
- ソースコード:
apps/rust/part7/src/chapter22.rs
Simple made easy.