nikkie-ftnextの日記

イベントレポートや読書メモを発信

ソフトウェアを作りたかった私へ:作ると使うを分ける

はじめに

よければポチッとお願いします!
味ついてておいしいです!nikkieです。

週末3/24(日)のOOC 2024 登壇1準備からアウトプットです。

目次

わからん殺し「APIに絡む処理を、小さめの関数としてまとめたはずなのに...」

今回2も関数に切り出した処理が再利用しづらいという例です。

外部のAwesome APIからデータを取得し、自分たちのクラス3のオブジェクトに変換して返す関数です。

def fetch_awesome() -> AwesomeData:
    client = AwesomeAPIClient()
    raw_data = client.get(...)  # パラメタを渡してデータ取得
    return AwesomeData.from_(raw_data)

行数としては小さい関数に見えますが、なぜ再利用しづらいんでしょう?

ちょうぜつ本 第7章 「使用と生成の分離」

使用と生成の分離 (Kindle版 p.293)

ある処理(関数、クラス)で 依存 するモノの 生成使用 を分けようという考え方です。

  • 生成、すなわち作る
    • 上の例のclientのような、処理が依存するモノを作ること
  • 使用、すなわち使う
    • 上の例でclientのメソッド呼び出しのように、処理が依存するモノを使うこと
    • 依存するモノは与えられるので、それを操作するだけ

使用に必要な依存が外から与えられる(=注入される)、すなわち依存性注入です💉

私がまとめた関数は、なぜ再利用しづらかったのか

fetch_awesome()関数は、生成も使用も両方やっています。
依存は注入されず、依存を自分で作って使います。
「この関数は単体で完結しているので逆にいいんじゃない」と作った時点の私は思っていたのですが、あとあと変更しなければいけなくなったときに再利用しづらさを感じました。

APIのメジャーアップデートに追従したいケース

例えば、Awesome APIのv2がリリースされた場合、どのように対応しましょうか?
fetch_awesome()関数はまるまる作り直しにせざるを得ないような...

def fetch_awesome_v2() -> AwesomeData:
    client = AwesomeAPIV2Client()
    raw_data = client.get(...)
    return AwesomeData.from_(raw_data)

(このコードは雰囲気を伝えるために話を単純にしています4が、)v2用のクライアントを生成して使用する別の関数です。

でもv1用のfetch_awesome()関数とかなり似ています。
clientの使い方や、自分たちのクラスのオブジェクトへの変換は変わっていないのだから、fetch_awesome()関数を拡張して対応できたらもっとよさそうですよね。

使用と生成が分かれていた場合、すなわちclientがfetch_awesome()関数に引数として渡された場合を考えてみましょう。

def fetch_awesome_di(client) -> AwesomeData:
    raw_data = client.get(...)
    return AwesomeData.from_(raw_data)

呼び出す時にAwesomeAPIClientAwesomeAPIV2Clientも渡すことができます!

fetch_awesome_di()依存の使用オンリーです。
依存を生成する処理は、この関数の外側にあります。
fetch_awesome_di()は(どう作られたか分からないけれど)渡されたclientを使うだけなのです。
v2用のクライアントの追加だけで機能が拡張できます!(詳細は後述。インターフェースの話が出てきます)

長いテストコード

また、この件でもテストコードは長くなりがちです。
(私は検知できていませんでしたが)これも再利用しづらいということを伝えていたのだと思います。

def fetch_awesome() -> AwesomeData:
    client = AwesomeAPIClient()
    raw_data = client.get(...)
    return AwesomeData.from_(raw_data)
  • テストでは、実際のAwesomeAPIClientが作られないように、unittest.mock.patchでモックに差し替えます
    • get()メソッドの呼び出しが検証できます
  • AwesomeDataはデータを使ってもいいですし、モックを使うこともできます
    • データを使う場合、getメソッドの返り値にデータを設定します(テストの行数が大きくなりますね)
    • モックにする場合はfrom_()メソッドの呼び出しを検証します

クラスの場合、インスタンス化で依存性を注入!

