继续补 .NET:把封装、继承、多态和接口/实体/服务层的关系理一遍
这篇主要是写给自己后面回头看的。
前一篇先把 .NET 语法和面向对象的大框架过了一遍,但看项目代码的时候,还是会反复卡在几个地方:封装、继承、多态到底落在代码里是什么意思;接口、实体、服务层为什么总是一起出现;这些东西看起来都认识,但放到项目里就容易混。
所以这篇直接把我这几天反复想的几个点记下来。
1. 先把一个误区记下来:面向对象不是“语法专题”,而是组织代码的方式
刚开始学的时候,很容易把面向对象理解成一组概念题:
- 什么是封装
- 什么是继承
- 什么是多态
这种理解方式的问题是,背的时候像是懂了,但一进项目还是会乱。
因为项目里真正的问题不是“会不会默写定义”,而是:
- 这段代码该放在哪
- 这个类该不该暴露出去
- 这个方法是写在实体里,还是写在服务里
- 这个地方为什么不用具体类,而要用接口
所以我现在对面向对象的理解先换成一句更直接的话:
面向对象的重点是代码怎么分层、怎么收口、怎么避免后面越来越乱。
这个角度一换,很多东西就没那么悬了。
2. 封装:不是“藏起来”这么简单,而是先管住边界
封装这个词,我一开始也会理解成:
把东西包起来,不让别人乱碰。
这个理解不算错,但还是太轻了。
现在我觉得更实用的理解是:
把一个对象该负责的数据和行为放到一起,并且控制外部到底能碰它多少。
封装至少有两层意思:
第一层:职责收口
谁的数据,谁管;谁的逻辑,尽量也放在谁那边。
比如一个用户对象的用户名、状态、创建时间这些信息,本来就应该是它自己的东西。如果这些数据相关的判断和处理逻辑全散在外面,后面维护起来就会越来越难受。
第二层:暴露边界
不是所有字段、状态、操作都应该被外部随便改。
所以会有:
publicprivateprotected
这些访问修饰符。
以前我会把这些关键字当语法点背,现在我开始觉得它们更像是在回答一个问题:
这个东西到底该不该让外面直接碰?
如果什么都 public,那代码表面上写起来省事,但后面谁都能改,状态也容易失控。
所以我现在对封装的阶段理解是:
- 不只是把代码放进类里
- 更重要的是把“该归谁管”这件事先管住
3. 继承:不是为了少写代码,而是为了抽共性
继承这个东西,一开始最容易被理解成:
父类写一遍,子类少写一点。
但如果只停在这个层面,后面很容易乱继承。
我现在看继承,会更偏向一个问题:
这些类之间是不是真的有稳定的“是一种”关系?
如果只是因为有几段代码长得像,就强行继承,其实很危险。因为一旦上层定义不稳,后面子类全都会被带偏。
所以我现在对继承的理解先记成这样:
- 继承不只是复用代码
- 更像是在抽一组稳定共性
- 前提是这种共性真的成立
举个很粗的例子,如果系统里有不同类型的消息通知,它们可能都有:
- 标题
- 内容
- 发送时间
那你可能会想抽一个基础类,把共同字段放进去。这个方向是合理的。
但如果两个类只是“碰巧有点像”,业务含义根本不是一类,那为了省几行代码硬继承,后面大概率要出问题。
所以我现在对继承先给自己立个提醒:
只有在抽象关系真的稳的时候,继承才值得用。
不然还不如老老实实拆开。
4. 多态:重点不是定义,是“调用方不用关心你到底是谁”
多态我一开始最虚。
因为这个词听起来就比封装、继承抽象,而且只看书面定义,很容易只记住一句“同一个接口,多种实现”,但还是不知道它在项目里到底值在哪。
我现在给自己记的版本更实际一点:
多态的意义,是调用方只关心你能做什么,不关心你到底是哪一个具体实现。
这个点一旦想通,接口也就顺带好理解很多。
比如:
- 我只需要“发送通知”这个能力
- 至于你底下是短信、邮件还是站内信
- 调用这边不一定要写死
多态真正解决的问题不是“概念高级”,而是:
- 降低调用方和实现方的耦合
- 后面好替换
- 好扩展
- 不至于所有代码都绑死在一个具体类上
我现在觉得,多态如果脱离接口和抽象去背,很容易发虚;但一放进“调用和实现解耦”这个场景里,就立刻顺很多。
5. 到这里我开始能理解:接口不是摆设,它是隔离变化的一层
前面我一直对接口有点模糊印象。
知道项目里很多地方会写:
IUserServiceIOrderServiceIRepository
但一开始说实话,挺容易觉得它们有点“形式主义”——既然最后还是要写实现类,为什么不直接调实现类?
这几天往后看一点以后,我慢慢明白接口真正值在哪了。
我现在对接口的理解是:
接口先定义“要提供什么能力”,具体怎么做,再交给实现类。
这样带来的直接好处就是:
- 调用方依赖的是能力定义,不是某一个死实现
- 后面如果实现方式变了,调用方不一定跟着大改
- 测试、替换、扩展都会轻一点
所以我现在看接口,不再只是把它当成 .cs 文件里多出来的一层,而是会先问:
这个地方未来有没有变化可能?如果有,值不值得先隔一层?
当然,我现在也不想把接口神化。
不是所有地方都必须为了“规范”先写一个接口。
但至少在服务层这种会不断变、会被调用、会被替换的位置上,接口确实有价值。
6. 实体:我现在先把它理解成“业务数据长什么样”
前面看项目的时候,另一个总出现的词就是实体。
我现在先不给它套太多理论,先记最实用的理解:
实体主要是在描述业务数据本身。
比如用户、订单、任务、日志,这些东西在系统里都不是一句话,而是一组稳定字段。
举个很粗的例子,一个用户实体里可能会有:
- Id
- Name
- Phone
- Status
- CreateTime
这些字段合起来,才比较像系统里的“用户”到底长什么样。
所以我现在看实体,会优先想到:
- 这不是服务
- 这不是接口
- 它先解决的是“数据结构怎么表示”
当然,后面更深入学的时候,实体不一定只是纯数据袋子,这个我现在也知道。但至少在我当前阶段,先把“实体主要在表示业务对象的数据结构”这个点抓住,对我已经很有用了。
7. 服务层:我现在理解为“把业务动作集中起来的地方”
如果实体偏“长什么样”,那服务层我现在先理解成:
系统要做什么事,主要集中放在这里。
比如:
- 创建用户
- 修改状态
- 提交订单
- 发起任务
- 校验参数
- 调数据库
- 组织返回结果
这些动作如果全都塞进控制器里,代码很快会炸;如果全都塞进实体里,也不合适。
所以服务层的价值,我现在理解成两点:
第一,收业务逻辑
把“这件事到底怎么做”的主流程集中起来。
第二,隔开上下游
上面接控制器,下面接实体、仓储、数据库或者别的组件。
这样控制器不用什么都懂,底层数据层也不用直接暴露业务细节。
我现在越来越觉得,服务层其实就是一个“业务编排层”。
它不一定做所有细节,但它至少要把一次业务动作串起来。
8. 现在把接口、实体、服务层放在一起看,关系终于顺一点了
前面最容易乱的就是这三个东西总是一起出现。
我现在先把它们记成一个最粗但够用的关系:
实体
描述“数据长什么样”。
接口
描述“这个能力长什么样”。
服务层
实现“这个能力到底怎么做”。
如果再往前推一点,我现在脑子里大概会这样想:
- 控制器接请求
- 服务层处理业务
- 服务层会操作实体
- 服务层通过接口暴露能力
- 具体实现类去真正把事情做完
这个理解虽然还不算很深,但至少比之前把这些词全混在一起要好多了。
我现在再看项目结构时,至少不会再觉得每一层都长得差不多。
9. 我现在给自己记一个更实用的判断法
后面如果再看代码,我准备先这样问自己:
这是数据,还是动作?
- 如果重点在字段和状态,更像实体
- 如果重点在处理流程,更像服务层
这是定义能力,还是实现能力?
- 如果只是约定能做什么,更像接口
- 如果是真正把逻辑写出来,更像实现类
这个东西该不该暴露给外面?
- 如果不该乱碰,就收一层,别全开
public - 如果将来可能会换实现,就考虑先隔接口
我感觉这种问法,比硬背术语对我更有用。
因为它能直接落到“写代码时怎么摆位置”这个层面。
10. 现阶段结论:后端难的不是会不会写,而是会不会收
这几天看下来,我现在对 .NET 和后端这条线最大的感受反而不是“语法多”,而是:
后端难的地方,是收口。
- 数据怎么收
- 逻辑怎么收
- 依赖怎么收
- 边界怎么收
- 变化怎么收
封装、继承、多态这些概念,如果只停在书面定义,确实容易空。
但一旦放到项目结构里去看,就会发现它们都在回答类似的问题:
代码多起来以后,怎么别让它散掉。
我现在越来越觉得,面向对象真正有用的地方,在项目开始变复杂的时候。
先记到这里。
