DeepSeek Harness: サービスと依存関係:Service 基底クラス

最終更新:2026-08-31

関数プラグインは apply を通じて「実行する」機能を登録しますが、Service プラグインはクラスを通じて「提供する」機能を公開します。プラグインが内部状態を維持し、呼び出し可能な API を公開し、他のプラグインの依存基盤として機能する必要がある場合、Service 基底クラスが最適です。

💡 ヒント:Service の核心的価値は「ステートフルなサービス」——インスタンスプロパティが状態を保持し、メソッドが API を公開し、他のプラグインは inject 依存を宣言して使用します。ツールとリスナーの登録だけなら関数形式の方がシンプルです。

📋 前提知識14-inject.md17-fiber.md の完了

1. 学習内容

サービス分離とスコープ


2. Service クラス定義

▶ サンプル 1:

TYPESCRIPT
import { Service } from '@deepseek-ai/cordis'

export default class MyService extends Service {
  constructor(ctx: Context) {
    super(ctx, 'my-service')
  }
}

Service 基底クラスのコンストラクタは2つのパラメータを受け取ります:

▶ サンプル 2:キャッシュサービス

TYPESCRIPT
import { Service, Context } from '@deepseek-ai/cordis'

export default class CacheService extends Service {
  private cache = new Map<string, { value: any; expires: number }>()

  constructor(ctx: Context) {
    super(ctx, 'cache')
  }

  get(key: string): any {
    const entry = this.cache.get(key)
    if (!entry) return undefined
    if (Date.now() > entry.expires) {
      this.cache.delete(key)
      return undefined
    }
    return entry.value
  }

  set(key: string, value: any, ttlMs: number = 60000): void {
    this.cache.set(key, { value, expires: Date.now() + ttlMs })
  }

  delete(key: string): boolean {
    return this.cache.delete(key)
  }

  clear(): void {
    this.cache.clear()
  }
}

(3) 自動登録

Service コンストラクタの super(ctx, 'cache') はインスタンスを cache サービスとして自動的に登録します。他のプラグインは inject: ['cache'] を宣言して使用します。


3. constructor とサービス名

(1) サービス名の目的

サービス名はグローバルレジストリのキーです:

TYPESCRIPT
super(ctx, 'cache')
// → 他のプラグインは ctx.cache でこのサービスインスタンスにアクセス

(2) 命名規則

TYPESCRIPT
// ✅ 推奨:kebab-case または camelCase
super(ctx, 'cache')
super(ctx, 'rate-limiter')
super(ctx, 'metricsCollector')

// ❌ 非推奨
super(ctx, 'Cache')        // 大文字開始
super(ctx, 'cache_service') // アンダースコア

(3) サービス名の競合

2つの Service が同じ名前を登録すると、後から登録されたものが前を上書き:

TEXT 📖 参照専用
Plugin A が 'cache' を登録 → CacheServiceA
Plugin B が 'cache' を登録 → CacheServiceB
→ 最終的な ctx.cache = CacheServiceB

(4) サービスへのアクセス

TYPESCRIPT
// コンシューマプラグイン
export const inject = ['cache']

export function apply(ctx: Context) {
  ctx.cache.set('user:1', { name: 'Alice' }, 300000)
  const user = ctx.cache.get('user:1')
}

4. static inject 宣言

(1) Service の依存関係

Service クラスは static inject で依存関係を宣言:

TYPESCRIPT
export default class DatabaseService extends Service {
  static inject = ['fs']

  constructor(ctx: Context) {
    super(ctx, 'database')
  }

  async query(sql: string) {
    const schema = await ctx.fs.readFile('schema.json')
    // ...
  }
}

(2) オプション依存関係

TYPESCRIPT
static inject = ['fs', 'cache?']

関数プラグインと同じ構文;? はオプションを意味します。

(3) サービス初期化タイミング

