-
routes/(HTTP 层:Zod 校验 + 审计)
-
services/(业务逻辑层)
-
lib/(工具/实现层)
-
schemas/(Zod 校验模式)
-
middleware/(认证授权、错误处理)
-
ssh/(SSH/SFTP)
-
结构符合"分层架构"雏形。现在写详细解释。用户偏好:通俗、表格、可直接照做的示例。
我来组织:
一、SOLID 五原则
每个:一句话直觉 + 通俗比喻 + web-ops 例子(有实际代码就引用,没有就构造小例子)
S - 单一职责(Single Responsibility) O - 开闭原则(Open/Closed) L - 里氏替换(Liskov Substitution) I - 接口隔离(Interface Segregation) D - 依赖倒置(Dependency Inversion)
二、整洁架构(Clean Architecture)
-
核心思想:依赖方向朝内(业务核心不依赖框架)
-
分层:实体/用例/接口适配器/框架
-
与 web-ops 对照
-
关键规则:依赖规则(内层不 import 外层)
三、对 web-ops 的实际意义 + 建议
我尽量用 web-ops 的真实代码来举例,比如:
-
S:routes/ 只做 HTTP 编排,services/ 做业务——serverService.ts 的 updateServer 就是业务,routes/servers.ts 的 PUT 就是 HTTP 层
-
O:inspectionService 的 TOOL_DEFS 注册新工具不改调用方——这就是开闭原则的体现!(技能里写了"注册即生效:runAllInspection 用 Object.keys(TOOL_DEFS) 遍历——新工具自动进入全部巡检")这是绝佳的例子!
-
D:lib/alert-rule.ts 的 getAlertRuleConfig 用 prisma 直接读写——依赖具体实现;如果抽象成接口,就是依赖倒置
也要指出 web-ops 现在做得好的和可以改进的(诚实、建设性)。
篇幅控制:用户要"详细解释",所以可以长一点,但要用表格和代码块组织,避免大段文字。用户偏好"干净有条理"。
我直接写,不需要更多工具调用。
好问题——我结合 web-ops项目现有代码来讲,这样不抽象。先快速确认一下项目分层结构,保证举例准确:
确认了项目结构:-routes/(HTTP层:Zod校验+审计)-services/(业务逻辑层)-lib/(工具/实现层)-schemas/(Zod校验模式)-middleware/(认证授权、错误处理)-ssh/(SSH/SFTP)-repositories/(应该有serverRepository,刚才列表被截断,但技能提到repositories/serverRepository.ts)结构符合"分层架构"雏形。现在写详细解释。用户偏好:通俗、表格、可直接照做的示例。我来组织:##一、SOLID五原则每个:一句话直觉+通俗比喻+web-ops例子(有实际代码就引用,没有就构造小例子)S-单一职责(SingleResponsibility)O-开闭原则(Open/Closed)L-里氏替换(LiskovSubstitution)I-接口隔离(InterfaceSegregation)D-依赖倒置(DependencyInversion)##二、整洁架构(CleanArchitecture)-核心思想:依赖方向朝内(业务核心不依赖框架)-分层:实体/用例/接口适配器/框架-与web-ops对照-关键规则:依赖规则(内层不import外层)##三、对web-ops的实际意义+建议我尽量用web-ops的真实代码来举例,比如:-S:routes/只做HTTP编排,services/做业务——serverService.ts的updateServer就是业务,routes/servers.ts的PUT就是HTTP层-O:inspectionService的TOOL_DEFS注册新工具不改调用方——这就是开闭原则的体现!(技能里写了"注册即生效:runAllInspection用Object.keys(TOOL_DEFS)遍历——新工具自动进入全部巡检")这是绝佳的例子!-D:lib/alert-rule.ts的getAlertRuleConfig用prisma直接读写——依赖具体实现;如果抽象成接口,就是依赖倒置也要指出web-ops现在做得好的和可以改进的(诚实、建设性)。篇幅控制:用户要"详细解释",所以可以长一点,但要用表格和代码块组织,避免大段文字。用户偏好"干净有条理"。我直接写,不需要更多工具调用。
项目结构确认了。下面用 web-ops 的真实代码来讲 SOLID 和整洁架构——这样你既能理解概念,又能看到自己项目里哪些地方已经在做、哪些可以改进。
一、SOLID 五原则
SOLID 是面向对象设计的五个原则的首字母缩写,核心目的就一句话:让代码好改、好加、好维护,改一处不炸十处。
| 字母 | 原则 | 一句话直觉 |
|---|---|---|
| S | 单一职责 | 一个类/函数只干一件事 |
| O | 开闭原则 | 加功能不改旧代码(对扩展开放,对修改关闭) |
| L | 里氏替换 | 子类能顶替父类,不搞特殊 |
| I | 接口隔离 | 接口别又大又全,按需拆分 |
| D | 依赖倒置 | 依赖抽象,不依赖具体实现 |
S —— 单一职责(Single Responsibility)
直觉:一个类/函数只有一个"为什么而变"的理由。
通俗比喻:厨师只负责炒菜,服务员只负责上菜,收银员只负责收钱。如果让厨师同时管收银,客人一多,菜也糊了、钱也算错。
web-ops 现状:你项目已经做对了。看分层:
routes/servers.ts → 只做 HTTP 编排(收请求、Zod 校验、调 service、回响应)
services/serverService → 只做业务逻辑(查库、加密、审计)
lib/alert-rule.ts → 只做告警规则这一个业务
ssh/sftp-manager.ts → 只做 SFTP
反面教材(要避免的)是:一个函数里既拼 SQL、又发邮件、又写日志、又算业务——改任何一处都得动它。
示例——项目里好的做法:
// routes/servers.ts —— 职责:HTTP 层,不做业务
serverRouter.put('/servers/:id', requireRole('OPS_ADMIN'), validate(UpdateServerSchema),
async (req, res, next) => {
const s = await serverService.updateServer(
⚠️ 单一职责的"度":不是函数越短越好,而是"变化的原因唯一"。比如
serverService.updateServer会因"业务规则变化"而变,routes的 PUT 会因"API 契约变化"而变——它们分开了,改 API 不动业务,改业务不动 API。
O —— 开闭原则(Open/Closed)
直觉:加新功能时,新增代码,而不是修改旧代码。
通俗比喻:插座。加一个新电器(扩展),不用拆墙改插座(修改),插上就行。
web-ops 现状:你的巡检工具模块就是教科书级例子!看技能里记的:
// inspectionService.ts —— 加新巡检工具只需:
// 1. 写 XXX_COMMAND + parseXxx()
// 2. 注册进 TOOL_DEFS(白名单)+ TOOL_NAMES(中文名)
// 3. 前端 tools.tsx 加 UI 入口
// runAllInspection 用 Object.keys(TOOL_DEFS) 遍历 —— 新工具自动进入全部巡检
加一个"检查防火墙状态"工具,完全不用改 runAllInspection 的代码——这就是对扩展开放、对修改关闭。改法如果是"在 runAllInspection 里加一行 if 判断",那就是违反 O 的坏味道,每加一个工具都要动核心函数,迟早改出 bug。
反面例子(很多项目这样写,要避免):
// ❌ 坏味道:加新通知渠道要改这个函数
function sendNotify(type) {
if (type === 'wecom') {
你项目的 lib/notify-webhook.ts 用 detectType + buildBody 按类型分发,如果渠道种类继续变多,将来可以考虑注册表模式(Map<type, builder>),彻底实现 O。
L —— 里氏替换(Liskov Substitution)
直觉:子类/替代实现,必须能无痛替换父类/原实现,调用方不用改任何代码。
通俗比喻:公司说"派一辆车来接你"。来的可以是奔驰、五菱宏光、还是货车——只要它能开、能载你到目的地,你就满意。如果来的是"坦克",把路压坏了(子类行为超出父类契约),这就是违反 L。
web-ops 现状:你的 Redis 降级就是 L 的体现:
// lib/redis.ts —— 没配 Redis 时直接返回 MemoryRedis
// 调用方根本不知道(也不关心)用的是真 Redis 还是内存版
function getRedis() {
if (!process.env.REDIS_URL) return new MemoryRedis(); // 替代实现
return realRedis;
}
调用 blacklistToken() 的地方,不管底层是 Redis 还是内存,行为一致——替换无痛。这就是 L。
违反 L 的例子:某处代码假设"一定返回 null 表示无数据",但内存版返回 undefined——替换实现破坏了调用方的假设。替换实现必须遵守原契约(入参范围、返回值类型、抛错行为)。
I —— 接口隔离(Interface Segregation)
直觉:别让调用方依赖它用不到的接口。
通俗比喻:一个"全能遥控器"有 50 个按钮,你只需要开关机。接口太胖,人人被迫依赖一堆没用到的按钮。
web-ops 现状:你的 checkServerReadAccess 和 requireServerAccess 分离就是 I 的思路——读取权限和写权限分开用,路由按需挂载,而不是一个"全权限检查函数"处处调用。
另一个例子:UpdateServerSchema 只暴露该更新的字段(name/ip/sshPort/sshUser/sshPassword/os/groupId),而不是把整个数据库字段全塞给前端——接口最小化。
要避免的:一个 UserService 同时提供 createUser / deleteUser / changePassword / sendEmail / exportReport / calcSalary……所有功能都塞进一个类,任何调用方都依赖整坨。
D —— 依赖倒置(Dependency Inversion)
直觉:高层策略不依赖低层细节;两者都依赖抽象。通俗说:面向接口编程,不面向具体实现编程。
通俗比喻:手机充电。你的手机(高层)不关心电来自插座还是充电宝(低层),它只依赖"USB-C 接口"这个抽象。没有这个抽象,手机就得针对"插座充电、充电宝充电、车载充电"各写一套。
web-ops 现状:部分做到了,部分没做到:
✅ 做到的地方:routes 依赖 serverService(业务抽象),不直接依赖 prisma。路由层换数据库实现时不用改。
❌ 可改进的地方:看 lib/session-config.ts:
// 现在:直接依赖具体实现 prisma
import { prisma } from './prisma';
export async function getSessionConfig() {
const row = await prisma.sessionConfig.findUnique({ where: { id: 1 } });
如果将来想换成别的存储(比如内存/配置文件),所有调用 getSessionConfig 的地方都要改——因为依赖的是具体实现。理想形态是定义接口:
// 抽象接口(高层依赖它)
export interface SessionConfigStore {
get(): Promise<SessionConfigData>;
save(data: SessionConfigData): Promise<SessionConfigData>;
}
// 具体实现(实现接口)
export const prismaSessionConfigStore: SessionConfigStore = {
get: () => prisma.sessionConfig.findUnique({ where: { id: 1 } }),
⚠️ 但注意度:中小项目全搞接口抽象会过度设计("接口爆炸")。务实做法:只有一个实现的先不抽象,等出现第二个实现(或测试需要 mock)再抽——这就是"敏捷的 D"。
二、整洁架构(Clean Architecture)
核心思想
整洁架构(Robert C. Martin 提出)就一条铁律:
依赖只能从外向内。内层(业务核心)永远不知道外层(框架/数据库/HTTP)的存在。
经典洋葱图,从内到外:
┌─────────────────────────────────────┐
│ Entity(实体/业务核心) │ ← 最内层:纯业务规则,不依赖任何框架
│ Use Cases(用例/应用服务) │ ← 业务操作:登录、创建服务器、巡检
│ Interface Adapters(接口适配器) │ ← 把外部世界翻译成业务语言
│ Frameworks & Drivers(框架驱动层) │ ← Express、Prisma、MySQL、SSH
└─────────────────────────────────────┘
依赖规则:箭头永远朝内。外层可以依赖内层,内层绝不允许依赖外层。
用一句话记:"数据库不知道业务,业务不知道 Express;Express 知道业务,业务不知道数据库。"
和 web-ops 的对照
你的项目已经有接近整洁架构的分层了:
| 整洁架构层 | web-ops 对应 | 举例 |
|---|---|---|
| Frameworks(框架) | routes/ + app.ts |
routes/servers.ts 处理 HTTP |
| Interface Adapters(适配) | schemas/ + middleware/ |
Zod 校验、authenticate、error-handler |
| Use Cases(业务用例) | services/ |
serverService.updateServer |
| Entities(业务实体) | lib/ 里的领域逻辑 + Prisma model |
alert-rule.ts、custom-task-scheduler.ts |
再精确一点,你的依赖方向:routes → services → lib/repositories → prisma,箭头是单向朝内的——这符合整洁架构精神。
client (React)
↓ HTTP /api/v1
routes (Express 路由,只做编排)
↓
services (业务逻辑)
↓
lib / repositories (领域实现、数据访问)
↓
Prisma / MySQL / SSH2 / Redis
整洁架构三个关键收益(在 web-ops 的具体体现)
收益 1:改框架不动业务
换 express 换 fastify,只改 routes/ 层,services/ 一行不动。
收益 2:业务可测试
lib/alert-rule.ts 的 checkMetricsAlerts 是纯业务逻辑,可以脱离 HTTP/数据库直接测——你们验证脚本 npx tsx 直测 services/lib 就是这个思路(技能里写的"不卡在登录,写临时脚本直测服务层"就是整洁架构带来的测试红利)。
收益 3:关注点隔离
middleware/error-handler.ts 负责"把错误翻译成 HTTP 状态码"——业务层只管抛 NotFoundError('服务器不存在'),不用知道 HTTP 是什么。这正是"接口适配器"的职责。
三、对 web-ops 的落地建议(务实版)
⚠️ 诚实提醒:完全照搬整洁架构会过度设计。web-ops 是单库中小型平台,关键是把 SOLID 的"度"把握好。
| 建议 | 理由 | 工作量 |
|---|---|---|
| 保持现有分层,别打散 | routes/services/lib 已符合单向依赖,不要为了"更整洁"拆出一堆 abstract class | 0 |
| 新功能默认按现有模式落地 | 新模块 = routes(校验+审计)+ services(业务)+ lib(实现),照抄现有模块 | 0(照惯例) |
| services 层别直接 import prisma 散落 | 现在有些 service 直接 prisma.xxx,将来统一走 repository 层(如 serverRepository)会更干净——但非紧急 |
中 |
| 开闭原则重点用在"多分支分发" | 通知渠道、巡检工具、文件类型处理这类会不断加种类的地方,用注册表/策略模式(Map<type, handler>)而不是 else if 堆 |
小~中 |
| 别把审计/日志/权限逻辑写进业务函数 | 审计应像现在一样在 routes 层或 service 尾部统一调,不要在 createServer 中间散落 |
0(保持现状) |
四、一句话总结
| 概念 | 一句话 |
|---|---|
| SOLID | 五个设计原则,目标 = 代码好加功能、好改、好测 |
| 整洁架构 | 一个分层规则:业务核心在中间,依赖只朝内 |
| 两者关系 | SOLID 是"怎么把类/函数写好",整洁架构是"怎么把系统分层"——SOLID 是砖,整洁架构是图纸 |
| web-ops 现状 | 分层骨架已符合,SOLID 里 S/O 做得最好(routes/services 分离 + 巡检工具注册表),D 有提升空间(services 直接依赖 prisma) |
浙公网安备 33010602011771号