コードがデータを処理する際、信頼できる唯一の情報源を持つことが重要です。
本記事では、データの種類、複数の情報源を持つことによる問題、およびその解決策について解説します。
データの種類
- 一次データ: コードに提供する必要があるデータ
- 派生データ: 一次データに基づいてコードが計算できるデータ
例えば、家計簿アプリケーションを考えてみましょう。
一次データは、収入と支出の取引履歴です。
派生データは、収入の合計、支出の合計、および収支バランスです。
収入の合計と支出の合計は、取引履歴から計算できます。
収支バランスは、収入の合計から支出の合計を差し引いて計算できます。
複数の情報源による問題
派生データを別の情報源として保持すると、不整合な状態が発生する可能性があります。
以下のコードは、その問題を示しています。
class HouseholdAccount: def __init__(self, transactions, income, expense, balance): self.transactions = transactions self.income = income self.expense = expense self.balance = balance
このクラスでは、取引履歴、収入の合計、支出の合計、および収支バランスを個別に保持しています。
しかし、収入の合計、支出の合計、および収支バランスは、取引履歴から計算できるため、冗長な情報です。
以下のようにインスタンス化すると、不整合な状態が発生します。
transactions = [ Transaction('income', 1000), Transaction('expense', 500) ] account = HouseholdAccount(transactions, 1000, 500, 1000) # 収支バランスが不整合
取引履歴から計算すると、収入の合計は1000、支出の合計は500、収支バランスは500になるはずです。
しかし、収支バランスが1000になっています。
このような不整合な状態は、バグの原因となります。
解決策: 一次データを信頼できる唯一の情報源とする
派生データは、一次データから計算するようにします。
以下のコードは、その解決策を示しています。
class Transaction: def __init__(self, type, amount): self.type = type self.amount = amount class HouseholdAccount: def __init__(self, transactions): self.transactions = transactions def get_income(self): return sum(t.amount for t in self.transactions if t.type == 'income') def get_expense(self): return sum(t.amount for t in self.transactions if t.type == 'expense') def get_balance(self): return self.get_income() - self.get_expense()
このコードでは、Transaction クラスを導入しています。
Transaction クラスは、取引の種類(type)と金額(amount)を表します。
取引の種類は、’income’(収入)または’expense’(支出)のいずれかです。
HouseholdAccount クラスは、Transaction オブジェクトのリスト(transactions)を受け取ります。
このリストは、家計簿の取引履歴を表します。
get_income() メソッドは、取引リストから収入の合計を計算することになります。
具体的には、取引の種類が’income’である取引の金額を合計します。
同様に、get_expense() メソッドは、取引リストから支出の合計を計算することになります。
get_balance() メソッドは、収入の合計から支出の合計を差し引いて、収支バランスを計算します。
取引リストを一次データとして扱い、収入の合計、支出の合計、および収支バランスを派生データとして計算しています。
このことにより、信頼できる唯一の情報源を維持しています。
派生データの計算コストが高い場合
派生データの計算コストが高い場合は、計算結果をキャッシュすることを検討しましょう。
以下のコードは、その例を示しています。
class HouseholdAccount: def __init__(self, transactions): self.transactions = transactions self.cached_income = None self.cached_expense = None def get_income(self): if self.cached_income is None: self.cached_income = sum(t.amount for t in self.transactions if t.type == 'income') return self.cached_income def get_expense(self): if self.cached_expense is None: self.cached_expense = sum(t.amount for t in self.transactions if t.type == 'expense') return self.cached_expense def get_balance(self): return self.get_income() - self.get_expense()
このクラスでは、収入の合計と支出の合計を計算した結果をキャッシュします。
キャッシュには、cached_income と cached_expense を使用する形です。
初回の get_income() または get_expense() の呼び出し時に、キャッシュに保存します。
その際、取引リストから収入または支出の合計を計算することになります。
次回以降の呼び出し時は、キャッシュから値を返すことで、計算コストを削減します。
ただし、キャッシュを使用する場合は、データの整合性に注意が必要です。
取引リストが変更された場合は、キャッシュをクリアする必要があります。
以下は、HouseholdAccount クラスの使用例です。
transactions = [ Transaction('income', 1000), Transaction('expense', 200), Transaction('income', 500), Transaction('expense', 100) ] account = HouseholdAccount(transactions) print(account.get_income()) # 1500 print(account.get_expense()) # 300 print(account.get_balance()) # 1200 transactions.append(Transaction('income', 200)) print(account.get_income()) # 1500 (キャッシュされた値) print(account.get_expense()) # 300 (キャッシュされた値) print(account.get_balance()) # 1200 (キャッシュされた値)
この例では、4つの取引(2つの収入と2つの支出)を持つ家計簿が対象です。
HouseholdAccount クラスを使って、収入、支出、および収支バランスを計算しています。
取引リストに新しい取引を追加しても、キャッシュされた値が返されるため、計算結果が更新されていないことに注意してください。
キャッシュを使用する場合は、データの変更に応じてキャッシュをクリアする仕組みが必要です。
まとめ
データに対して信頼できる唯一の情報源を持つことは、コードの読みやすさと保守性を高めるために重要です。
派生データは、一次データから計算するようにしましょう。
計算コストが高い場合は、キャッシュの検討も考慮することも必要です。
ただし、キャッシュを使用する場合は、データの整合性に注意が必要となります。
これらのテクニックを活用し、より読みやすく、保守性の高いコードを書くことを心がけましょう。