✓=レビュー済 ○=未レビュー ⚠=Suspect(複数同時表示あり。IDクリックで詳細へ)
| グループ | REQ | ARCH | SPEC | TST | IMPL | ADR |
|---|---|---|---|---|---|---|
| DI | REQ012 ✓ 全コンポーネント(DB、ストレージ、シリアライザ、リミッター等)が `Protocol/ABC` に基づく依存注入により差し替え可能であること。ファクトリ関数が... | ARCH012 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|構築| B[core.Spot] A ... | SPEC022 ✓ ## インターフェース ```python def Spot( name: str = "default", storage_path: st... | TST022 ✓ ## 目的 `bs.Spot()` ファクトリ関数は全コンポーネントの DI 配線を担う唯一のパブリックエントリポイントであり、デフォルト構成の正確性とカスタ... | IMPL022 ✓ ## 実装概要 `beautyspot` パッケージのメインエントリポイントとなる `Spot` ファクトリ関数の実装。 引数として渡された各コンポーネント(... | ADR009 ✓ # Database Dependency Injection and Abstraction ## Context and Problem Statemen... |
| DI | REQ012 ✓ 全コンポーネント(DB、ストレージ、シリアライザ、リミッター等)が `Protocol/ABC` に基づく依存注入により差し替え可能であること。ファクトリ関数が... | ARCH012 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|構築| B[core.Spot] A ... | SPEC022 ✓ ## インターフェース ```python def Spot( name: str = "default", storage_path: st... | TST022 ✓ ## 目的 `bs.Spot()` ファクトリ関数は全コンポーネントの DI 配線を担う唯一のパブリックエントリポイントであり、デフォルト構成の正確性とカスタ... | IMPL022 ✓ ## 実装概要 `beautyspot` パッケージのメインエントリポイントとなる `Spot` ファクトリ関数の実装。 引数として渡された各コンポーネント(... | ADR026 ✓ # Factory Function for Default Dependency Injection ## Context and Problem Stat... |
| リンク方向 | カバー数 | カバー率 | 未カバー |
|---|---|---|---|
| ARCH → REQ | 1 / 1 | 100.0% | — |
| SPEC → ARCH | 1 / 1 | 100.0% | — |
| TST → SPEC | 1 / 1 | 100.0% | — |
| IMPL → SPEC | 1 / 1 | 100.0% | — |
| ADR → REQ | 1 / 1 | 100.0% | — |
全コンポーネント(DB、ストレージ、シリアライザ、リミッター等)が Protocol/ABC に基づく依存注入により差し替え可能であること。ファクトリ関数がデフォルトの組み立てを提供すること。
親: —
graph TD
A[bs.Spot Factory] -->|構築| B[core.Spot]
A -->|DI| C[TaskDBBase]
A -->|DI| D[BlobStorageBase]
A -->|DI| E[SerializerProtocol]
A -->|DI| F[StoragePolicyProtocol]
A -->|DI| G[LimiterProtocol]
| コンポーネント | 責務 | インターフェース |
|---|---|---|
| bs.Spot (Factory) | 環境(名前、パス等)に応じたデフォルトコンポーネントの選定と組み立て | bs.Spot() |
| core.Spot | 注入された依存関係を使用して、キャッシュロジックをオーケストレーションする | mark(), cached_run() |
| Protocols / ABCs | 各コンポーネントが満たすべき契約(インターフェース)の定義 | SerializerProtocol 等 |
| 技術領域 | 選定 | 理由 |
|---|---|---|
| パターン | Constructor Injection | 依存関係を明示的に渡し、テスト時のMock差し替えを容易にする |
| インターフェース | Protocol (Duck Typing) | 厳密な継承を強制せず、構造的部分型によりサードパーティ実装を受け入れ |
| デフォルト設定 | 規約より構成 (CoC) | .beautyspot/ ディレクトリを基点とした標準パスを自動生成 |
core.Spot は具体的なクラス名(SQLite等)を知らず、Protocolのみに依存する親: REQ012
子: SPEC022
def Spot(
name: str = "default",
storage_path: str | Path | None = None,
serializer: SerializerProtocol | None = None,
limiter: LimiterProtocol | None = None,
# ...その他の依存
) -> core.Spot: ...
storage_path が未指定の場合、.beautyspot/ 以下のプロジェクト名ディレクトリを使用するSQLiteTaskDB を生成し、マイグレーションを実行s3://等)を判定し、適切な BlobStorage 実装を生成core.Spot のコンストラクタに注入して返す| パラメータ | デフォルト値の導出 |
|---|---|
db |
.beautyspot/{name}.db |
blobs |
.beautyspot/blobs/{name}/ |
serializer |
MsgpackSerializer() |
policy |
WarningOnlyPolicy (閾値10MB) |
OSError を送出する。name で複数回呼び出した場合も、原則として新しいインスタンスを返すが、DBファイルへの接続は共有される(SQLiteのWALモードを活用)。親: ARCH012
bs.Spot() ファクトリ関数は全コンポーネントの DI 配線を担う唯一のパブリックエントリポイントであり、デフォルト構成の正確性とカスタム実装の差し替え可能性がシステム全体の柔軟性と正確性を決定する。
SQLiteTaskDB, LocalStorage, MsgpackSerializer, TokenBucket, WarningOnlyPolicy が自動注入されること。ワークスペースディレクトリ(.beautyspot/)が自動作成されることTaskDBBase / TaskDBCore プロトコル準拠のカスタムDBを db= で注入し、正常にキャッシュ操作が行えることBlobStorageBase 準拠のモック Storage を storage_backend= で注入し、Blob保存が委譲されることSerializerProtocol 準拠のカスタム Serializer を注入し、シリアライズ処理が差し替わることbs.Spot() の戻り値型が pyright / mypy で正しく推論されることreferences: tests/integration/core/test_dependency_injection.py
親: SPEC022
子: —
beautyspot パッケージのメインエントリポイントとなる Spot ファクトリ関数の実装。
引数として渡された各コンポーネント(DB、Serializer、Storage等)を依存性注入(DI)で
解決し、デフォルトのコンポーネント(SQLiteTaskDB, MsgpackSerializer, LocalStorage等)を
インスタンス化して CacheManager と _Spot コアエンジンを組み立てる。
_Spot クラス自体のコンストラクタは複雑な依存関係を要求するが、
ユーザー向けに Spot() 関数を提供することで、通常は name を渡すだけで
「ゼロ設定」で動作するようにカプセル化している。
同時に、高度なユーザーは各コンポーネントを自由に差し替え可能な DI アーキテクチャを維持している。
Spot() 関数内でデフォルトのDB(SQLiteTaskDB)を自動生成した場合、
spot._owns_db = True フラグを立て、Spotエンジンのシャットダウン時に
DBも自動でクローズされるようにする。一方、ユーザーが明示的に db= を
渡した場合は、DBのライフサイクル管理は呼び出し元に委ねる(勝手に閉じない)。
save_blob フラグや引数によって WarningOnlyPolicy,
AlwaysBlobPolicy 等に解決される__all__ に各種プロトコルやデフォルト実装、例外クラスをエクスポートし、
ライブラリとしての公開API境界を明確に定義しているreferences: src/beautyspot/__init__.py
親: SPEC022
子: —
これまでの beautyspot (v0.x) は、Project クラス内部で TaskDB (SQLite実装) をハードコードしてインスタンス化していた。
# v0.x implementation
self.db = TaskDB(self.db_path)
この設計には以下の課題がある:
v1.0.0 では、ユーザー体験(DX)としての「手軽さ(パスを指定するだけ)」を維持しつつ、アーキテクチャレベルでの柔軟性を確保する必要がある。
TaskDB 実装を注入 (Inject) できるようにする。db_path は維持しつつ、内部のファクトリで実装を切り替える。Project に DB インスタンスを直接渡す Dependency Injection (DI) パターン。Chosen option: Option 3.
src/beautyspot/db.py に抽象基底クラス TaskDB を定義し、インターフェース(save, get, init_schema 等)を強制する。SQLiteTaskDB として再定義する。Project クラスのコンストラクタ引数を db_path: str から db: Union[str, TaskDB] に変更する。str が渡された場合: 内部で SQLiteTaskDB(path) を生成する(Convenience)。TaskDB が渡された場合: そのインスタンスをそのまま使用する(Injection)。Project(..., db=PostgresDB(...)) のように利用可能になる。MemoryTaskDB などを差し込むことで、高速かつクリーンなテストが可能になる。db_path 引数が廃止(または db に統合)されるため、既存コードの引数名変更が必要になる(v1.0.0 での破壊的変更)。親: REQ012
子: —
v2.0 以降、beautyspot は依存性注入 (DI) アーキテクチャを採用しました。Spot クラス(core.py)の初期化には、TaskDB, Serializer, Storage の各インスタンスを明示的に渡す必要があります。
これはテスト容易性と柔軟性の観点では優れていますが、単に「すぐに使い始めたい」だけのユーザーにとっては、初期化が非常に冗長になってしまうという課題がありました。
# 一般的なスクリプトには冗長すぎる
db = SQLiteTaskDB(...)
storage = LocalStorage(...)
serializer = MsgpackSerializer()
spot = Spot(name="app", db=db, storage=storage, serializer=serializer)
core.py の Spot クラスを具象クラス(SQLite 等)の詳細から保護し、ピュアな状態に保つこと。Spot.__init__ の引数にデフォルト値(具象クラス)を設定する。__init__.py でファクトリ関数 Spot を提供し、そこでデフォルトの組み立てを行う。Chosen option: Option 3.
beautyspot/__init__.py において、具象クラスのインスタンス化を伴うファクトリ関数 Spot を公開します。
# beautyspot/__init__.py
def Spot(name: str, db=None, ...):
resolved_db = db or SQLiteTaskDB(...)
# ... 他の解決ロジック ...
return _Spot(name, db=resolved_db, ...)
これにより、ライブラリのトップレベルからインポートされる Spot は関数となり、内部で core.py の _Spot クラスを組み立てて返します。
_Spot クラスが、SQLiteTaskDB などの具象実装をインポートする必要がなくなる(関心の分離)。Spot がクラスではなく関数になるため、Spot を直接継承することが難しくなる。継承が必要な場合は core から _Spot をインポートする必要があるが、本ライブラリでは継承よりもコンポジションを推奨するため、許容範囲とする。親: REQ012
子: —