100%
sequenceDiagram
    participant F as Framework
    participant FS as FS Service
    participant DB as Database Service
    participant Tool as Tool Plugin

    F->>FS:ロード → active
    F->>DB:inject ['fs'] ✅ → constructor 実行 → active
    F->>Tool:inject ['database'] ✅ → apply 実行 → active

(4) コンストラクタでの依存関係使用

TYPESCRIPT
export default class MyService extends Service {
  static inject = ['tools']
  
  private defaultTool: string

  constructor(ctx: Context) {
    super(ctx, 'my-service')
    // ctx.tools は準備完了(inject が保証)
    this.defaultTool = ctx.tools.getDefault()
  }
}

5. サービスの登録と検出

(1) 登録機構

Service コンストラクタの super(ctx, name) が登録をトリガー:

TYPESCRIPT
// フレームワーク内部(疑似コード)
class Service {
  constructor(ctx: Context, name: string) {
    ctx.provide(name, this)
    // このサービスに依存する保留中のプラグインのアクティブ化をトリガー
  }
}

(2) 検出機構

他のプラグインは inject で依存を宣言;フレームワークは準備完了時に注入:

TYPESCRIPT
// コンシューマプラグイン
export const inject = ['cache']

export function apply(ctx: Context) {
  // ctx.cache は自動注入、型安全
  ctx.cache.set('key', 'value')
}

(3) ランタイムサービス照会

TYPESCRIPT
// サービスが登録されているか確認
if (ctx.cache) {
  ctx.cache.get('key')
}

// 登録済みサービス一覧
ctx.logger.info('available services:', ctx.serviceNames)

(4) サービス依存グラフ

100%
graph TB
    FS[fs Service] --> DB[database Service]
    CACHE[cache Service] --> DB
    DB --> TOOL1[tool-a Plugin]
    DB --> TOOL2[tool-b Plugin]
    CACHE --> TOOL1

6. 型安全なサービスアクセス

(1) TypeScript 型拡張

ctx.cache に適切な型ヒントを与えるため、型拡張を宣言:

TYPESCRIPT
// プラグインの型宣言ファイル
declare module '@deepseek-ai/cordis' {
  interface Context {
    cache: CacheService
  }
}

▶ サンプル 2:

TYPESCRIPT
// cache-service.ts
import { Service, Context } from '@deepseek-ai/cordis'

export default class CacheService extends Service {
  private cache = new Map<string, any>()

  constructor(ctx: Context) {
    super(ctx, 'cache')
  }

  get(key: string): any {
    return this.cache.get(key)
  }

  set(key: string, value: any, ttl?: number): void {
    this.cache.set(key, value)
  }

  has(key: string): boolean {
    return this.cache.has(key)
  }

  delete(key: string): boolean {
    return this.cache.delete(key)
  }

  clear(): void {
    this.cache.clear()
  }
}

// 型拡張
declare module '@deepseek-ai/cordis' {
  interface Context {
    cache: CacheService
  }
}

(3) コンシューマ側の型安全性

TYPESCRIPT
// コンシューマプラグイン
import { Context } from '@deepseek-ai/cordis'

export const inject = ['cache']

export function apply(ctx: Context) {
  ctx.cache.set('key', 'value')   // ✅ 型正しい
  ctx.cache.invalid()             // ❌ コンパイルエラー:メソッドが存在しない
  ctx.cache.get('key').foo()      // ⚠️ any 型、さらなる型付けが必要
}

(4) ジェネリックサービス

TYPESCRIPT
export default class CacheService<T = any> extends Service {
  private cache = new Map<string, T>()

  get(key: string): T | undefined {
    return this.cache.get(key)
  }

  set(key: string, value: T): void {
    this.cache.set(key, value)
  }
}

7. Service と関数プラグインの比較

(1) 機能比較

