依存オブジェクトの寿命をどう扱うか
ワインは熟成すれば香りが増すが、ある瞬間を過ぎると急激に劣化する。依存オブジェクトも同様で、使い方を誤るとメモリリークや性能劣化を招く。本章では、DIコンテナやPure DIを通じて、依存関係の寿命を適切に制御する方法を解説する。
Composerが握るライフサイクルの主導権
「Composer」とは、依存オブジェクトを生成・管理する統一的な役割を担うコンポーネントである。DIコンテナでも手動のファクトリでも構わないが、依存の寿命はComposerが決めるという原則は共通する。
// 例:Pure DIでシングルトンとトランジェントを混在させる
public sealed class CommerceComposer
{
private readonly string _conn;
private readonly IUserContext _userCtx; // Singleton
private readonly IRouteCalc _calc; // Singleton
public CommerceComposer(string connectionString)
{
_conn = connectionString;
_userCtx = new WebUserContextAdapter();
_calc = new RouteCalculator();
}
public HomeController ResolveHomeController()
{
// CommerceContextはリクエストごとに新規
var ctx = new CommerceContext(_conn);
return new HomeController(
new ProductService(
new SqlProductRepository(ctx),
_userCtx));
}
}
IDisposable依存を安全に破棄する
実装がアンマネージリソースを扱う場合、Disposeの呼び出し義務が生じる。しかし、インターフェースに IDisposable を露出させるのは設計の臭いである。代わりに Composer が暗黙的にライフサイクルを管理する。
// 一時的なWCFチャネルを隠蔽
public class WcfProductGateway : IProductGateway
{
private readonly ChannelFactory<IProductSvc> _factory;
public WcfProductGateway(ChannelFactory<IProductSvc> factory)
{
_factory = factory;
}
public void Delete(Guid id)
{
var ch = _factory.CreateChannel();
try { ch.DeleteProduct(id); }
finally
{
try { ((IDisposable)ch).Dispose(); }
catch { /* 例外は無視 */ }
}
}
}
3つの基本ライフサイクルパターン
| パターン | 説明 | 使用例 |
|---|---|---|
| Singleton | プロセス内で唯一のインスタンスを共有 | 設定オブジェクト、キャッシュ、無状態サービス |
| Scoped | 1リクエスト(1ユニット・オブ・ワーク)内で共有 | Entity Framework DbContext、ユニット・オブ・ワーク |
| Transient | 都度新規インスタンス | DTO、非スレッドセーフな依存 |
Scoped ライフサイクルの実装例
public sealed class ConsoleScopeComposer
{
private readonly string _conn;
public ConsoleScopeComposer(string conn) => _conn = conn;
public RatePresenter BuildPresenter()
{
// 1分ごとのタイマー実行で毎回新しいスコープ
var ctx = new CommerceContext(_conn);
return new RatePresenter(
new SqlCurrencyRepo(ctx),
new CurrencyConverter(
new SqlRateProvider(ctx)));
}
}
アンチパターン:Captive Dependency
Singleton なサービスに Transient または Scoped 依存を注入すると、依存が本来の寿命より長く拘束され、スレッドセーフでないリソースが複数スレッドから同時に参照される危険がある。
// 悪い例:Scoped DbContext を Singleton サービスに閉じ込める
public sealed class BadComposer
{
private readonly IOrderRepo _repo;
public BadComposer(string conn)
{
_repo = new SqlOrderRepo(new CommerceContext(conn)); // NG
}
}
回避策は、Scoped 依存をファクトリメソッドの内部で局所化すること。
漏洩抽象を防ぐ
Lazy<T> や IEnumerable<T> をコンストラクタで受け取らせると、ライフサイクル戦略が呼び出し側に漏洩する。代わりに、Composite または Virtual Proxy で包む。
// 複数ロガーを隠蔽する Composite
public sealed class CompositeLogger : ILogger
{
private readonly ILogger[] _loggers;
public CompositeLogger(ILogger[] loggers) => _loggers = loggers;
public void Log(LogEntry e)
{
foreach (var l in _loggers)
try { l.Log(e); } catch { /* 失敗を無視 */ }
}
}
スレッド固有ライフサイクルの罠
[ThreadStatic] や per-thread ライフサイクルは、async/await によるスレッド移動で意図せず複数スレッドから参照され、競合状態を引き起こす。スコープ境界は「リクエスト」または「操作」単位で設計すべき。
まとめ
- Composer が依存の寿命を主導し、適切に破棄する。
- Singleton/Scoped/Transient を使い分け、Captive Dependency を回避。
- IDisposable は実装側に留め、抽象に漏洩させない。
- 遅延初期化や複数実装は Proxy/Composite で隠蔽。
- スレッド固有状態は避け、スコープ境界を明確にする。