整洁架构说明
这个应用严格遵循了整洁架构的原则:
-
领域层 (Domain Layer)
Task: 核心业务实体,包含业务规则和方法
TaskRepository: 数据访问抽象接口
TaskFilter: 值对象,封装过滤逻辑
-
应用层 (Application Layer)
TaskService: 业务逻辑,协调领域对象完成用例
不依赖任何外部框架或技术细节
-
接口适配器层 (Interface Adapters)
InMemoryTaskRepository: 仓储接口的具体实现
TaskPresenter: 数据展示器,准备视图所需的数据格式
TaskController: 处理用户输入,调用应用层服务
-
框架层 (Frameworks & Drivers)
TaskManagerApp: Dash 应用,处理 UI 渲染和用户交互
依赖关系从外层指向内层
依赖关系规则
内层不依赖于外层
依赖方向:框架层 → 接口适配器层 → 应用层 → 领域层
所有跨层通信都通过接口进行
代码示例
python
"""
使用 Dash 构建的整洁架构应用示例
任务管理系统
架构层次:
1. 领域层 (Domain) - 业务实体和规则
2. 应用层 (Application) - 用例和业务逻辑
3. 接口适配器层 (Interface Adapters) - 控制器、Presenter、Repository 实现
4. 框架层 (Frameworks & Drivers) - UI、数据库、外部服务
"""
import dash
from dash import dcc, html, Input, Output, State, callback_context
import dash_bootstrap_components as dbc
from datetime import datetime
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, Any
import uuid
# ======================
# 领域层 (Domain Layer)
# ======================
class Task:
"""任务实体 - 核心业务对象"""
def __init__(self, id: str, title: str, description: str,
status: str = "pending", created_at: str = None):
self.id = id
self.title = title
self.description = description
self.status = status # "pending" or "completed"
self.created_at = created_at or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def mark_completed(self):
"""标记任务为已完成"""
self.status = "completed"
def mark_pending(self):
"""标记任务为待完成"""
self.status = "pending"
def toggle_status(self):
"""切换任务状态"""
if self.status == "pending":
self.mark_completed()
else:
self.mark_pending()
def is_completed(self) -> bool:
"""检查任务是否已完成"""
return self.status == "completed"
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
'id': self.id,
'title': self.title,
'description': self.description,
'status': self.status,
'created_at': self.created_at
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'Task':
"""从字典创建任务"""
return cls(
id=data['id'],
title=data['title'],
description=data['description'],
status=data['status'],
created_at=data['created_at']
)
class TaskRepository(ABC):
"""任务仓储接口 - 数据访问抽象"""
@abstractmethod
def add(self, task: Task) -> None:
pass
@abstractmethod
def get_by_id(self, task_id: str) -> Optional[Task]:
pass
@abstractmethod
def get_all(self) -> List[Task]:
pass
@abstractmethod
def update(self, task: Task) -> None:
pass
@abstractmethod
def delete(self, task_id: str) -> bool:
pass
class TaskFilter:
"""任务过滤器 - 值对象"""
def __init__(self, status: str = "all"):
self.status = status # "all", "pending", "completed"
def apply(self, tasks: List[Task]) -> List[Task]:
"""应用过滤器"""
if self.status == "all":
return tasks
elif self.status == "pending":
return [task for task in tasks if task.status == "pending"]
elif self.status == "completed":
return [task for task in tasks if task.status == "completed"]
return tasks
# ======================
# 应用层 (Application Layer)
# ======================
class TaskService:
"""任务服务 - 业务逻辑"""
def __init__(self, task_repository: TaskRepository):
self.task_repository = task_repository
def create_task(self, title: str, description: str) -> Task:
"""创建新任务"""
if not title or not description:
raise ValueError("标题和描述不能为空")
task_id = str(uuid.uuid4())
task = Task(id=task_id, title=title, description=description)
self.task_repository.add(task)
return task
def get_task(self, task_id: str) -> Optional[Task]:
"""获取任务"""
return self.task_repository.get_by_id(task_id)
def get_all_tasks(self, filter: TaskFilter = None) -> List[Task]:
"""获取所有任务"""
tasks = self.task_repository.get_all()
if filter:
tasks = filter.apply(tasks)
return tasks
def toggle_task_status(self, task_id: str) -> Optional[Task]:
"""切换任务状态"""
task = self.task_repository.get_by_id(task_id)
if task:
task.toggle_status()
self.task_repository.update(task)
return task
return None
def delete_task(self, task_id: str) -> bool:
"""删除任务"""
return self.task_repository.delete(task_id)
def get_task_statistics(self) -> Dict[str, int]:
"""获取任务统计"""
tasks = self.task_repository.get_all()
total = len(tasks)
completed = len([task for task in tasks if task.is_completed()])
pending = total - completed
return {
'total': total,
'completed': completed,
'pending': pending
}
# ======================
# 接口适配器层 (Interface Adapters)
# ======================
class InMemoryTaskRepository(TaskRepository):
"""内存任务仓储实现"""
def __init__(self):
self.tasks: Dict[str, Task] = {}
def add(self, task: Task) -> None:
self.tasks[task.id] = task
def get_by_id(self, task_id: str) -> Optional[Task]:
return self.tasks.get(task_id)
def get_all(self) -> List[Task]:
return list(self.tasks.values())
def update(self, task: Task) -> None:
if task.id in self.tasks:
self.tasks[task.id] = task
def delete(self, task_id: str) -> bool:
if task_id in self.tasks:
del self.tasks[task_id]
return True
return False
class TaskPresenter:
"""任务展示器 - 准备视图所需的数据"""
@staticmethod
def present_task(task: Task) -> Dict[str, Any]:
"""展示单个任务"""
return task.to_dict()
@staticmethod
def present_task_list(tasks: List[Task]) -> List[Dict[str, Any]]:
"""展示任务列表"""
return [task.to_dict() for task in tasks]
@staticmethod
def present_statistics(statistics: Dict[str, int]) -> Dict[str, Any]:
"""展示统计信息"""
return {
'total_tasks': statistics['total'],
'completed_tasks': statistics['completed'],
'pending_tasks': statistics['pending'],
'completion_rate': (
round(statistics['completed'] / statistics['total'] * 100, 2)
if statistics['total'] > 0 else 0
)
}
class TaskController:
"""任务控制器 - 处理用户输入"""
def __init__(self, task_service: TaskService):
self.task_service = task_service
self.presenter = TaskPresenter()
def create_task(self, title: str, description: str) -> Dict[str, Any]:
"""创建任务"""
try:
task = self.task_service.create_task(title, description)
return {
'success': True,
'task': self.presenter.present_task(task),
'message': '任务创建成功'
}
except ValueError as e:
return {
'success': False,
'task': None,
'message': str(e)
}
def get_tasks(self, filter_status: str = "all") -> Dict[str, Any]:
"""获取任务列表"""
task_filter = TaskFilter(status=filter_status)
tasks = self.task_service.get_all_tasks(task_filter)
return {
'success': True,
'tasks': self.presenter.present_task_list(tasks),
'filter': filter_status
}
def toggle_task_status(self, task_id: str) -> Dict[str, Any]:
"""切换任务状态"""
task = self.task_service.toggle_task_status(task_id)
if task:
return {
'success': True,
'task': self.presenter.present_task(task),
'message': '任务状态已更新'
}
else:
return {
'success': False,
'task': None,
'message': '任务未找到'
}
def delete_task(self, task_id: str) -> Dict[str, Any]:
"""删除任务"""
success = self.task_service.delete_task(task_id)
if success:
return {
'success': True,
'message': '任务已删除'
}
else:
return {
'success': False,
'message': '任务未找到'
}
def get_statistics(self) -> Dict[str, Any]:
"""获取统计信息"""
statistics = self.task_service.get_task_statistics()
return {
'success': True,
'statistics': self.presenter.present_statistics(statistics)
}
# ======================
# 框架层 (Frameworks & Drivers)
# ======================
class TaskManagerApp:
"""Dash 应用 - UI 框架层"""
def __init__(self, controller: TaskController):
self.controller = controller
self.app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
self.setup_layout()
self.setup_callbacks()
def setup_layout(self):
"""设置应用布局"""
self.app.layout = dbc.Container([
# 标题区域
dbc.Row([
dbc.Col([
html.H1("任务管理系统 - 整洁架构示例",
className="text-center my-4 text-primary")
])
]),
# 统计信息区域
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardBody([
dbc.Row([
dbc.Col([
html.H5("总任务数", className="card-title"),
html.H3(id="total-tasks", className="card-text text-primary")
], width=3),
dbc.Col([
html.H5("待完成", className="card-title"),
html.H3(id="pending-tasks", className="card-text text-warning")
], width=3),
dbc.Col([
html.H5("已完成", className="card-title"),
html.H3(id="completed-tasks", className="card-text text-success")
], width=3),
dbc.Col([
html.H5("完成率", className="card-title"),
html.H3(id="completion-rate", className="card-text text-info")
], width=3),
])
])
], className="mb-4")
])
]),
# 添加任务区域
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("添加新任务"),
dbc.CardBody([
dbc.Row([
dbc.Col([
dbc.Input(
id="task-title-input",
placeholder="任务标题",
type="text",
className="mb-2"
),
dbc.Input(
id="task-desc-input",
placeholder="任务描述",
type="text",
className="mb-2"
),
], width=10),
dbc.Col([
dbc.Button(
"添加任务",
id="add-task-btn",
color="primary",
className="w-100 h-100"
)
], width=2)
]),
html.Div(id="add-task-message")
])
], className="mb-4")
])
]),
# 过滤和控制区域
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardBody([
dbc.Row([
dbc.Col([
dbc.ButtonGroup([
dbc.Button("全部任务", id="filter-all",
color="primary", outline=True),
dbc.Button("待完成", id="filter-pending",
color="warning", outline=True),
dbc.Button("已完成", id="filter-completed",
color="success", outline=True),
]),
], width=8),
dbc.Col([
dbc.Button("刷新统计", id="refresh-stats",
color="info", className="float-end")
], width=4)
])
])
], className="mb-4")
])
]),
# 任务列表区域
dbc.Row([
dbc.Col([
html.Div(id="tasks-container")
])
]),
# 存储组件
dcc.Store(id="tasks-store", data=[]),
dcc.Store(id="filter-store", data="all")
], fluid=True)
def setup_callbacks(self):
"""设置应用回调"""
# 初始化统计信息和任务列表
@self.app.callback(
[Output("tasks-store", "data"),
Output("total-tasks", "children"),
Output("pending-tasks", "children"),
Output("completed-tasks", "children"),
Output("completion-rate", "children")],
[Input("filter-all", "n_clicks"),
Input("filter-pending", "n_clicks"),
Input("filter-completed", "n_clicks"),
Input("refresh-stats", "n_clicks")]
)
def update_tasks_and_stats(all_clicks, pending_clicks, completed_clicks, refresh_clicks):
"""更新任务列表和统计信息"""
ctx = callback_context
filter_status = "all"
if ctx.triggered:
trigger_id = ctx.triggered[0]['prop_id'].split('.')[0]
if trigger_id == "filter-pending":
filter_status = "pending"
elif trigger_id == "filter-completed":
filter_status = "completed"
# 获取任务列表
tasks_result = self.controller.get_tasks(filter_status)
tasks_data = tasks_result['tasks'] if tasks_result['success'] else []
# 获取统计信息
stats_result = self.controller.get_statistics()
if stats_result['success']:
stats = stats_result['statistics']
total = stats['total_tasks']
pending = stats['pending_tasks']
completed = stats['completed_tasks']
completion_rate = f"{stats['completion_rate']}%"
else:
total = pending = completed = 0
completion_rate = "0%"
return tasks_data, total, pending, completed, completion_rate
# 添加任务
@self.app.callback(
[Output("add-task-message", "children"),
Output("task-title-input", "value"),
Output("task-desc-input", "value")],
[Input("add-task-btn", "n_clicks")],
[State("task-title-input", "value"),
State("task-desc-input", "value")]
)
def add_task(n_clicks, title, description):
"""添加新任务"""
if n_clicks is None or n_clicks == 0:
return "", "", ""
if not title or not description:
return dbc.Alert("请输入任务标题和描述", color="danger"), title, description
result = self.controller.create_task(title, description)
if result['success']:
return dbc.Alert(result['message'], color="success"), "", ""
else:
return dbc.Alert(result['message'], color="danger"), title, description
# 渲染任务列表
@self.app.callback(
Output("tasks-container", "children"),
[Input("tasks-store", "data")]
)
def render_tasks(tasks_data):
"""渲染任务列表"""
if not tasks_data:
return dbc.Alert("暂无任务", color="info")
task_cards = []
for task in tasks_data:
status_color = "success" if task['status'] == 'completed' else "warning"
status_text = "已完成" if task['status'] == 'completed' else "待完成"
card = dbc.Card([
dbc.CardBody([
dbc.Row([
dbc.Col([
html.H5(task['title']),
html.P(task['description'], className="text-muted"),
html.Small(f"创建时间: {task['created_at']}", className="text-muted")
], width=8),
dbc.Col([
dbc.Badge(status_text, color=status_color, className="mb-2 d-block"),
html.Div([
dbc.Button(
"切换状态",
id={"type": "toggle-btn", "index": task['id']},
color="primary",
size="sm",
className="me-1 mb-1"
),
dbc.Button(
"删除",
id={"type": "delete-btn", "index": task['id']},
color="danger",
size="sm",
className="mb-1"
)
])
], width=4, className="text-end")
])
])
], className="mb-3")
task_cards.append(card)
return task_cards
# 处理任务操作(切换状态、删除)
@self.app.callback(
[Output("tasks-store", "data", allow_duplicate=True),
Output("total-tasks", "children", allow_duplicate=True),
Output("pending-tasks", "children", allow_duplicate=True),
Output("completed-tasks", "children", allow_duplicate=True),
Output("completion-rate", "children", allow_duplicate=True)],
[Input({"type": "toggle-btn", "index": dash.dependencies.ALL}, "n_clicks"),
Input({"type": "delete-btn", "index": dash.dependencies.ALL}, "n_clicks")],
prevent_initial_call=True
)
def handle_task_actions(toggle_clicks, delete_clicks):
"""处理任务操作"""
ctx = callback_context
if not ctx.triggered:
return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update
trigger_id = ctx.triggered[0]['prop_id']
# 解析触发按钮的类型和ID
if 'toggle-btn' in trigger_id:
task_id = eval(trigger_id.split('.')[0])['index']
self.controller.toggle_task_status(task_id)
elif 'delete-btn' in trigger_id:
task_id = eval(trigger_id.split('.')[0])['index']
self.controller.delete_task(task_id)
# 更新任务列表和统计信息
tasks_result = self.controller.get_tasks()
tasks_data = tasks_result['tasks'] if tasks_result['success'] else []
stats_result = self.controller.get_statistics()
if stats_result['success']:
stats = stats_result['statistics']
total = stats['total_tasks']
pending = stats['pending_tasks']
completed = stats['completed_tasks']
completion_rate = f"{stats['completion_rate']}%"
else:
total = pending = completed = 0
completion_rate = "0%"
return tasks_data, total, pending, completed, completion_rate
def run(self, debug=True):
"""运行应用"""
self.app.run(debug=debug)
# ======================
# 依赖注入和应用启动
# ======================
def create_app():
"""创建应用实例 - 依赖注入容器"""
# 创建仓储
task_repository = InMemoryTaskRepository()
# 创建服务
task_service = TaskService(task_repository)
# 创建控制器
task_controller = TaskController(task_service)
# 创建Dash应用
app = TaskManagerApp(task_controller)
return app
if __name__ == "__main__":
# 创建并运行应用
app = create_app()
app.run(debug=True)