面向对象基本概念:从类与对象到设计测试的全链路梳理

发布时间:2026/9/9 6:58:00
面向对象基本概念:从类与对象到设计测试的全链路梳理 干这行十年有个感受越来越深很多同学面向对象的概念背得滚瓜烂熟什么封装继承多态都能脱口而出可一到实际项目里类写成了“万能工具箱”继承用成了“复制粘贴”分析、设计、测试三个阶段完全脱节。今天这篇就围绕“面向对象基本概念”展开把它拆成分析、设计、测试三条线来讲透。不管你是刚学Java、C、Python的初学者还是写了两年代码想回头补基础的同学这篇文章都能帮你在动手写代码之前先把思路理顺。为了不空谈理论我会用一个“图书管理系统”的小项目贯穿全文。别嫌例子小凡是能在小例子里讲透的原理放到大项目里一样适用。面向对象最值钱的不是那几行语法而是一种看问题和组织代码的思维方式。1. 面向对象到底在讲什么——先把地基打牢1.1 类与对象一个是造图纸一个是按图纸造出来的实物很多人刚开始学面向对象会背“类是对象的抽象对象是类的实例”但一到写代码就分不清。我的理解方式很简单类就是图纸对象就是按照图纸造出来的实物。图纸上定义了“图书”应该有哪些信息书名、作者、ISBN、库存有哪些行为被借走、被归还但它本身不是一本具体的书。只有当你new了一个对象比如“《三体》这本书ISBN为978-7-536-69293-0库存5本”这个对象才有了实实在在的状态。同一个类可以new出无数个对象就像同一张图纸可以造出无数辆车每辆车的颜色、里程各不相同。代码里的差异也值得留意。Java和C里用class定义类用new创建对象Python里同样是class但实例化时不需要new关键字。语法虽然不同思想的底层是一致的把数据和操作数据的方法绑在一起形成一个自治的单元。我在带新人的时候经常强调一点对象是“有状态”的。所谓的状态就是属性当前的值。两个Book对象类都一样但状态不同——一本可借一本借出。理解了这点你才会明白为什么面向对象强调“封装”因为状态要被保护不能谁都能乱改。1.2 封装、继承、多态三大特性不是语法是设计约束封装不是把变量设为private就完事了它的本质是“隐藏内部细节暴露受控的接口”。用图书管理系统举个例子库存数量是Book的内部状态不应该允许外部代码直接book.stock 5因为这样绕过了所有业务规则。你应该提供一个borrow()方法在里面检查库存是否大于0然后才减库存。外部调用者只需要知道“我调borrow()就能借书”不需要知道库存是怎么扣的。这就是封装的价值它把变化隔离在方法内部修改内部实现不会影响外部代码。继承要小心使用。最开始学继承最容易犯的错是“为了复用代码而继承”。比如发现PaperBook和EBook都要记录书名和作者就把共同代码抽到Book里让它们继承。这没错但前提是子类和父类确实存在is-a关系电子书是书的一种这成立。如果你只是想复用两个方法而它们之间没有天然的is-a关系那应该用组合而不是继承。我见过太多代码为了省几行重复代码硬造出一个违背直觉的父类最后改需求时牵一发动全身。多态是三大特性里最抽象、也最值钱的一个。通俗地说多态就是“同一句话不同对象听了之后做出不同反应”。在图书系统里借阅过期要通知读者通知方式可能是短信、邮件、站内信。如果按面向过程的思路写你会写一堆if else去判断该发什么。而用多态的思路你定义一个Notifier接口接口里有个notify()方法SmsNotifier和EmailNotifier分别实现它。调用方只需要面向Notifier编程具体发短信还是发邮件由运行时传入的对象决定。以后要加微信通知新增一个类就行不用改调用方一行代码。多态还有两个容易混淆的概念重载和重写。重载是同一个类里方法名相同、参数列表不同属于编译期的多态重写是子类重新实现父类的方法属于运行期的多态。面试常问实际开发中也常见但很多人把二者混为一谈这里帮你理顺了。1.3 抽象与接口降低复杂度的关键手段抽象类和接口很多人觉得它们很像不知道什么时候该用哪个。我的经验是抽象类适合“定义骨架共享实现”的场景接口适合“定义契约约束行为”的场景。拿书这个例子来说Book可以设计成一个抽象类它实现了公共属性书名、作者和公共方法获取基本信息但声明一个abstract方法getFormat()让子类去描述自己是纸质还是电子。这就是模板方法思路公共逻辑父类写死变化的地方留给子类。而接口更纯粹。比如Loanable接口定义“可以借出”这个能力里面有borrow()和returnBook()两个方法签名不关心实现细节。Java里一个类只能继承一个父类但可以实现多个接口这个限制其实是在逼你多用接口做能力组合。面向接口编程带来的好处我用一句话概括调用方依赖抽象而不是依赖具体这样具体实现就能被替换。你能轻松地把老的通知服务换成新的而不必改动上层代码。后面讲设计和测试时你会发现接口是解耦和Mock的基础没有接口很多测试都无法顺利展开。2. 分析阶段先想清楚“世界是什么样”再谈怎么写代码2.1 识别对象从需求文本里“捞”出候选类和候选职责面向对象分析的第一步是把需求转成对象模型。很多项目失败不是代码写得烂而是分析阶段就没搞清楚系统里“有哪些对象、各自负责什么”。我常用的方法有两个都是经典但不花哨的技巧。第一个叫名词分析法把需求文档里的名词圈出来。比如“图书管理系统需要管理图书、读者、借书记录图书管理员可以处理借书和还书操作”。圈出来的名词有图书、读者、借书记录、图书管理员。这些大概率会成为候选类。动词则大概率会成为方法或操作比如“处理借书”就是图书管理员的一个行为落实到代码里是某个控制器或服务的方法。第二个叫CRC卡片法Class-Responsibility-Collaboration类-职责-协作。找一些卡片每张卡片写一个类的名字左边写这个类的职责右边写它需要协作的其他类。这个方法很适合团队讨论几个人围在一起把每个类存在的意义说清楚。我做过一个项目在分析会议上光靠CRC卡片就砍掉了三个看起来需要、其实没用的类因为大家发现它们没有任何职责也没有任何类需要和它们协作。做这一步时有个典型的坑把动词当成类。比如“借书”不是类“借书”是发生在“读者”和“图书”之间的行为。把操作建模成类往往会导致后面代码里出现一堆“XXXManager”“XXXUtil”这些类往往既没有状态也没有明确职责最终变成代码坏味道的源头。2.2 用例图与领域模型分析期最实用的两张图分析阶段别急着画漂亮的类图先把两件事做扎实一是用例图二是领域模型。用例图用来梳理系统的功能边界。参与者Actor是站在系统外面的人或系统比如读者、图书管理员、支付平台用例Use Case是参与者通过系统完成的一件事比如“借书”“还书”“续借”“查询逾期记录”。用例图最大的作用是让需求方和开发方在“系统到底要做什么”上达成一致。很多需求争议本质上是用例边界没画清楚管理员能不能自己借书读者能不能在移动端续借这些在用例图里都是一个个明确的用例讨论起来有的放矢。领域模型则是用概念层面的类图描述业务对象和它们之间的关系完全不涉及数据库表、框架、API。图书、读者、借书记录之间的关系在领域模型里是“读者1—借书记录—1图书”这样表达的一个读者可以有多条借书记录一条借书记录对应一本图书。这个阶段最重要的一点是忘掉技术实现只关心业务概念。如果你在领域模型阶段就开始想数据库外键、索引、ORM映射你的分析会被技术细节带偏。很多同学跳过领域模型直接从需求跳到建表结果表结构改了一轮又一轮。我的习惯是数据库表可以后面再设计但领域模型必须在分析阶段就定格因为它是后续设计和编码的锚点。2.3 职责分配单一职责不能只是口号面向对象分析的核心工作其实就是职责分配。所谓“高内聚低耦合”本质上说的是每个类做自己该做的事类之间少打交道。单一职责原则SRP在很多教科书里被写成“一个类只做一件事”。这个说法太容易理解了也容易被误执行。业界更准确的定义是一个类应该只有一个引起它变化的原因。拿图书管理员来举例如果让一个Admin类既管图书借还又管读者信息审核还要管逾期罚款计算那么任何一个业务规则变了你都要改这个类它就是一个“上帝类”内聚度极低。我在分析阶段的实操方法是先写出一份职责清单再按业务领域分组每个组对应一个类。比如图书管理维护图书信息、查询图书库存、上下架处理借阅管理创建借书记录、处理归还、计算逾期读者管理注册读者、更新联系方式、冻结异常账号三组职责明显属于三个业务域应该拆成三个类来承载后续演进成三个服务也顺理成章。这个动作看似简单但想清楚“这个类到底为什么存在”比写出漂亮的代码更能决定项目的走向。3. 设计阶段把分析结果变成可落地的方案3.1 从领域模型到设计模型补上接口、参数和异常分析阶段的领域模型是概念模型设计阶段的类图才是能指导写代码的设计模型。两者之间的差别在于设计模型必须考虑接口定义、方法签名、返回值和异常处理。举个例子领域模型里“Book”只需要有“借出”这个行为但到了设计模型你就要明确borrow()方法的参数是什么是传入一个Reader对象还是只传readerId返回值是boolean还是抛异常库存不足时是返回一个结果对象还是抛出BookNotAvailableException这些都是在设计阶段需要拍板的事。另一个重要工作是把依赖关系显式化。分析阶段你知道BookService要调用InventoryService但具体怎么调是直接new一个InventoryService还是通过接口注入我在项目里通常会让依赖面向接口。比如定义一个BookRepository接口提供findById()、save()等方法再写一个DatabaseBookRepository实现它。这样业务代码依赖的是抽象数据库实现怎么改都不影响上层。接口这一步很多人觉得麻烦觉得“就一个小小的系统搞什么接口”。但接口的开销极低收益在测试阶段就会体现。没有接口后面想做单元测试、想替换实现都得伤筋动骨。设计阶段多花五分钟加个接口测试阶段能省一上午。3.2 类之间的关系关联、聚合、组合千万别搞混UML里类与类之间的关系有好几种最容易被搞混的就是关联、聚合和组合。我在面试中问过很多候选人能讲清楚的不到一半。关联是最普通的关系表示两个类之间有某种逻辑联系但双方生命周期独立。比如Reader和Book读者可以借书但读者没了书还在书下架了读者还在。这就是关联关系通常表现为一个类持有另一个类的引用对应代码里就是一个对象的成员变量。聚合是整体和部分的关系但部分可以独立于整体存在。比如班级和学生班级没了学生还可以转去别的班级。在代码里聚合通常表现为集合类型的成员变量比如ClassRoom里有一个ListStudent但Student对象并不是ClassRoom创建和销毁的。组合比聚合更强整体与部分生命周期绑定。比如订单和订单项订单没了订单项就没有存在的意义。创建订单时创建订单项删除订单时级联删除订单项。在代码里组合通常表现为Order内部直接new OrderItem。有个特别实用的判断方法问自己“整体不存在了部分还有意义吗”如果还有意义那就是聚合如果没有那就是组合。设计和编码时搞不清这个会导致级联删除的范围出错轻则数据残留重则误删业务数据。依赖关系也要注意。依赖比关联更弱通常体现在方法参数、局部变量或静态方法调用上。比如BookService的exportReport()方法里用到ExcelUtil这就是依赖不体现在成员变量里。依赖关系越少类的独立性越强测试起来越容易。3.3 设计原则与设计模式用在该用的地方设计部分绕不开两个高频热词设计原则和设计模式。很多同学一上来就背SOLID背二十几个模式但不知道怎么用。我的建议是原则优先模式其次模式不是越多越好而是“痛点驱动”。SOLID五条原则里我实际项目中最有体感的是开闭原则对扩展开放对修改关闭和依赖倒置原则依赖抽象不依赖具体。这两个原则做到了你就能在不修改旧代码的情况下加新功能。比如图书逾期通知从一开始就设计Notifier接口后面加短信、加邮件、加微信都是新增类不动既有代码。这就是开闭原则的落地。设计模式方面新手最容易掌握也最容易滥用的是单例模式。单例用来保证全局只有一个实例比如系统配置类、日志服务这合理。但很多人图省事把所有服务类都做成单例结果状态满天飞测试难写。我的经验是真正需要单例的场景极少大多数时候直接new一个传进去就够了。工厂模式也很实用。图书系统里要创建不同类型的图书纸质书、电子书可以在BookFactory里根据参数决定实例化哪个子类调用方不需要关心具体的new逻辑。策略模式用的场景是“算法有多个变体运行时选择”比如计算逾期罚款时普通图书和期刊的费率不同可以把费率计算封装成不同的策略类。观察者模式在处理“状态变化后要通知别人”的场景里非常好用。图书状态变为“已归还”后需要通知读者、更新统计、清空预约队列如果把这些逻辑硬编码在returnBook()方法里这个方法会膨胀得可怕。用观察者模式图书归还时只广播一个“图书已归还”事件订阅者各自处理调用方保持干净。我见过不少团队陷入“过度设计”的泥潭账还没算清楚先把十几个设计模式套上去最后没人改得动代码。记住一句话设计模式解决的是具体问题的通用解法出现问题了才用没有痛点就别套。4. 测试阶段面向对象的代码该怎么测才靠谱4.1 面向对象特性给测试挖的坑很多人在测试阶段不适应是因为面向对象代码和面向过程代码的测试思路差别很大。封装、继承、多态这些设计特性会给测试带来一些天然障碍提前知道才能绕开。封装带来的问题是内部细节藏起来了你没法直接构造所有状态。比如Book类里库存是一个private字段你测试borrow()时想先把库存设置为0但外部访问不到。这时候不应该通过反射去改私有字段而应该通过公开的行为去触发状态变化——先借几本把库存借到0或者提供一个测试专用的构造器。测试应该是行为驱动的而不是和内部实现纠缠。继承给测试带来的问题更隐蔽。如果你为父类写了一组单元测试子类继承了父类的代码那这些测试对子类也成立吗通常不成立因为子类可能重写了方法或者改变了前置条件。我踩过的一个坑是父类的测试全部通过子类继承后没跑父类的测试结果子类重写的方法在边界情况下出了Bug。后来我定了条规矩每个类不管是否继承父类都要有自己的测试类重点测它重写和新增的行为。多态给测试带来的问题是调用同一个方法实际执行的是哪个实现运行时才确定。因此测试时不能只看接口要覆盖所有实现类。你有一个Notifier接口、三个实现类别只测了EmailNotifier就以为万事大吉SmsNotifier和WechatNotifier的逻辑是各自独立的都要有对应的用例。4.2 单元测试、集成测试和系统测试各测各的层次面向对象的测试策略通常分三个层次单元测试、集成测试、系统测试。单元测试针对单个类。目的是隔离验证这个类的独立逻辑比如BookService.calculateFine()的逾期费用算法。写单测时有个核心原则只测这一个类它的协作对象都用测试替身代替。这样才能保证测试失败的原因是自身逻辑出错而不是队友掉了链子。以JUnit为例一个典型的单元测试长这样class BookServiceTest { private BookRepository mockRepo; private BookService service; BeforeEach void setUp() { mockRepo mock(BookRepository.class); service new BookService(mockRepo); } Test void borrow_shouldDecreaseStock_whenBookIsAvailable() { Book book new Book(978-7-536-69293-0, 5); when(mockRepo.findById(978-7-536-69293-0)).thenReturn(book); service.borrow(978-7-536-69293-0); assertEquals(4, book.getStock()); } }这段代码要表达的不是具体语法而是思路依赖通过构造器注入测试时传一个mock进去把外部依赖掐断然后专心验证核心逻辑。集成测试关注的是类与类的协作。比如BookService调用InventoryService、NotificationService集成测试要验证这些对象串起来后调用链是否能正常走通。这个阶段可以用真实对象但外部资源数据库、第三方接口建议还是用轻量的替代方案比如内存数据库。系统测试站在用户视角验证完整业务流程读者选书、借出、库存扣减、生成借书记录、通知读者。到了这一层往往还要做回归测试防止上一个版本改动的功能影响旧的业务。测试金字塔是个很好的参考底层大量快速的单元测试中间一圈较少的集成测试顶端少量端到端系统测试。遵循这个比例你的测试套件跑得快反馈及时维护成本也可控。我见过很多项目把大量测试堆在系统层每次回归跑一个小时还没跑完已经没耐心了最终测试沦为摆设。4.3 TDD与自动化测试从“写完再测”到“测试驱动”面向对象时代的测试还有一个更高级的形态测试驱动开发TDD。核心循环是Red-Green-Refactor先写一个会失败的测试再写刚好让测试通过的实现最后重构代码让结构更清晰。用一个通俗的比喻来说TDD不是先盖楼再验收而是先写验收标准再按标准盖楼。它逼着你从调用方视角思考这个类应该有什么公共行为参数是什么返回值是什么很多设计上的别扭会在写测试的第一时间暴露出来——如果这个类测起来很麻烦大概率是它的设计有问题耦合太重或者依赖太多。实际项目中我发现TDD还有一个隐性好处倒逼依赖注入和面向接口设计。为了让一个类能在测试中独立运行你就得把它的依赖通过构造器传进来而不是在方法内部new一个。这个过程自然地把代码往低耦合方向推。自动化测试是另一件值得投入的事。单元测试和集成测试写好了要让它自动跑起来才有价值。接入持续集成CI后每次代码提交都会自动触发测试有问题第一时间拉响警报。我在团队里还习惯让测试报告接入通知测试失败就发消息到群里谁改的谁负责不让问题过夜。自动化测试的关键是稳定和快速。别把脆弱的、依赖大量外部资源的用例放进自动化套件否则天天误报大家用脚投票最后连CI都懒得看一眼。我踩过最大的坑是用一个对网络波动特别敏感的第三方验证码接口做自动化集成测试结果每周都有两三次因为对方服务抖动而全组通告“测试失败”查半天发现是外部问题白白消耗大家精力。后来我把这类用例全部改成Mock把真实调用迁移到独立的冒烟测试环境问题才根治。4.4 测试维护的心得测试代码也是工程资产最后聊一个容易被忽略的话题测试代码的维护。在很多团队里测试代码被视为“附属品”写得比较随便。但测试代码和业务代码一样是长期维护的工程资产烂测试比没测试更可怕。我的几条实操心得如下。第一不要为了覆盖率而写测试。我见过把getter/setter都写单测的项目覆盖率冲到90%但实际上最核心的业务逻辑反而没覆盖到。有效的覆盖率看的是“关键分支有没有跑过”不是“方法有没有被调到”。第二测试命名要把行为说清楚。test1、test2这种命名等于没写。我习惯用“方法名_场景_预期结果”的格式比如borrow_shouldThrowException_whenStockIsZero别人一眼就看出这个用例在验证什么失败时排查也快。第三当业务代码重构时测试代码要同步调整并且调整测试代码是有价值的。很多人重构业务代码时希望测试代码完全不用动这不现实。合理的做法是重构时保持公共接口稳定这样测试代码大部分不用变如果公共接口确实要变那测试代码的更新应视为重构工作量的一部分不要省略。第四Mock要克制不要过度。Mock能隔离依赖但也会让测试脱离真实环境。如果一个用例里Mock了超过三五个对象你要开始警觉是不是被测类的依赖太多了。依赖太多往往说明设计出了问题而不是测试的问题。这时候应该回头调整类结构而不是硬写测试。从分析到设计再到测试面向对象不是三个孤立的知识点而是一条贯穿始终的主线。分析阶段解决“系统里有哪些对象、各自负责什么”设计阶段解决“这些对象如何协作、如何依赖”测试阶段验证“这些对象是否正确完成了它们的行为”。如果你在写每一行代码前都能用这个思路自问一遍那你基本就拿到面向对象的真东西了。我个人的建议是别急着用复杂的框架拿这个小图书系统把分析、设计、测试各写一遍。哪怕只写一个借书功能也把类图画出来、接口设计好、Mock写起来全套走完。踩过几次坑之后你会发现面向对象不再是一堆概念的堆砌而是你自然而然思考问题的方式。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询