周三下午,你打开一个文件,往下滚,再往下滚,然后忍不住嘀咕一句:这玩意儿到底经历了什么。SOLID五部曲的第一站,就从这种熟悉的窒息感开始。今天只讲一个字母:S,单一职责原则。用大白话说,就是"你的类是不是兼职太多了"。
![]()
先把教科书定义摆出来:一个类应该只有一个引起它变化的原因。这句话是对的,但对刚上手的人几乎没用——因为没人告诉你,"变化的原因"长什么样。所以别背定义,直接写点东西。
我们给一家咖啡店写个小小的发票功能。它要干三件事:把订单里的商品加总、把收据打印成文本、把发票存起来方便老板以后查。三个任务,记住这个数字。
第一天,大多数人(包括我)会这么写:一个 Invoice 类,构造函数收进 id 和商品列表,total() 负责算钱,print() 负责拼收据文本,save() 负责写文件。跑一下,终端里出现一张漂亮的收据,./invoices/0001.txt 也躺好了。在我机器上能跑,完美。
然后玩个游戏,叫"下周会发生什么"。三张工单落到你桌上:会计要加10%的税,设计师要收据改成HTML好发邮件,后端负责人要把发票从文本文件挪进Postgres。三个人,三件完全不相干的事,而每一件都会逼你打开同一个 invoice.ts。
这就是味道所在。Invoice 有三个变化的原因,因为它有三份工作:做算术、画文本、跟磁盘打交道。麻烦还会滚雪球:想测 total(),测试文件却被迫引入了一个碰文件系统的东西;HTML工单会先加一个 printHtml(),再加一个 format 标志,再加一串 if;Postgres工单则会把数据库驱动拖进一个本职只是"加几个数"的类里。
拆解的第一步,先别写代码,把职责念出来:计算客户该付多少、把发票渲染给人看、把发票存到某个地方。三句话,三个类。整套戏法就这么点东西。
第二步,让数据保持"笨"。Invoice 退化成一袋数据,加上本来就属于它的算术:total() 留着,fs 的 import 没了,格式化也没了。它只是安安静静当一张发票。
第三步,单独一个类负责画。新建 InvoicePrinter,printText(invoice) 接收一张发票,吐出收据字符串。等HTML工单来了,就在旁边加一个 printHtml(),Invoice 全程不知情。
第四步,单独一个类负责存。InvoiceRepository 里放 saveToFile(),需要的话再补一个 saveToDatabase()。Postgres工单来了?打开这个文件,把方法填上,项目里其他东西一概不关心。
第五步,在入口把它们接起来:造一张发票,用打印机打出来,用仓库存下去。再跑一遍,同样的收据,同样的文件,零 drama。
现在把三张工单重放一遍。加税,打开 invoice.ts 改 total(),打印机和仓库不受影响;HTML收据,打开 invoice-printer.ts 加方法,算术和存储不受影响;换Postgres,打开 invoice-repository.ts 改存储,算术和收据不受影响。每个文件都恰好对应一个会来找你改它的人。这就是"一个变化的原因"在实践中的意思:一个利益相关方,一份工作,一个文件。
还有几个白送的好处:Invoice 变得极易做单元测试,没有磁盘,没有 mock,数字进数字出;代码评审变小了,改税不可能顺手弄坏收据排版;git blame 也不再像案发现场,四个人改同一个两百行的类。
但别上头。SRP 不是"每个类只能有一个方法"。一个类可以有十个方法,仍然只有一份工作。要问的永远是那句:谁会来要求我改这里?答案是"好几个互不相干的人",就拆;答案是"一个人",就别动它。
这就是 S。下一站是 O,开闭原则,我们会看看那条处理折扣的 if/else 链,是怎么一步步变成代码库里你最不想碰的部分的。在那之前,去找一个兼职太多的类,给它配几个同事。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.