小ネタですが、fetch_awesome()関数に相当するクラスを用意する場合、__init__()でclientを注入するように書きます。

class Fetcher:
    def __init__(self, client):
        self._client = client
    
    def fetch() -> AwesomeData:
        raw_data = self.client.get(...)
        return AwesomeData.from_(raw_data)

依存が属性に設定されているので、メソッドの実装ではそれを使用することだけを考えればいいのです。

作ると使うを分けた世界

依存性注入は、他の設計テクニックと結びつくように思われます。

注入するclientですが、インターフェースを使って扱えます。
型ヒントを

def fetch_awesome_di(client: AwesomeAPIClient | AwesomeAPIV2Client) -> AwesomeData:
    # 省略

と具体的に書く代わりに

def fetch_awesome_di(client: AwesomeAPIClientInterface) -> AwesomeData:
    # 省略

と書けるでしょう。

インターフェースは使い方を示すものですから、実装にあたってはget()メソッドが呼べるということさえ分かればよいです。
clientがインターフェースを具体的にどう実装しているかは、fetch_awesome_di()関数は興味がありません。

テストコードもAwesomeAPIClientInterfaceを実装したモックを使って書けますね。

インターフェースを導入した状態、当初から比べると、依存性が逆転していると言えますよね。

  • 当初:fetch_awesome()関数は、具体的なAwesomeAPIClientに依存していた
  • fetch_awesome_di()関数は、AwesomeAPIClientInterface(抽象)に依存している
    • このインターフェースを実装したクライアントを渡せばよい。動きを切り替えられる

依存性注入するように引数に移動しただけで、SOLID原則のIやDに至る道が見えるようになります。
実際に過去の自分のコードで手を動かしてこの点に気づいたときには、設計の知識がきれいにつながって震えました!

作ると使うを分けていく(リファクタリング

過去に書いたコードは作ると使うを一緒にやっているので、触るときにリファクタリングします。
具体的なリファクタリングテクニックは関数宣言の変更5です。

def fetch_awesome() -> AwesomeData:
    client = AwesomeAPIClient()
    raw_data = client.get(...)
    return AwesomeData.from_(raw_data)

一時的な名前をつけた関数を抽出します。
抽出した関数はclientを引数に受け取ります。

def xx_fetch_awesome(client) -> AwesomeData:
    raw_data = client.get(...)
    return AwesomeData.from_(raw_data)


def fetch_awesome() -> AwesomeData:
    client = AwesomeAPIClient()
    return xx_fetch_awesome(client)

fetch_awesome()をインライン化し、xx_fetch_awesome()fetch_awesome()にリネームして、できあがり!

この手順はfetch_awesome()の呼び出し箇所が少ないと仮定しています(多い場合は今後直面した時に工夫を考えます)

終わりに

ソフトウェアを作りたかった過去の私に伝えたい、「作ると使うを分ける」(依存性注入 from ちょうぜつ本)でした。

  • 作ると使うを混ぜていると、その関数は拡張しづらい
  • 作る処理を外に切り出し、与えられたモノ(依存)を使うだけとする
    • インターフェース(使い方)を定義することで、数パターンの具象を渡せるようになる(=拡張できる)
    • 作ると使うが混ざった状態から、依存性が逆転(具象ではなく抽象に依存する)
  • 作ると使うが混ざった関数を見つけたら、関数宣言の変更でリファクタリングをしよう

作ると使うを混ぜていたところから分けるように考え方を変えただけで、インターフェースによる依存性の逆転まで私の中ではつながりました。
そういうことだったのか!


  1. 変更しやすいコードを書くコツを話します(時間帯が vs ミノ駆動さん & nrslibさん。お? やんのか?(震え声))
  2. 前回
  3. ドメイン」という言葉が該当するかと思いますが、ドメイン駆動設計はまだまだ全然練習中なので、この語の使用を避けています
  4. Awesome API v1でもv2でも返り値からAwesomeDataが作れると仮定しています。返り値に含まれるフィールドが増えたとか、パスだけが /v1/foo から /v2/awesome/foo から変わったとか、そんな変更がAPI側にありました
  5. カタログより Change Function Declaration