アーキテクチャ設計ガイド - クリーンアーキテクチャと GTD 基盤¶
はじめに¶
このドキュメントでは、Kage アプリケーションで採用しているクリーンアーキテクチャに基づく設計思想について、Python 初学者向けに分かりやすく説明します。本プロジェクトは、GTD(Getting Things Done)手法をベースとしたタスク管理システムとして、保守性・拡張性・テスタビリティを重視した設計になっています。
プロジェクト概要¶
技術スタック(2025 年 8 月現在)¶
- UI フレームワーク: Flet 0.27.6
- ORM: SQLModel 0.0.24
- AI/Agent: LangChain 0.3.26 + LangGraph 0.4.9
- パッケージ管理: uv 0.7.3
- 開発ツール: Ruff, Pyright, pytest
- データベース: SQLite(Alembic 対応)
GTD 基盤の実装¶
Kage は以下の GTD 核心概念を実装しています:
- Inbox: すべてのタスクの受け皿
- Next Action: 次に取る具体的なアクション
- Waiting For: 他者の対応待ち項目
- Someday/Maybe: いつかやるかもしれない項目
- Projects: 複数のアクションを伴う成果
- Delegated: 委譲されたタスク
なぜアーキテクチャが重要なのか?¶
悪い例:すべてを一つのファイルに書く場合¶
# ❌ 悪い例:すべてが混在している
import flet as ft
from sqlmodel import Session, create_engine, SQLModel, Field
import uuid
from datetime import datetime
# データベース、UI、ビジネスロジックがすべて混在
class Task(SQLModel, table=True):
id: uuid.UUID = Field(primary_key=True)
title: str
completed: bool = False
def main():
engine = create_engine("sqlite:///tasks.db")
def on_add_task(e):
# UIからデータを取得
title = title_field.value
# バリデーション(ビジネスロジック)
if not title:
show_error("タイトルを入力してください")
return
if len(title) > 100:
show_error("タイトルは100文字以内で入力してください")
return
# データベースに直接保存(データアクセス)
with Session(engine) as session:
task = Task(id=uuid.uuid4(), title=title)
session.add(task)
session.commit()
# UIを更新(プレゼンテーション)
refresh_task_list()
# UIコンポーネント作成
title_field = ft.TextField(label="タスクタイトル")
# ...
この書き方の問題点:
- 単一責任原則違反: 一つの関数で多くの責任を負っている
- テストが困難: データベース、UI、ビジネスロジックが密結合
- 修正時の影響が大きい: 一箇所の変更が他の部分に波及しやすい
- コードの再利用ができない: 特定の UI に依存したロジック
- 拡張が困難: 新機能追加時に既存コードに大きな影響
良い例:クリーンアーキテクチャに基づく責任分離¶
# ✅ 良い例:責任が明確に分離されている
# models/task.py - データ構造とGTDの実装
from enum import Enum
from sqlmodel import SQLModel, Field
import uuid
class TaskStatus(str, Enum):
"""GTDシステムに基づくタスクステータス"""
INBOX = "inbox"
NEXT_ACTION = "next_action"
WAITING_FOR = "waiting_for"
SOMEDAY_MAYBE = "someday_maybe"
DELEGATED = "delegated"
COMPLETED = "completed"
CANCELLED = "cancelled"
class TaskBase(SQLModel):
"""タスクの基本モデル"""
title: str = Field(index=True)
description: str = Field(default="")
status: TaskStatus = Field(default=TaskStatus.INBOX, index=True)
class TaskCreate(TaskBase):
"""新規作成時に使用(IDは不要)"""
pass
class TaskRead(TaskBase):
"""読み取り時に使用(IDが必要)"""
id: uuid.UUID
# logic/services/task_service.py - ビジネスロジック
class TaskService:
def __init__(self, repository: TaskRepository):
self.repository = repository
def create_task(self, command: TaskCreate) -> TaskRead:
"""GTDルールに従ってタスクを作成"""
if not command.title.strip():
raise TaskServiceCreateError("タスクのタイトルは必須です")
return self.repository.create(command)
# logic/application/task_application_service.py - アプリケーションサービス
class TaskApplicationService:
"""View層からSession管理を分離"""
def create_task(self, command: CreateTaskCommand) -> TaskRead:
with SqlModelUnitOfWork() as uow:
service_factory = create_service_factory(uow.session)
task_service = service_factory.create_task_service()
return task_service.create_task(command.to_create_model())
# views/task/view.py - UI表示
class TaskView(BaseView):
def __init__(self):
self.task_app_service = get_application_service_container().get_task_application_service()
def on_add_task(self, e):
"""UIからアプリケーションサービスを呼び出すだけ"""
try:
command = CreateTaskCommand(title=self.title_field.value)
task = self.task_app_service.create_task(command)
self.refresh_task_list()
except TaskServiceCreateError as e:
self.show_error(str(e))
クリーンアーキテクチャとレイヤー構造¶
本プロジェクトは、ロバート・C・マーティンのクリーンアーキテクチャ原則に基づき、関心事の分離を徹底したレイヤードアーキテクチャを採用しています。
┌─────────────────────────────────┐
│ Views Layer │ 🎨 UIとユーザーインタラクション
│ (src/views/) │
└──────────────┬──────────────────┘
│ Commands/Events
┌──────────────▼──────────────────┐
│ Application Layer │ 🔄 セッション管理とワークフロー調整
│ (src/logic/application/) │
└──────────────┬──────────────────┘
│ Business Logic
┌──────────────▼──────────────────┐
│ Services Layer │ 🧠 ビジネスルールとGTDロジック
│ (src/logic/services/) │
└────────┬──────────┬─────────────┘
│ │
┌────────▼────┐ ┌──▼─────────────┐
│ Models │ │ Agents │ 🤖 LLM/AI自動化
│ (src/models)│ │ (src/agents/) │
└─────────────┘ └────────────────┘
│
┌────────▼──────────────────┐
│ Infrastructure │ 💾 データ永続化とリポジトリ
│ (src/logic/repositories/) │
└───────────────────────────┘
各層の責務¶
- Views Layer: Flet を使用した UI 表示、ユーザー入力処理、Application Service の呼び出し
- Application Layer: トランザクション境界管理、複数サービスの調整、セッション管理
- Services Layer: GTD ビジネスルール、ドメインロジック、バリデーション
- Models Layer: エンティティ定義、データ構造、型安全性
- Agents Layer: LangChain/LangGraph による AI 自動化、複雑なタスクの委譲
- Infrastructure Layer: データアクセス、リポジトリパターン、外部システム連携
モデル層(Model Layer)の詳細設計¶
1. GTD ベースのデータ構造定義¶
モデル層では、GTD 手法に基づいたタスク管理の概念をデータ構造として表現します。
# models/task.py - 実際のプロジェクト実装例
from enum import Enum
from datetime import date
from sqlmodel import Field, SQLModel
import uuid
class TaskStatus(str, Enum):
"""GTDシステムに基づくタスクステータス"""
INBOX = "inbox" # 受信箱(未分類)
NEXT_ACTION = "next_action" # 次のアクション
WAITING_FOR = "waiting_for" # 他者の対応待ち
SOMEDAY_MAYBE = "someday_maybe" # いつかやるかもしれない
DELEGATED = "delegated" # 委譲済み
COMPLETED = "completed" # 完了
CANCELLED = "cancelled" # キャンセル
class TaskBase(SQLModel):
"""タスクの基本属性を定義"""
project_id: uuid.UUID | None = Field(default=None, foreign_key="project.id", index=True)
parent_id: uuid.UUID | None = Field(default=None, foreign_key="task.id", index=True)
title: str = Field(index=True) # 検索用インデックス
description: str = Field(default="")
status: TaskStatus = Field(default=TaskStatus.INBOX, index=True) # ステータス検索用
due_date: date | None = Field(default=None) # 締切日
2. CQRS 対応の型安全なモデル設計¶
Command Query Responsibility Segregation (CQRS) パターンに基づき、用途別にモデルを分離:
# 作成用モデル(IDは不要)
class TaskCreate(TaskBase):
"""新規タスク作成時に使用"""
pass
# 読み取り用モデル(IDが必要)
class TaskRead(TaskBase):
"""タスク取得・表示時に使用"""
id: uuid.UUID
created_at: datetime
updated_at: datetime
# 更新用モデル(変更フィールドのみ)
class TaskUpdate(SQLModel):
"""タスク更新時に使用(部分更新対応)"""
title: str | None = None
description: str | None = None
status: TaskStatus | None = None
due_date: date | None = None
# データベーステーブル定義
class Task(TaskBase, table=True):
"""実際のデータベーステーブル"""
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
3. 関連エンティティとの連携¶
# models/project.py - プロジェクト管理
class Project(SQLModel, table=True):
"""GTDのプロジェクト概念を実装"""
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
name: str = Field(index=True)
description: str = Field(default="")
status: ProjectStatus = Field(default=ProjectStatus.ACTIVE)
# models/tag.py - タグシステム
class Tag(SQLModel, table=True):
"""コンテキストとタグ管理"""
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
name: str = Field(unique=True, index=True)
color: str = Field(default="#808080")
# models/task_tag.py - 多対多関係
class TaskTag(SQLModel, table=True):
"""タスクとタグの関係テーブル"""
task_id: uuid.UUID = Field(foreign_key="task.id", primary_key=True)
tag_id: uuid.UUID = Field(foreign_key="tag.id", primary_key=True)
なぜ複数のモデルを作るのか?¶
理由 1: 型安全性の確保¶
# ❌ 悪い例:単一モデルでの曖昧な使用
def create_task(task: Task) -> Task: # IDが含まれてしまう可能性
pass
# ✅ 良い例:意図が明確
def create_task(task_data: TaskCreate) -> TaskRead: # IDは不要、戻り値はIDを含む
pass
理由 2: セキュリティとバリデーション¶
# API経由での不正なID指定を防止
@app.post("/api/tasks")
def create_task_endpoint(task: TaskCreate): # IDフィールドが存在しない
return task_service.create_task(task)
理由 3: 部分更新の最適化¶
# 必要な項目のみを更新
def update_task_status(task_id: uuid.UUID, status: TaskStatus) -> TaskRead:
update_data = TaskUpdate(status=status) # titleやdescriptionは変更しない
return repository.update(task_id, update_data)
サービス層(Services Layer)の詳細設計¶
1. GTD ビジネスルールの実装¶
サービス層は「GTD の原則とアプリケーションの仕様」を実装します。
# logic/services/task_service.py - 実際のプロジェクト実装
class TaskService(ServiceBase):
"""タスクのビジネスロジックを管理"""
def __init__(self, task_repo: TaskRepository, project_repo: ProjectRepository):
self.task_repo = task_repo
self.project_repo = project_repo
def create_task(self, task_data: TaskCreate) -> TaskRead:
"""GTDルールに従ってタスクを作成"""
# ビジネスルール1: タイトルは必須
if not task_data.title.strip():
raise TaskServiceCreateError("タスクのタイトルは必須です")
# ビジネスルール2: プロジェクトの存在確認
if task_data.project_id:
project = self.project_repo.get_by_id(task_data.project_id)
if not project:
raise TaskServiceCreateError("指定されたプロジェクトが存在しません")
# ビジネスルール3: 新規タスクは自動的にInboxに配置(GTD原則)
if not task_data.status:
task_data.status = TaskStatus.INBOX
return self.task_repo.create(task_data)
def move_to_next_action(self, task_id: uuid.UUID) -> TaskRead:
"""タスクをNext Actionに移動(GTDワークフロー)"""
task = self.task_repo.get_by_id(task_id)
if not task:
raise TaskServiceError("タスクが見つかりません")
# GTDルール: Inboxからのみ Next Action に移動可能
if task.status != TaskStatus.INBOX:
raise TaskServiceError("InboxのタスクのみNext Actionに移動できます")
update_data = TaskUpdate(status=TaskStatus.NEXT_ACTION)
return self.task_repo.update(task_id, update_data)
2. 複雑な GTD ワークフローの実装¶
class TaskService(ServiceBase):
def process_inbox_item(self, task_id: uuid.UUID, decision: InboxDecision) -> TaskRead:
"""Inboxアイテムの処理(GTDの核心プロセス)"""
task = self.task_repo.get_by_id(task_id)
match decision.action:
case InboxAction.DELETE:
# 不要なタスクを削除
self.task_repo.delete(task_id)
return None
case InboxAction.DELEGATE:
# タスクを委譲
update_data = TaskUpdate(
status=TaskStatus.DELEGATED,
description=f"Delegated to: {decision.delegate_to}"
)
return self.task_repo.update(task_id, update_data)
case InboxAction.DO_NOW:
# 2分以内で完了可能なタスクはすぐに実行
update_data = TaskUpdate(status=TaskStatus.COMPLETED)
return self.task_repo.update(task_id, update_data)
case InboxAction.SCHEDULE:
# 特定の日時に実行するタスク
update_data = TaskUpdate(
status=TaskStatus.NEXT_ACTION,
due_date=decision.scheduled_date
)
return self.task_repo.update(task_id, update_data)
case InboxAction.SOMEDAY_MAYBE:
# いつかやるかもしれないタスク
update_data = TaskUpdate(status=TaskStatus.SOMEDAY_MAYBE)
return self.task_repo.update(task_id, update_data)
def get_next_actions_by_context(self, context_tags: list[str]) -> list[TaskRead]:
"""コンテキスト別のNext Action取得(GTD実践)"""
return self.task_repo.get_by_status_and_tags(
TaskStatus.NEXT_ACTION,
context_tags
)
3. Repository パターンによるデータアクセス分離¶
# logic/repositories/task.py - 実際のプロジェクト実装
class TaskRepository(BaseRepository[Task, TaskCreate, TaskUpdate]):
"""タスクのデータアクセスを担当"""
def get_by_status_and_tags(self, status: TaskStatus, tag_names: list[str]) -> list[Task]:
"""ステータスとタグでタスクを検索(GTD実践用)"""
stmt = (
select(Task)
.where(Task.status == status)
.join(TaskTag)
.join(Tag)
.where(Tag.name.in_(tag_names))
)
return self.session.exec(stmt).all()
def get_overdue_tasks(self) -> list[Task]:
"""期限切れタスクの取得"""
today = date.today()
stmt = select(Task).where(
and_(
Task.due_date < today,
Task.status.in_([TaskStatus.NEXT_ACTION, TaskStatus.WAITING_FOR])
)
)
return self.session.exec(stmt).all()
def get_tasks_by_project(self, project_id: uuid.UUID) -> list[Task]:
"""プロジェクト別タスク取得"""
stmt = select(Task).where(Task.project_id == project_id)
return self.session.exec(stmt).all()
アプリケーション層(Application Layer)の設計¶
アプリケーション層は、ビューからビジネスロジックを完全に分離し、トランザクション境界とセッション管理を担当します。
1. Application Service パターン¶
# logic/application/task_application_service.py - 実際のプロジェクト実装
class TaskApplicationService(BaseApplicationService):
"""View層からSession管理を分離し、ビジネスロジックを調整"""
def create_task(self, command: CreateTaskCommand) -> TaskRead:
"""タスク作成のワークフロー調整"""
with SqlModelUnitOfWork() as uow:
# サービスファクトリでDIコンテナから取得
service_factory = create_service_factory(uow.session)
task_service = service_factory.create_task_service()
# ビジネスロジックの実行
task = task_service.create_task(command.to_create_model())
# トランザクションコミットは UnitOfWork が管理
return task
def process_gtd_inbox_review(self, decisions: list[InboxDecision]) -> list[TaskRead]:
"""GTD Inbox Review の一括処理"""
results = []
# 単一トランザクションで複数タスクを処理
with SqlModelUnitOfWork() as uow:
service_factory = create_service_factory(uow.session)
task_service = service_factory.create_task_service()
for decision in decisions:
try:
result = task_service.process_inbox_item(
decision.task_id,
decision
)
results.append(result)
logger.info(f"Processed task {decision.task_id}: {decision.action}")
except TaskServiceError as e:
# エラーが発生した場合、全体をロールバック
logger.error(f"Failed to process {decision.task_id}: {e}")
raise
return results
2. Command/Query パターンの実装¶
# logic/commands/task_commands.py - コマンドパターン
@dataclass
class CreateTaskCommand:
"""タスク作成コマンド"""
title: str
description: str = ""
project_id: uuid.UUID | None = None
due_date: date | None = None
def to_create_model(self) -> TaskCreate:
"""ドメインモデルに変換"""
return TaskCreate(
title=self.title,
description=self.description,
project_id=self.project_id,
due_date=self.due_date
)
# logic/queries/task_queries.py - クエリパターン
@dataclass
class GetTasksByStatusQuery:
"""ステータス別タスク取得クエリ"""
status: TaskStatus
limit: int = 100
offset: int = 0
class TaskQuery:
"""タスククエリサービス"""
def get_tasks_by_status(self, query: GetTasksByStatusQuery) -> list[TaskRead]:
with SqlModelUnitOfWork() as uow:
repo = TaskRepository(Task, uow.session)
tasks = repo.get_by_status(query.status, query.limit, query.offset)
return [TaskRead.model_validate(task) for task in tasks]
3. 依存性注入とファクトリパターン¶
# logic/container.py - DI コンテナの実装
class ServiceContainer:
"""サービスコンテナ(Dependency Injection)"""
def __init__(self) -> None:
self._task_app_service: TaskApplicationService | None = None
self._project_app_service: ProjectApplicationService | None = None
self._tag_app_service: TagApplicationService | None = None
def get_task_application_service(self) -> TaskApplicationService:
"""タスクApplication Serviceを取得(シングルトン)"""
if self._task_app_service is None:
self._task_app_service = TaskApplicationService()
return self._task_app_service
# logic/factory.py - ファクトリパターン
class ServiceFactory:
"""サービスファクトリ(Session注入)"""
def __init__(self, repository_factory: RepositoryFactory):
self.repository_factory = repository_factory
def create_task_service(self) -> TaskService:
"""TaskServiceを生成"""
task_repo = self.repository_factory.create_task_repository()
project_repo = self.repository_factory.create_project_repository()
return TaskService(task_repo, project_repo)
# 使用例
def get_application_service_container() -> ServiceContainer:
"""View層で使用するDIコンテナ取得"""
return service_container # グローバルシングルトン
4. Unit of Work パターンによるトランザクション管理¶
# logic/unit_of_work.py - トランザクション境界管理
class SqlModelUnitOfWork:
"""SQLModel用のUnit of Workパターン実装"""
def __init__(self):
self.session = Session(get_engine())
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.session.commit() # 正常終了時はコミット
else:
self.session.rollback() # 例外発生時はロールバック
self.session.close()
def commit(self):
"""明示的なコミット"""
self.session.commit()
def rollback(self):
"""明示的なロールバック"""
self.session.rollback()
Agent 層(Agent Layer)と AI 統合¶
Agent 層は、LangChain/LangGraph を使用して LLM ベースの自動化と複雑な判断を実装します。
1. GTD 自動化エージェント¶
# agents/gtd_processor.py - GTD処理の自動化
class GTDProcessorAgent:
"""GTD処理を自動化するエージェント"""
def __init__(self):
self.llm = ChatGoogleGenerativeAI(model="gemini-pro")
self.task_service_tool = TaskServiceTool() # Services層のツール化
def auto_categorize_inbox_item(self, task_description: str) -> InboxDecision:
"""Inboxアイテムの自動分類"""
prompt = f"""
以下のタスクをGTDの原則に従って分類してください:
タスク: {task_description}
分類選択肢:
1. DELETE - 不要なタスク
2. DO_NOW - 2分以内で完了可能
3. DELEGATE - 他者に委譲すべき
4. SCHEDULE - 特定日時に実行
5. SOMEDAY_MAYBE - いつかやるかもしれない
理由と共に回答してください。
"""
response = self.llm.invoke(prompt)
return self._parse_categorization_response(response.content)
# agents/task_automation.py - タスク自動化
class TaskAutomationAgent:
"""タスクの自動処理エージェント"""
def suggest_next_actions(self, context: str, available_time: int) -> list[TaskRead]:
"""現在のコンテキストと利用可能時間に基づく推奨アクション"""
# LangGraphで複雑なワークフローを実装
workflow = StateGraph(TaskSuggestionState)
workflow.add_node("analyze_context", self._analyze_context)
workflow.add_node("filter_by_time", self._filter_by_available_time)
workflow.add_node("prioritize_tasks", self._prioritize_tasks)
workflow.add_edge("analyze_context", "filter_by_time")
workflow.add_edge("filter_by_time", "prioritize_tasks")
app = workflow.compile()
initial_state = TaskSuggestionState(
context=context,
available_time=available_time
)
result = app.invoke(initial_state)
return result["suggested_tasks"]
2. LangGraph による複雑なワークフロー¶
# agents/workflows/weekly_review.py - 週次レビューの自動化
class WeeklyReviewWorkflow:
"""GTD週次レビューの自動化ワークフロー"""
def create_review_workflow(self) -> StateGraph:
"""週次レビューのワークフローを構築"""
workflow = StateGraph(WeeklyReviewState)
# ノード定義
workflow.add_node("collect_completed_tasks", self._collect_completed_tasks)
workflow.add_node("review_project_progress", self._review_project_progress)
workflow.add_node("update_someday_maybe", self._update_someday_maybe)
workflow.add_node("plan_next_week", self._plan_next_week)
workflow.add_node("generate_review_report", self._generate_review_report)
# エッジ定義(実行順序)
workflow.add_edge(START, "collect_completed_tasks")
workflow.add_edge("collect_completed_tasks", "review_project_progress")
workflow.add_edge("review_project_progress", "update_someday_maybe")
workflow.add_edge("update_someday_maybe", "plan_next_week")
workflow.add_edge("plan_next_week", "generate_review_report")
workflow.add_edge("generate_review_report", END)
return workflow.compile()
def _collect_completed_tasks(self, state: WeeklyReviewState) -> dict:
"""今週完了したタスクを収集"""
task_service = self.service_container.get_task_application_service()
completed_tasks = task_service.get_completed_tasks_this_week()
return {
"completed_tasks": completed_tasks,
"completion_stats": self._calculate_completion_stats(completed_tasks)
}
3. Agent-Service 連携パターン¶
# agents/tools/task_service_tool.py - Services層のツール化
class TaskServiceTool(BaseTool):
"""TaskServiceをLangChainツールとして公開"""
name = "task_service"
description = "GTDタスク管理システムとの連携"
def _run(self, action: str, **kwargs) -> str:
"""エージェントからサービス層を呼び出し"""
task_app_service = get_application_service_container().get_task_application_service()
match action:
case "create_task":
command = CreateTaskCommand(**kwargs)
task = task_app_service.create_task(command)
return f"タスクを作成しました: {task.title}"
case "get_next_actions":
context = kwargs.get("context", [])
tasks = task_app_service.get_next_actions_by_context(context)
return f"Next Actions: {[task.title for task in tasks]}"
case "complete_task":
task_id = kwargs["task_id"]
task = task_app_service.complete_task(task_id)
return f"タスクを完了しました: {task.title}"
- 責務: LLM を活用した自律的な問題解決、GTD ワークフローの自動化、複数サービスの連携
- 実装:
LangChain 0.3.26
とLangGraph 0.4.9
を使用した状態管理型ワークフロー - 連携: Services 層をツール化してエージェントから利用
- 詳細: Agent 層 設計ガイドを参照してください
設計の利点¶
1. 単一責任の原則¶
- 各クラスが一つの責任のみを持つ
- 修正時の影響範囲が限定される
2. テストしやすさ¶
# ビジネスロジックを単独でテスト可能
## Views層(View Layer)- Flet UI
Views層は、Fletを使用してクロスプラットフォーム対応のモダンなUIを提供します。
### 1. ビューの階層構造
```python
# views/base.py - ベースビュークラス
class BaseView(ABC):
"""全ビューの基底クラス"""
def __init__(self, page: ft.Page, service_container: ServiceContainer):
self.page = page
self.service_container = service_container
self._content: ft.Control | None = None
@abstractmethod
def build(self) -> ft.Control:
"""ビューのUIを構築"""
pass
@property
def content(self) -> ft.Control:
"""ビューのコンテンツを取得(遅延構築)"""
if self._content is None:
self._content = self.build()
return self._content
def refresh(self) -> None:
"""ビューを再構築"""
self._content = None
self.page.update()
# views/main_view.py - メインビュー
class MainView(BaseView):
"""アプリケーションのメインビュー"""
def __init__(self, page: ft.Page, service_container: ServiceContainer):
super().__init__(page, service_container)
self.task_list_view = TaskListView(page, service_container)
self.inbox_view = InboxView(page, service_container)
self.project_view = ProjectView(page, service_container)
def build(self) -> ft.Control:
"""メインレイアウトを構築"""
return ft.Container(
content=ft.Row([
# サイドバー
ft.Container(
content=self._build_sidebar(),
width=200,
bgcolor=ft.colors.SURFACE_VARIANT
),
# メインコンテンツエリア
ft.Container(
content=self._build_main_content(),
expand=True
)
]),
expand=True
)
def _build_sidebar(self) -> ft.Control:
"""サイドバーメニューを構築"""
return ft.Column([
ft.TextButton("📥 Inbox", on_click=self._on_inbox_click),
ft.TextButton("⚡ Next Actions", on_click=self._on_next_actions_click),
ft.TextButton("📋 Projects", on_click=self._on_projects_click),
ft.TextButton("🔍 Contexts", on_click=self._on_contexts_click),
ft.TextButton("⏰ Scheduled", on_click=self._on_scheduled_click),
ft.TextButton("🤔 Someday/Maybe", on_click=self._on_someday_click),
])
2. GTD 特化コンポーネント¶
# views/components/task_component.py - タスクコンポーネント
class TaskComponent(ft.UserControl):
"""GTDタスクを表示するコンポーネント"""
def __init__(self, task: TaskRead, on_complete: Callable[[str], None] = None):
super().__init__()
self.task = task
self.on_complete = on_complete
def build(self) -> ft.Control:
"""タスクカードのUIを構築"""
# GTD分類による色分け
status_color = self._get_status_color(self.task.status)
return ft.Card(
content=ft.Container(
content=ft.Column([
ft.Row([
ft.Checkbox(
value=self.task.status == TaskStatus.DONE,
on_change=self._on_checkbox_change
),
ft.Text(
self.task.title,
style=ft.TextStyle(
decoration=ft.TextDecoration.LINE_THROUGH
if self.task.status == TaskStatus.DONE
else None
),
expand=True
),
self._build_status_chip()
]),
if self.task.description:
ft.Text(
self.task.description,
size=12,
color=ft.colors.ON_SURFACE_VARIANT
),
self._build_metadata_row()
]),
padding=ft.padding.all(16)
),
surface_tint_color=status_color
)
def _get_status_color(self, status: TaskStatus) -> str:
"""GTDステータスに応じた色を取得"""
color_map = {
TaskStatus.INBOX: ft.colors.ORANGE,
TaskStatus.NEXT_ACTION: ft.colors.GREEN,
TaskStatus.WAITING: ft.colors.YELLOW,
TaskStatus.SCHEDULED: ft.colors.BLUE,
TaskStatus.SOMEDAY_MAYBE: ft.colors.PURPLE,
TaskStatus.DONE: ft.colors.GREY,
}
return color_map.get(status, ft.colors.SURFACE)
# views/components/quick_capture.py - クイックキャプチャー
class QuickCaptureComponent(ft.UserControl):
"""GTDクイックキャプチャー(Inbox追加)"""
def __init__(self, task_service: TaskApplicationService):
super().__init__()
self.task_service = task_service
self.text_field = ft.TextField(
label="何をしますか?",
hint_text="思いついたことを何でも入力してください",
on_submit=self._on_submit,
expand=True
)
def build(self) -> ft.Control:
return ft.Row([
self.text_field,
ft.IconButton(
icon=ft.icons.ADD,
tooltip="Inboxに追加",
on_click=self._on_submit
)
])
def _on_submit(self, e=None):
"""Inboxアイテムを作成"""
if not self.text_field.value:
return
try:
command = CreateTaskCommand(
title=self.text_field.value,
status=TaskStatus.INBOX
)
self.task_service.create_task(command)
# 成功フィードバック
self.page.show_snack_bar(
ft.SnackBar(content=ft.Text("Inboxに追加しました"))
)
self.text_field.value = ""
self.text_field.update()
except Exception as e:
logger.exception("Inboxアイテム作成エラー")
self.page.show_snack_bar(
ft.SnackBar(
content=ft.Text(f"エラー: {str(e)}"),
bgcolor=ft.colors.ERROR
)
)
3. ルーティングとナビゲーション¶
# router.py - ビュールーティング
class ViewRouter:
"""ビュー間のナビゲーション管理"""
def __init__(self, page: ft.Page, service_container: ServiceContainer):
self.page = page
self.service_container = service_container
self.views: dict[str, BaseView] = {}
self.current_view: str = "main"
def navigate_to(self, view_name: str, **kwargs) -> None:
"""指定されたビューに遷移"""
if view_name not in self.views:
self.views[view_name] = self._create_view(view_name)
view = self.views[view_name]
self.page.clean()
self.page.add(view.content)
self.current_view = view_name
self.page.update()
def _create_view(self, view_name: str) -> BaseView:
"""ビューのファクトリメソッド"""
view_factories = {
"main": lambda: MainView(self.page, self.service_container),
"inbox": lambda: InboxView(self.page, self.service_container),
"projects": lambda: ProjectView(self.page, self.service_container),
"contexts": lambda: ContextView(self.page, self.service_container),
}
factory = view_factories.get(view_name)
if not factory:
raise ValueError(f"Unknown view: {view_name}")
return factory()
- 責務: ユーザーインターフェース、ユーザー体験、GTD 特化 UI、リアルタイム更新
- 実装:
Flet 0.27.6
を使用したクロスプラットフォーム対応 UI - パターン: MVP パターン、コンポーネントベース設計、ルーティング
- 詳細: Views 層 設計ガイドを参照してください
テスト戦略¶
1. 単体テストの構造¶
# tests/logic/services/test_task_service.py - サービス層テスト
class TestTaskService:
"""TaskService のテスト"""
def test_create_task_with_valid_data(self):
"""正常なタスク作成のテスト"""
repository = MockTaskRepository()
service = TaskService(repository)
task_data = TaskCreate(
title="新しいタスク",
description="説明"
)
result = service.create_task(task_data)
assert result.title == "新しいタスク"
assert result.status == TaskStatus.INBOX
assert len(repository.tasks) == 1
def test_create_task_with_empty_title(self):
"""タイトルが空の場合のテスト"""
repository = MockTaskRepository()
service = TaskService(repository)
with pytest.raises(ValueError, match="タスクのタイトルは必須です"):
service.create_new_task("")
2. 統合テストの実装¶
# tests/integration/test_gtd_workflow.py - GTDワークフロー統合テスト
class TestGTDWorkflow:
"""GTDワークフロー全体のテスト"""
@pytest.fixture
def service_container(self):
"""テスト用サービスコンテナ"""
return create_test_service_container()
def test_inbox_to_next_action_workflow(self, service_container):
"""Inbox → Next Action の完全なワークフロー"""
task_app_service = service_container.get_task_application_service()
# 1. Inboxアイテム作成
command = CreateTaskCommand(
title="重要な会議の準備",
status=TaskStatus.INBOX
)
inbox_task = task_app_service.create_task(command)
# 2. GTD処理(分類)
decision = InboxDecision(
task_id=inbox_task.id,
action=InboxAction.MAKE_NEXT_ACTION,
context_id=None,
scheduled_date=None
)
# 3. Next Actionに変換
next_action = task_app_service.process_inbox_item(decision)
# 4. 検証
assert next_action.status == TaskStatus.NEXT_ACTION
assert next_action.title == "重要な会議の準備"
- 責務: 品質保証、リグレッション防止、仕様の文書化
- 実装:
pytest
を使用した包括的テストスイート - カバレッジ: 単体テスト、統合テスト、エンドツーエンドテスト
実際の使用例¶
# main.py - アプリケーションエントリーポイント
def main(page: ft.Page):
"""Fletアプリケーションのメイン関数"""
# 依存関係の組み立て
service_container = create_service_container()
router = ViewRouter(page, service_container)
# GTD特化のページ設定
page.title = "Kage - GTD Task Manager"
page.theme_mode = ft.ThemeMode.SYSTEM
page.padding = 0
# メインビューを表示
router.navigate_to("main")
if __name__ == "__main__":
ft.app(target=main)
UI 層では、Application Service を呼び出すだけ¶
# views/components/task_form.py - タスク作成フォーム
def on_add_task(self, e):
"""タスク追加ボタンのイベントハンドラ"""
# バリデーション
if not self.title_field.value:
self._show_error("タスクのタイトルを入力してください")
return
try:
# Application Serviceを呼び出し
command = CreateTaskCommand(
title=self.title_field.value,
description=self.description_field.value,
status=TaskStatus.INBOX
)
task_app_service = self.service_container.get_task_application_service()
new_task = task_app_service.create_task(command)
# UI更新
self._clear_form()
self._refresh_task_list()
self._show_success(f"タスク「{new_task.title}」を作成しました")
except Exception as e:
logger.exception("タスク作成エラー")
self._show_error(f"エラーが発生しました: {str(e)}")
まとめ¶
この設計パターンを採用することで:
- 理解しやすい: 各層の責任が明確で、GTD の概念と一致
- 修正しやすい: 影響範囲が限定され、変更の波及を抑制
- テストしやすい: 各層を独立してテスト可能
- 拡張しやすい: AI 機能やエージェント追加が容易
- 再利用しやすい: コア機能は他のインターフェースでも利用可能
最初は複雑に感じるかもしれませんが、GTD ワークフローの複雑さを管理し、LLM との統合を行う上で、この設計の価値を実感できるはずです。