次元 Service プラグイン 関数プラグイン
形式 クラス 関数/オブジェクト
状態管理 インスタンスプロパティ クロージャ変数
サービス公開 super(ctx, name) 自動登録 ctx.provide() 手動登録
依存宣言 static inject export const inject
ライフサイクル コンストラクタ/破棄 Apply/自動クリーンアップ
継承 ✅ サポート ❌ 非サポート
テスト容易性 ✅ モックしやすい ⚠️ ctx のモックが必要
コード量 多い 少ない

(2) 選択ガイド

TEXT 📖 参照専用
Service を選ぶケース:
  → 他のプラグインに API を公開する必要がある
  → ステートフルなデータを維持する必要がある
  → 継承と再利用が必要
  → 複数プラグインの基盤依存として機能する

関数プラグインを選ぶケース:
  → ツールとリスナーの登録のみ
  → ステートレスまたは単純な状態
  → 最小コード、迅速開発
  → 他のプラグインから依存される必要がない

(3) 混合使用

両方の形式は同一プロジェクト内で共存可能:

TYPESCRIPT
// CacheService — Service プラグイン
export default class CacheService extends Service { ... }

// CacheTool — 関数プラグイン、CacheService を消費
export const inject = ['cache', 'tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'cache_get',
    // ...
    async execute({ key }, ctx) {
      return ctx.cache.get(key)
    }
  }))
}

(4) 関数プラグインから Service への移行

関数プラグインが複雑化したら Service に移行:

TYPESCRIPT
// 変更前:関数プラグイン
export function apply(ctx: Context) {
  const cache = new Map()
  ctx.provide('cache', {
    get: (k) => cache.get(k),
    set: (k, v) => cache.set(k, v)
  })
}

// 変更後:Service プラグイン
export default class CacheService extends Service {
  private cache = new Map()
  
  constructor(ctx: Context) {
    super(ctx, 'cache')
  }
  
  get(k: string) { return this.cache.get(k) }
  set(k: string, v: any) { this.cache.set(k, v) }
}

❓ よくある質問

Q 1つの Service で複数のサービス名を登録できますか?
A 技術的にはコンストラクタで super を複数回呼び出すことは可能ですが、非推奨です。Service は1つのサービス名を登録すべき——単一責任の原則。
Q Service の this.ctx とは?
A this.ctx はコンストラクタで受け取った ctx パラメータで、Service 基底クラスが自動的に保存します。プラグインのコンテキストで、注入されたすべてのサービスにアクセス可能です。
Q Service の破棄ロジックの書き方は?
A dispose() メソッドをオーバーライド:typescript export default class DbService extends Service { private pool:Pool dispose() { this.pool.end() }
Q 関数プラグインの ctx.provide() と Service の違いは?
A 機能的には同等。違いは Service がクラスインスタンスと継承サポートを持つこと;ctx.provide() は単にオブジェクトを手動登録するだけです。
Q Service は別の Service に依存できますか?
A はい。static inject で依存を宣言し、コンストラクタで ctx.serviceName を通じてアクセスします。
Q サードパーティ Service の型拡張の書き方は?
A .d.ts ファイルで declare module '@deepseek-ai/cordis' を宣言し、Context インターフェースを拡張。この宣言ファイルが TypeScript コンパイラに含まれることを確認してください。

📖 まとめ


📝 練習問題

1. ⭐ 基礎RateLimiterService を書き、check(key):boolean メソッド(1分間に最大60回)を実装してください。rate-limiter サービスとして登録し、別の関数プラグインから inject で使用すること。

2. ⭐⭐ 応用:CacheService の型拡張(declare module)を追加し、ctx.cache.get() がコンシューマプラグインで適切に型付けされるようにしてください。コンシューマプラグインを書き、TypeScript の型チェックが通ることを確認。

3. ⭐⭐⭐ チャレンジMetricsService を実装し、ツール呼び出し回数とタイミング統計を収集してください。tools サービスに依存し、ツール実行の前後でデータを記録。getStats():Record<string, { count, avgMs }> メソッドを提供。Web UI に metrics コマンドを登録して統計を表示すること。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%