刚学面向对象时有个困惑:需求一来,不知道该怎么建类。要么只建一个Student类塞满所有字段,要么凭感觉随便建几个,后面一改需求就全乱。其实实体抽象不是靠灵感,而是有一套可以反复用的拆解流程。下面以"学生档案系统"为例,完整走一遍。
第一步:把需求说成完整句子
不能只盯着"学生档案系统"这六个字。可以先在脑子里编几个真实的使用场景,把需求展开成完整的句子:
- 新生入学,要录入基本信息,然后分到某个班级
- 老师要给某门课录入成绩
- 学生可以申请转专业,档案信息要跟着改
- 学生毕业时,档案要归档,后续可能调档
- 管理员、教师、学生登录后看到的权限不同
每句话里出现的主语、宾语、涉及的东西,基本都是候选实体。这一步看似简单,但很因为需求只有一句话,脑子里容易没有场景,需要经常联想
第二步:圈名词,然后逐个筛
把上面场景里的名词圈出来:学生、班级、专业、学院、老师、课程、成绩、档案、账号、角色、学期。
然后可以关联问题:
- 它有没有自己的数据要存?有 → 可能是实体
- 它有没有独立的生命周期?能单独创建、修改、删除 → 可能是实体
- 它会不会被多个地方引用?会 → 值得单独建类
比如"手机号",它只是学生的一个属性,不会单独被管理,就不建类。"成绩"看着像学生的属性,但它同时关联学生和课程,还有学期、补考记录,需要独立存在,所以要拆成单独的Score类。
第三步:圈动词,找职责归属
动词有:录入、分班、录成绩、算平均分、转专业、归档、调档、登录、鉴权。
对每个动词问:干这件事需要谁的数据?谁最该负责?
- 改手机号 → 只涉及学生自己的数据,放
Student身上,写成changePhone() - 分班 → 涉及班级和学生,放
Clazz或ClassService - 算平均分 → 要一堆成绩数据,放
ScoreService - 登录鉴权 → 跟学生本身没关系,放
AuthService,别塞进Student
这一步能避免"书自己卖自己"那种职责放错的问题。
第四步:看关系,反推缺失实体
把实体之间的关系画一下,很容易发现漏掉的:
- 学生和课程是多对多 → 中间必然要有一个
Score或Enrollment(选课记录) - 老师和课程是一对多 → 需要
Course来承载 - 专业属于学院 → 需要
Department - 档案有调阅历史 → 可能需要
ArchiveRecord(调档记录)
多对多关系几乎必然隐含一个中间实体
第五步:区分实体、服务、工具
- 有数据、有生命周期 → 实体类(
Student、Course、Clazz) - 是一串流程、要协调多个对象 → Service(
ScoreService、ArchiveService) - 跟业务无关的通用操作 → 工具类(日期处理、字符串处理)
- 只是打包数据传过来传过去 → DTO / VO
不要把登录逻辑塞进Student,也不要把成绩计算塞进Course,各司其职才容易维护。
第六步:用"改需求"验证
假装客户改需求,看你的抽象会不会崩:
- "以后一个学生可以辅修第二个专业" → 说明学生里直接存专业字段不行,要抽
Major和关联关系 - "以后还有教师档案" → 说明可以把人和档案抽象出通用结构
- "不同学院毕业要求不一样" → 说明毕业判断适合用策略模式,而不是 if-else 堆一坨
流程概览
从场景找名词 → 逐个筛实体 → 从动词找职责 → 从关系找漏掉的 → 区分实体和服务 → 用改需求验证
用"书店卖书"走一遍六步拆解法
第一步:把需求说成完整句子
"书店卖书"四个字,把真实场景展开:
- 顾客走进书店,挑选一本书,到收银台付款
- 收银员扫描书的条码,系统扣减库存,打印小票
- 书店有多种书,每本书有书名、作者、定价、库存数量
- 会员可以享受折扣,不同会员等级折扣不同
- 卖出去的书要记一笔交易记录,方便后面查账
- 库存不够时,店员要补货
第二步:圈名词,然后逐个筛
圈出来的名词:顾客、书、书店、收银员、会员、折扣、库存、小票、交易记录、补货单。
逐个问三个问题:
- 书:有书名、作者、ISBN、定价、库存 → 有数据、有生命周期 →建类
- 顾客:有姓名、手机号、会员等级 → 会被多处引用 →建类
- 书店:有名称、地址、库存清单、营业额 →建类
- 会员:本质是顾客的一种状态或类型,可以用枚举或继承,不一定单独建类
- 折扣:只是顾客的一个属性或计算规则,不建类
- 小票 / 交易记录:每次交易产生一条,有独立生命周期 →建类(可以叫
Order或Transaction) - 补货单:如果系统要管理补货流程,就建类;如果只是简单加库存,可以不建
- 收银员:如果只是"执行卖书动作的人",可以归到
Staff或干脆作为操作者参数,不一定单独建类
这一步筛完,核心实体大概有:Book、Customer、Bookstore、Order。
第三步:圈动词,找职责归属
动词有:挑选、付款、扫描、扣库存、打印小票、计算折扣、补货、查账。
逐个问"谁该负责":
- 扣库存:库存是书的属性,但扣库存这个动作涉及交易,放
Book上写reduceStock()可以,但更合理的是由Bookstore或OrderService来调用 - 计算折扣:跟会员等级有关,但折扣规则可能变化,适合单独抽一个
DiscountStrategy,而不是塞进Customer - 打印小票:小票是
Order生成的,Order上可以有个generateReceipt()方法 - 付款:涉及钱、支付方式、订单,放
OrderService或PaymentService,不要放Book上 - 补货:是书店的操作,放
Bookstore上,写成restock(book, quantity)
关键判断:book.sell()是错的,因为书不知道自己有没有库存、卖给谁、多少钱成交、要不要打折。卖书是一个涉及多方协作的流程,应该由Bookstore或OrderService来协调。
第四步:看关系,反推缺失实体
画一下关系:
- 顾客和书是多对多(一个顾客可以买多本书,一本书可以被多个顾客买)→ 中间必须有
Order或OrderItem - 书店和书是一对多 →
Bookstore里有List<Book>库存 - 顾客和订单是一对多 →
Customer里有List<Order> - 订单和书是多对多 → 需要
OrderItem(订单项),记录某笔订单里买了哪本书、买了几本、单价多少
这里很容易漏掉OrderItem。如果直接在Order里存一个List<Book>,就没法记录"买了两本同一本书"或者"当时成交价和标价不一样"。
第五步:区分实体、服务、工具
- 实体类:
Book、Customer、Bookstore、Order、OrderItem - 服务类:
OrderService(下单、付款)、InventoryService(库存管理)、DiscountService(折扣计算) - 枚举:
MemberLevel(普通会员、银卡、金卡)、PaymentMethod(现金、微信、支付宝) - 工具类:
PriceFormatter(价格格式化)、BarcodeScanner(条码扫描,如果要做硬件对接)
不要把"计算总价""应用折扣"全塞进Order,一旦折扣规则变复杂,Order就会膨胀。
第六步:用"改需求"验证
- "以后支持网购配送"→ 说明
Order里需要加收货地址、物流状态,Bookstore可能拆成"线上店"和"线下店",抽象出一个Store接口 - "书有精装版和平装版,价格不同"→ 说明
Book可能需要子类,或者加一个Edition实体 - "书店有多个分店"→ 说明
Bookstore之上还需要一个ChainStore,库存要按分店管理 - "支持退货"→ 说明
Order要有状态流转(已支付、已完成、已退货),OrderService要处理退款逻辑
Book — 书名、作者、ISBN、定价、当前库存 Customer — 姓名、手机号、会员等级 Bookstore — 名称、地址、库存列表、营业额 Order — 订单号、顾客、订单项列表、总价、状态、时间 OrderItem — 书、数量、成交单价 OrderService — 创建订单、付款、退货 DiscountService — 根据会员等级计算折扣