はじめに
単一責任原則(Single Responsibility Principle, SRP)は、ロバート・C・マーティン氏が提唱したSOLID原則の一つで、「クラスが変更される理由は1つだけであるべき」という考え方です。
マーティン氏は後年、この「理由(reason)」を「アクター(actor)」すなわち「利害関係者」として補足しました。つまり、異なる役割を持つ人々からの変更要求に対して、クラスは単一の責任のみを負うべきということです。
AIが開発現場に浸透した現在、新しい「アクター」が登場し、この原則の適用範囲と粒度に変化が生まれていると思います。
今回は、「AI以前」と「AI以後」で単一責任原則の適用がどう変わったかを個人的な視点で考察してみました。
この記事では、サンプルとして Python/JavaScript のコードを利用しています。
AI以前の単一責任原則
従来の考え方
AI導入前は、単一責任原則は主に以下の観点で語られていました。
[ 問題のあるパターン ]
一つのクラスが複数の全く異なる責任を持ってしまうケースです。以下の例では、Userクラスがユーザー情報の管理・メール送信・データベース操作という3つの異なる責任を持っています。
// ❌ 悪い例:複数の責任を持つクラス
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
// ユーザー情報の管理
updateProfile(newName) {
this.name = newName;
}
// メール送信(異なる責任)
sendWelcomeEmail() {
// メール送信ロジック
}
// データベース操作(さらに異なる責任)
saveToDatabase() {
// DB保存ロジック
}
}
[ 改善されたパターン ]
それぞれの責任を独立したクラスに分離することで、変更理由が明確になります。メール送信の仕様が変わってもUserクラスは影響を受けません。
// ✅ 良い例:責任を分離
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
updateProfile(newName) {
this.name = newName;
}
}
class EmailService {
sendWelcomeEmail(user) {
// メール送信ロジック
}
}
class UserRepository {
save(user) {
// DB保存ロジック
}
}
AI以前の課題
- 手動での責任分析 : 開発者が経験と直感で責任を判断
- 過度な分離 : 小さすぎるクラスの乱立
- 責任の境界が曖昧 : 何が「単一の責任」かの判断が困難
- アクターの概念の欠如 : 「誰からの変更要求か」という視点が不十分
AI以後の単一責任原則
新しい視点での責任分離
AI時代では、以下の新しい「アクター(利害関係者)」と観点が加わりました。
- データサイエンティスト : AIモデルの精度向上やハイパーパラメータ調整
- MLOps担当者 : モデルのデプロイメント、監視、バージョン管理
- データガバナンス担当者 : データの品質、セキュリティ、コンプライアンス
- プロンプトエンジニア : プロンプトの設計と最適化
1. AI処理の責任分離
AI処理が含まれる場合、従来以上に責任分離が重要になります。AI処理は不確実性が高く、エラーハンドリングやデータ前処理が複雑になるためです。
[ 従来の問題のあるパターン ]
一つのクラスがテキスト抽出からAI分析、結果保存、通知まで全てを担当しています。これでは、AI分析の部分だけを変更したい場合でも、他の処理に影響を与えてしまいます。
# ❌ AI以前的な考え方
class DocumentProcessor:
def __init__(self):
self.ai_model = load_model()
def process_document(self, document):
# テキスト抽出
text = self.extract_text(document)
# AI分析
analysis = self.ai_model.analyze(text)
# 結果保存
self.save_results(analysis)
# 通知送信
self.send_notification(analysis)
[ 改善されたパターン ]
各処理段階を独立したクラスに分離し、DocumentProcessorはオーケストレーション(全体の流れの制御)のみを担当します。これにより、AIモデルを変更しても他の処理は影響を受けません。
# ✅ AI以後的な考え方
class TextExtractor:
"""テキスト抽出の責任のみ"""
def extract(self, document):
return extracted_text
class AIAnalyzer:
"""AI分析の責任のみ"""
def __init__(self):
self.model = load_model()
def analyze(self, text):
return self.model.analyze(text)
class ResultsRepository:
"""結果保存の責任のみ"""
def save(self, analysis):
# DB保存
class NotificationService:
"""通知送信の責任のみ"""
def send(self, analysis):
# 通知送信
class DocumentProcessor:
"""オーケストレーションの責任"""
def __init__(self):
self.extractor = TextExtractor()
self.analyzer = AIAnalyzer()
self.repository = ResultsRepository()
self.notifier = NotificationService()
def process(self, document):
text = self.extractor.extract(document)
analysis = self.analyzer.analyze(text)
self.repository.save(analysis)
self.notifier.send(analysis)
2. プロンプトエンジニアリングの責任分離
AIを使用する際、プロンプトの作成が非常に重要になります。プロンプトは頻繁に調整が必要で、ビジネスロジックとは独立して管理すべき要素です。
[ 問題のあるパターン ]
プロンプトの生成、AI処理、結果の整形、ログ記録が一つのメソッドに混在しています。プロンプトを調整したいだけでも、全体のロジックを理解する必要があります。
# ❌ プロンプトとロジックが混在
class ChatBot:
def generate_response(self, user_input, context):
prompt = f"""
あなたは親切なアシスタントです。
ユーザーの質問: {user_input}
文脈: {context}
以下の点を考慮して回答してください:
- 丁寧な言葉遣い
- 具体的な例を含める
- 150文字以内で回答
"""
response = self.ai_model.generate(prompt)
formatted_response = self.format_response(response)
# ログ記録
self.log_interaction(user_input, response)
return formatted_response
[ 改善されたパターン ]
プロンプト生成、レスポンス整形、ログ記録をそれぞれ独立したクラスに分離します。これにより、プロンプトの調整やログ形式の変更が他の処理に影響を与えません。
# ✅ 責任を分離
class PromptTemplate:
"""プロンプト生成の責任"""
@staticmethod
def create_assistant_prompt(user_input, context):
return f"""
あなたは親切なアシスタントです。
ユーザーの質問: {user_input}
文脈: {context}
以下の点を考慮して回答してください:
- 丁寧な言葉遣い
- 具体的な例を含める
- 150文字以内で回答
"""
class ResponseFormatter:
"""レスポンス整形の責任"""
def format(self, raw_response):
return formatted_response
class InteractionLogger:
"""ログ記録の責任"""
def log(self, user_input, response):
# ログ記録
class ChatBot:
"""チャット機能の責任"""
def __init__(self):
self.formatter = ResponseFormatter()
self.logger = InteractionLogger()
def generate_response(self, user_input, context):
prompt = PromptTemplate.create_assistant_prompt(user_input, context)
response = self.ai_model.generate(prompt)
formatted_response = self.formatter.format(response)
self.logger.log(user_input, response)
return formatted_response
AI時代の新しい責任境界
AI処理を含むシステムでは、従来の責任分離に加えて、以下のような新しい責任の境界が生まれます。
1. データ処理の責任
AI処理には、入力データの前処理、検証、推論、後処理という明確な段階があります。それぞれを独立したクラスに分離することで、各段階の変更が他に影響しません。
class DataPreprocessor:
"""AI用データ前処理の責任"""
def preprocess_for_ai(self, raw_data):
# データのクリーニング、正規化、フォーマット変換
return processed_data
class DataValidator:
"""データ検証の責任"""
def validate(self, data):
# データの整合性チェック、必須項目の確認
return is_valid, errors
class AIModelWrapper:
"""AI推論の責任"""
def predict(self, data):
# AIモデルの実行とエラーハンドリング
return prediction
class PostProcessor:
"""AI結果の後処理責任"""
def process_output(self, raw_output):
# AIの出力を業務で使用可能な形式に変換
return processed_output
2. エラーハンドリングの責任
AI処理では、モデル固有のエラーや制限に対する特別な処理が必要です。これらを専用のクラスに分離することで、エラー対応の変更が他の処理に影響しません。
class AIErrorHandler:
"""AI特有のエラー処理責任"""
def handle_model_error(self, error):
# モデルの応答エラーに対する対処
pass
def handle_token_limit_error(self, error):
# トークン制限エラーに対する対処(テキスト分割など)
pass
class RetryStrategy:
"""リトライ戦略の責任"""
def should_retry(self, error):
# エラーの種類に応じたリトライ判定
return retry_decision
def get_retry_delay(self, attempt):
# 指数バックオフによる待機時間の計算
return min(2 ** attempt, 60) # 最大60秒
class CircuitBreaker:
"""サーキットブレーカーの責任"""
def __init__(self, failure_threshold=5, timeout=30):
self.failure_count = 0
self.failure_threshold = failure_threshold
self.timeout = timeout
self.last_failure_time = None
self.state = "CLOSED" # CLOSED, OPEN, HALF_OPEN
def call(self, func, *args, **kwargs):
if self.state == "OPEN":
if time.time() - self.last_failure_time > self.timeout:
self.state = "HALF_OPEN"
else:
raise Exception("Circuit breaker is OPEN")
try:
result = func(*args, **kwargs)
self.reset()
return result
except Exception as e:
self.record_failure()
raise
実践的な適用例
ケーススタディ - AI搭載のブログ執筆支援ツール
実際のAI搭載システムで、責任分離の Before/After を見てみましょう。
[ 従来の問題のあるアプローチ ]
一つのクラスがブログ執筆に関わる全ての処理を担当しています。記事構成の生成方法を変更したい場合、SEOや画像生成のロジックも理解する必要があります。
# AI以前のモノリシックな設計
class BlogWriter:
def write_blog_post(self, topic, keywords):
# 記事構成生成
outline = self.generate_outline(topic)
# 記事執筆
content = self.write_content(outline, keywords)
# SEO最適化
optimized_content = self.optimize_seo(content, keywords)
# 画像生成
images = self.generate_images(topic)
# 記事保存
self.save_post(optimized_content, images)
return optimized_content
[ 改善されたアプローチ ]
各処理段階を独立したクラスに分離し、BlogWriterOrchestratorが全体の流れを制御します。これにより、AIモデルの変更や新機能の追加が他の処理に影響しません。また、ユニットテストでAI呼び出しをモック化することで、テストの独立性も保たれます。
# AI以後の責任分離設計
class OutlineGenerator:
"""記事構成生成の責任"""
def generate(self, topic):
prompt = f"以下のトピックで記事の構成を作成してください: {topic}"
return self.ai_model.generate(prompt)
class ContentWriter:
"""記事執筆の責任"""
def write(self, outline, keywords):
prompt = f"以下の構成で記事を執筆してください: {outline}"
return self.ai_model.generate(prompt)
class SEOOptimizer:
"""SEO最適化の責任"""
def optimize(self, content, keywords):
# メタタグ生成、見出し最適化、キーワード密度調整
return optimized_content
class ImageGenerator:
"""画像生成の責任"""
def generate(self, topic):
prompt = f"以下のトピックの画像を生成: {topic}"
return self.image_ai.generate(prompt)
class BlogRepository:
"""記事保存の責任"""
def save(self, content, images):
# データベースへの保存処理
pass
class BlogWriterOrchestrator:
"""全体のオーケストレーション責任"""
def __init__(self):
self.outline_generator = OutlineGenerator()
self.content_writer = ContentWriter()
self.seo_optimizer = SEOOptimizer()
self.image_generator = ImageGenerator()
self.repository = BlogRepository()
self.circuit_breaker = CircuitBreaker() # 横断的関心事
self.tracer = DistributedTracer() # 分散トレーシング
def create_blog_post(self, topic, keywords):
with self.tracer.start_span("blog_creation"):
outline = self.circuit_breaker.call(
self.outline_generator.generate, topic
)
content = self.content_writer.write(outline, keywords)
optimized_content = self.seo_optimizer.optimize(content, keywords)
images = self.image_generator.generate(topic)
self.repository.save(optimized_content, images)
return optimized_content
# ユニットテストでのAI呼び出しのモック化例
import unittest.mock as mock
class TestBlogWriter(unittest.TestCase):
def test_create_blog_post(self):
# AI呼び出しをモック化してテストの独立性を保つ
with mock.patch.object(OutlineGenerator, 'generate') as mock_outline:
mock_outline.return_value = "テスト構成"
orchestrator = BlogWriterOrchestrator()
result = orchestrator.create_blog_post("テストトピック", ["キーワード"])
mock_outline.assert_called_once_with("テストトピック")
AI時代の単一責任原則のベストプラクティス
重要な判断基準:過度な分割を避ける
SRPの過剰適用で「1ファイル=1メソッド」のように極端に分割すると、かえって可読性・パフォーマンスが低下します。「変わる可能性があるかどうか」と「異なるアクターからの変更要求かどうか」を基準に分割することが重要です。
1. AI処理の段階的分離
AI処理では、データの前処理、検証、推論、後処理、エラーハンドリングという明確な段階があります。これらを独立したクラスに分離することで、各段階の変更や改良が他の処理に影響しません。
# 推奨パターン
class AIWorkflow:
def __init__(self):
self.preprocessor = DataPreprocessor() # 前処理
self.validator = DataValidator() # 検証
self.ai_service = AIService() # AI処理
self.postprocessor = PostProcessor() # 後処理
self.error_handler = AIErrorHandler() # エラー処理
2. プロンプトの責任分離
プロンプトは頻繁に調整される要素であり、ビジネスロジックから独立して管理する必要があります。現場では、プロンプトをバージョン管理し、A/Bテストやフィーチャーフラグで切り替える運用が主流です。
class PromptManager:
"""プロンプト管理の責任"""
def get_system_prompt(self, version="v1.0"):
# バージョン管理されたプロンプトの取得
return self.prompt_repository.get_system_prompt(version)
def get_user_prompt(self, context, template="default"):
# テンプレートベースのプロンプト生成
return f"以下の条件で..."
class PromptValidator:
"""プロンプト検証の責任"""
def validate_length(self, prompt):
# トークン数の制限チェック
return len(prompt) < MAX_TOKENS
def validate_content(self, prompt):
# 不適切な内容のチェック
return not self.contains_unsafe_content(prompt)
class PromptVersionManager:
"""プロンプトバージョニングの責任"""
def __init__(self):
self.feature_flags = FeatureFlags()
def get_active_prompt_version(self, user_id):
# A/Bテストやフィーチャーフラグに基づくプロンプト選択
if self.feature_flags.is_enabled("new_prompt_v2", user_id):
return "v2.0"
return "v1.0"
3. 設定管理の責任分離
AI関連の設定は複雑で頻繁に変更されるため、APIの設定とAIモデルの設定を分離して管理します。また、環境変数以外にもフィーチャーフラグやパラメータストアの活用が重要です。
class AIModelConfig:
"""AI モデル設定の責任"""
def __init__(self):
self.temperature = 0.7 # 生成の創造性
self.max_tokens = 1000 # 最大トークン数
self.model_name = "gpt-4" # 使用モデル
class APIConfig:
"""API設定の責任"""
def __init__(self):
self.api_key = os.getenv("API_KEY")
self.base_url = "https://api.openai.com"
self.timeout = 30 # タイムアウト秒数
class ParameterStoreConfig:
"""外部パラメータストアの責任"""
def __init__(self):
self.ssm = boto3.client('ssm') # AWS Systems Manager
def get_parameter(self, name):
response = self.ssm.get_parameter(Name=name, WithDecryption=True)
return response['Parameter']['Value']
class FeatureFlags:
"""フィーチャーフラグの責任"""
def is_enabled(self, flag_name, user_id):
# フィーチャーフラグサービス(LaunchDarkly等)との連携
return self.flag_service.is_enabled(flag_name, user_id)
まとめ
AI時代の単一責任原則は、原則そのものが変質したわけではありません。変わったのは、AIがもたらす新しい「責任」と「アクター」が増えたことで、適用範囲と粒度が拡大したのです。
変化したポイント
変化したポイントをまとめてみました。
- 新しいアクターの登場: データサイエンティスト、MLOps担当者、プロンプトエンジニアなど
- AI処理の段階的分離: 前処理→AI処理→後処理の明確な分離
- プロンプトエンジニアリングの独立: プロンプト生成とビジネスロジックの分離
- 高度なエラーハンドリング: サーキットブレーカー、指数バックオフなどの専門的な処理
- 設定管理の細分化: フィーチャーフラグ、パラメータストア活用による動的設定管理
新しい責任の境界
- データ変換責任: AI用のデータ形式変換
- プロンプト生成責任: 適切なプロンプトの構築とバージョン管理
- AI推論責任: モデルの実行と結果取得
- 結果解釈責任: AI出力の意味解釈と変換
- エラー回復責任: AI特有のエラーからの回復と監視
ということで..
AI時代の単一責任原則は、より複雑で専門的な責任分離を要求しますが、その分、保守性と拡張性の高いシステムを構築できるようになりました!!
重要なのは、AI処理を「ブラックボックス」として扱うのではなく、その前後の処理も含めて 適切に責任を分離し、変更される可能性のある要素を独立させる こと。これにより、AIの進化に合わせてシステムも柔軟に進化させることができるのではないかと思いました。