ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

软件设计师下午题:面向对象、UML与设计模式一体化通关

软件设计师下午题:面向对象、UML与设计模式一体化通关 软件设计师这门考试有个挺有意思的现象上午题拼的是记忆面的宽度下午题拼的是手上功夫。而所谓手上功夫八成集中在面向对象、UML、设计模式这三块上。我见过不少人上午题能考到五十多分下午题却卡在四十分出头翻来覆去就是类图画不对、多重度标不准、代码填不进去——问题不在知识量在于没把这三块当成一个整体来练。这篇内容就是围绕这条主线展开的面向对象是思维方式UML 是表达这种思维的图纸语言设计模式是前人总结出的成熟图纸模板。三者是递进关系不是三门并列的课。我会从考试构成出发把 OO 三特性落到真实代码里讲透把用例图、类图、顺序图、状态图、包图的判定规则和扣分点一条条摆出来再挑出几类真正高频的设计模式讲识别信号最后给我自己的备考节奏安排。适合正在啃中级软件设计师、或者准备设计模式期末和大作业的人参考也适合已经会写代码但没系统画过 UML 的同学补课。1. 从真题倒推软设下午题为什么总绕着这三块转1.1 试卷结构决定了复习的重心在哪里中级软件设计师的下午卷是五道必做加一道选做总分七十五分。必做题里数据流图、数据库设计、UML 图、算法与数据结构C 语言描述各占一道最后一道是 Java 或 C 的选做题干通常给一段带空缺的代码加一张残缺的类图。你会发现UML 那道题和最后的选做代码题本质上考的是同一套东西——只不过一个让你画一个让你写。这就是为什么我不建议把面向对象UML设计模式当成三个独立模块去背。你画的那张类图就是设计模式的骨架你填的那几行代码就是类图的落地实现。考试设计者本来就想让它们互相印证。我统计过自己刷过的近十年下午真题UML 那道题出现频率最高的图形是类图和用例图顺序图和状态图次之包图、活动图偶有出现。选做代码题里出现过的模式集中在策略、工厂方法、抽象工厂、观察者、模板方法、装饰器、单例、适配器这几类冷门的解释器、访问者几乎没单独考过。这个分布直接决定了你的复习投入比例而不是把二十三种模式平均用力。1.2 大多数人复习顺序是反的一个很常见的做法是这样先买一本知识点总结把封装继承多态UML 九种图二十三种设计模式挨个背一遍然后再去做题。背的时候感觉都懂一做题就发现——题干说系统需要支持多种支付方式且未来可能新增你知道这是要上策略或者工厂但类图上那个菱形到底该画空心还是实心就卡住了。更合理的方向是倒过来先做两三套下午真题暴露自己真正卡在哪个环节是读不懂需求文字还是不知道该画哪种图还是关系标注拿不准。带着这些具体问题再回去补知识点记忆效率完全不一样。我自己第一次做下午题时最大的障碍其实是不知道题干里哪句话对应哪个类跟背不背设计模式关系不大。1.3 把三块内容串成一条主线的判断标准我给自己定了一个很朴素的自测标准拿到一段三百字的需求描述能不能在十五分钟内产出一张有类名、有属性、有方法、有关系标注含多重度的类图并且能说清楚每个关系为什么这么标。能做到这一步UML 那道题基本稳了再能把类图里的抽象类、接口位置指出来说明你已经具备了选做代码题的骨架能力。这条标准背后其实是三块知识的合流读需求靠面向对象的抽象能力画图靠 UML 的语法而类与类之间怎么组织才合理靠的是设计模式积累的套路感。所以后面的章节我会按这条线走先讲思维再讲图纸最后讲套路。2. 面向对象把封装、继承、多态放进真实代码里理解2.1 类与对象数据和行为绑在一起才有意义教科书上会说类是对具有相同属性和行为的对象的抽象这句话没错但太干。换个说法类就是一张表格模板对象就是按这张模板填出来的具体一行。为什么要把数据属性和行为方法绑在一起因为一旦分开数据就可能被任何代码随意修改出了问题你根本找不到是谁改的。看个反例就很清楚。假设用最朴素的写法处理一个银行账户balance 1000 def withdraw(amount): global balance balance - amount任何一段代码只要写一句balance -99999整个账户体系就废了因为余额是裸露在全局的。改成面向对象的写法class Account: def __init__(self, balance): self.__balance balance def withdraw(self, amount): if amount 0: raise ValueError(金额必须为正) if amount self.__balance: raise ValueError(余额不足) self.__balance - amount def get_balance(self): return self.__balance余额被双下划线修饰成了私有属性外部只能通过withdraw和get_balance访问。所有对余额的修改都必须经过这两个方法校验逻辑就只有一份。这就是封装的实际价值——不是把变量藏起来这种形式动作而是把所有修改入口收拢到一个地方方便加规则、方便排查问题。2.2 继承不是复用代码的万能钥匙继承看起来很诱人class SavingsAccount(Account)一写父类的方法全归子类了。但继承建立的是 is-a 关系是最强的耦合。父类改一个方法签名所有子类都得跟着改。需求一变化继承层次就容易长成三层四层的怪物。所以有个合成复用原则也常被叫做组合优于继承能用组合has-a表达的关系就别用继承。举个例子汽车有发动机应该是组合货车是车才是继承。判断方法很直接——造个句子读一读。说学生是一个人通顺那就是继承说班级是一个学生不通顺那就不是继承。考试里选项往往会给用继承实现 A 和 B 的关系如果 A 和 B 是整体部分关系那正确答案基本就是聚合或组合。2.3 重写和重载两个中文只差一个字的概念这是下午题里最容易混的一处必须掰清楚。重写override发生在父子类之间子类重新实现父类已有的方法方法名、参数列表、返回类型都一样。它靠的是运行时的动态绑定也就是说编译时看的是父类引用运行时才决定调哪个版本。这就是运行时多态。重载overload发生在同一个类里方法名相同但参数列表不同。编译器在编译阶段就能根据实参类型确定调哪个属于编译时多态。class Shape { public double area() { return 0; } } class Circle extends Shape { private double r; public Circle(double r) { this.r r; } Override public double area() { return Math.PI * r * r; } } class Calc { public double sum(double a, double b) { return a b; } public double sum(double a, double b, double c) { return a b c; } }Circle的area()是重写Calc的两个sum是重载。考试里描述成同一消息可以根据发送对象的不同采用不同的行为方式说的就是重写带来的运行时多态。多态真正的用处在于把变化挡在调用方之外。上层代码只写for (Shape s : shapes) total s.area();将来新增三角形、矩形这行循环一个字都不用改。这才是设计模式大量依赖多态的根本原因。2.4 抽象类与接口选择哪一种抽象类表达的是一种半成品——它既可以有已实现的方法也可以有纯抽象方法子类只能单继承。接口表达的是一份能力契约——只声明不实现Java 8 之后允许默认方法一个类可以实现多个。选哪个的判断标准如果多个类共享同一套状态和部分实现用抽象类如果只是约定一组行为用接口。设计模式里工厂方法常配抽象类策略、观察者、命令这类常配接口原因就是后者只需要能执行某件事不需要共享状态。2.5 三种语言写法的对照软考下午选做题是 Java 和 C 二选一但很多学校的课程和大作业会用到 Python、C#。我整理了一张对照表主要看访问控制、继承和接口的写法差异概念JavaCPythonC#私有成员privateprivate:__name名字改编private继承extendsclass B : public Aclass B(A)class B : A接口interface纯虚函数抽象类抽象基类abcinterface方法重写标记Overridevirtualoverride直接重定义override抽象方法abstract 0abstractmethodabstractPython 里没有真正意义上的私有__balance只是被解释器改成了_Account__balance属于约定俗成的保护。搞清这一点看 Python 实现的设计模式代码时就不会被为什么还能访问绕住。3. UML 图考场上画得对只是及格线画得快才是优势3.1 用例图include 和 extend 的判定套路用例图考的次数不算最多但一旦出现扣分点几乎全在 include 和 extend 的方向上。判断方法其实很机械include包含多个用例里重复出现的、必然执行的一段公共行为被抽成一个独立用例。箭头从基础用例指向被包含用例标注include。extend扩展在特定条件下才发生的、可选的行为。箭头从扩展用例指向基础用例标注extend。拿图书馆系统举例。借书这个用例每次都要先验证读者身份验证是必然发生的公共步骤所以借书include验证读者身份。而缴纳逾期罚款只在读者有超期图书时才发生属于可选扩展所以缴纳罚款extend借书。方向搞反是最高频的错误。参与者还分主参与者主动发起和次参与者被动响应比如借书里读者是主图书管理系统对接的支付网关是次。题干里出现由...触发需要...配合完成这类词就要留意区分。3.2 类图六种关系的表示法和语义类图是重头戏下面这张表建议直接记住考场上看一眼就能判断关系图形表示语义代码中的体现依赖虚线 开放箭头临时使用方法参数、局部变量关联实线可带箭头长期结构关系成员变量聚合实线 空心菱形整体-部分部分可独立存在成员变量构造函数注入组合实线 实心菱形整体-部分同生共死成员变量内部 new 出来泛化实线 空心三角is-a继承extends/:实现虚线 空心三角实现接口implements聚合和组合的分界我一般这么判断整体被销毁时部分还能不能活。学院和学生是聚合学院撤销了学生还在订单和订单项是组合订单删了订单项也就没意义了。菱形永远画在整体那一端箭头指向部分。多重度也是常考填空。常见的写法有1、0..1、*、1..*、0..*。题干里一个订单可以包含任意多个订单项就是1对*每个订单必须有一个且只有一个客户就是1对1。这里最容易错的是把可以和必须搞混*和1..*的区别就在这。3.3 顺序图与状态图消息和迁移的细节分顺序图的消息区分三种同步消息是实心箭头调用方会阻塞等待异步消息是开放箭头调用完立刻返回返回消息是虚线加开放箭头。生命线上的矩形条表示激活期也就是对象正在执行操作的时间段。顺序图里的编号其实是可选的但写上能体现调用顺序不写也不算错。状态图考的是状态 事件 迁移三元组。矩形圆角框是状态实心圆是初始状态牛眼圆里套实心圆是终止状态。迁移箭头上的标注格式是事件[守卫条件]/动作比如提交订单[金额0]/扣减库存。守卫条件放在方括号里动作放在斜杠后面写反或者漏掉方括号都会扣分。状态图里有个容易忘的点同一个状态可以有自迁移箭头从自己出来又指回自己表示在状态内部响应某个事件但不改变状态比如待支付状态下修改订单。3.4 包图与部署图分层结构的表达方式包图用来表达系统的分组结构形状就是文件夹图标。包之间的关系主要两种依赖虚线开放箭头和泛化实线空心三角。经典的题是三层架构——表示层依赖业务逻辑层依赖数据访问层箭头方向从上层指向下层不能画反。部署图描述的是物理节点的部署关系节点画成三维立方体节点之间的通信用连线表示。这类图在实际考试里出现频率低但一旦考到往往是让你补节点之间的连接关系判断依据是题干里部署在...服务器上通过...协议通信这类句子。3.5 用 Visio 画类图的顺手的几个设置画图工具不限定Visio 是最常见的。我用下来有几点经验新建时直接选软件和数据库分类下的 UML 模型图比用空白绘图再拖形状省事得多因为它自带了类、接口、包、关系连接线。拖出类形状后在形状的属性对话框里填属性和方法比双击文本框手打更快而且会自动应用标准的可见性符号公开、-私有、#保护。画关系线之前先打开对齐网格和连接线让线自动吸附到形状的连接点上避免线头悬空——线头悬空在打印出来或者截图后特别显眼也容易被判成关系不明确。泛化、聚合这些关系有专门的形状别用普通箭头硬拼箭头样式不对直接影响判分。画完统一调整字号和排列建议全部左对齐类与类之间留出固定间距视觉上会比随手摆的干净很多。4. 设计模式二十三种不必全背先啃透这几类4.1 创建型模式解决对象怎么造出来创建型一共五个简单工厂不算 GoF 的正式成员但考点上非常高频必须掌握。核心矛盾是如果调用方直接 new 具体类需求一变化就要改调用方代码。模式一句话本质典型触发词简单工厂一个工厂类按参数造不同产品根据类型创建工厂方法父类定义创建接口子类决定造什么每种产品由对应工厂创建抽象工厂创建一族相关产品整套界面风格切换单例全局只有一个实例唯一共享配置管理器建造者分步组装复杂对象多个可选部件组合原型复制现有对象克隆深拷贝单例是最简单的也是最容易写错的。饿汉式在类加载时就创建实例线程安全但可能浪费资源懒汉式延迟创建但必须处理并发双重检查锁定的写法在 Java 里还得给实例字段加volatile否则可能出现指令重排导致的半初始化对象。软考里常让你补全getInstance()方法volatile和同步块是常见的两个空。简单工厂虽然简单但它的可扩展性差新增产品就要改工厂类的 switch。工厂方法把这个变化下移到子类符合开闭原则所以考试里经常给出简单工厂的实现让你改成工厂方法。4.2 结构型模式解决类与类怎么拼装结构型七个考点最集中在这几个适配器接口不兼容时的转接器。触发词是已有的类接口不匹配需要复用遗留代码。装饰器在不改变原类的前提下动态叠加功能。触发词是动态添加多种功能自由组合典型的例子是给输入流套缓冲、套加密。代理为对象提供替身控制访问。和装饰器的区别在于代理的目的是控制装饰的目的是增强。外观给一个复杂子系统提供统一入口。触发词是简化调用提供统一接口。组合把树形结构中的叶子节点和容器节点统一对待。桥接把抽象和实现分离成两个维度各自独立变化。触发词是两个维度正交变化比如形状和颜色。享元共享细粒度对象节省内存。触发词是大量相似对象外部状态和内部状态分离。装饰器和代理长得像区分点我总结成一句装饰器是你主动给它加东西代理是你不得不多走一层。装饰器的装饰者和被装饰者通常实现同一接口代理也是但代理一般自己持有真实对象的引用由它决定要不要转发。4.3 行为型模式解决职责怎么分配行为型十一个高频的有策略、观察者、模板方法、命令、责任链、状态这几个。策略把一堆可互换的算法各自封装起来让它们可以互相替换调用方只依赖抽象策略。触发词是多种算法可切换运行时选择不同算法。考试里最常见的实现结构是抽象策略接口 若干具体策略类 一个持有策略引用的上下文类。观察者建立一对多的依赖被观察者状态改变时自动通知所有观察者。触发词是变化通知订阅联动更新。抽象主题维护一个观察者列表提供attach、detach、notify三个方法是所有观察者题目的固定套路。模板方法在抽象类里定义算法骨架把变化的步骤留给子类实现。它和策略的区别在于策略是用组合替换整个算法模板方法是用继承替换算法中的某几个步骤。题干里出现流程固定但某些步骤不同就是模板方法出现整个算法都要能换就是策略。命令把请求封装成对象从而支持排队、记录日志、撤销重做。责任链把处理者串成链请求沿链传递直到被处理。状态把状态迁移封装进状态类避免大段的 if-else 判断——它和策略的结构几乎一样区别在于策略之间是平级的、由客户端选状态之间会自行迁移。4.4 识别模式的三个信号做题时不用去猜出题人想考哪个模式题干里通常有信号词。我总结了三条找变化点题干里出现根据...不同当...时未来可能增加...说明这里需要一个可替换的抽象多半是策略、工厂或状态。找扩展方向出现不修改原代码的前提下增加新功能基本是装饰器增加新的产品种类是工厂方法增加新的处理环节是责任链。找耦合位置出现需要屏蔽子系统的复杂性是外观需要复用接口不兼容的旧类是适配器需要控制对某对象的访问是代理。这三条信号在真题里的命中率相当高。练到后面你会发现读完题干的前两句话模式的轮廓基本就出来了。4.5 选做代码题的答题节奏最后一道题的结构非常固定给一段带空缺的 Java 或 C 代码配一张缺了几处的类图让你填类名、填关系、填方法体。我的做法是分三步走第一步先通读题干需求在草稿纸上把名词圈出来这些通常是类把动词圈出来这些通常是方法。第二步对照已有代码判断哪些类已经确定、哪些需要补。空缺的类名往往在题干里有明确命名提示比如客户信息存储类就写CustomerStorage之类重点是把抽象类或接口的位置找准。第三步最后填方法体。方法体里通常只有一两行重点是理解上下文。比如策略模式里的上下文类空缺处一般是strategy.algorithm()模板方法里的抽象类空缺处一般是子类实现的具体步骤。注意如果 Java 和 C 都能做选你更熟的那个。我见过有人因为 C 里虚函数和纯虚函数的语法细节丢分明明思路全对最后只拿到一半分数。5. 一道下午题的完整推演从需求文字到代码填空5.1 读题阶段把散文翻译成对象清单假设题干描述的是一个咖啡店点单系统顾客选择咖啡类型不同咖啡价格不同可以额外加牛奶、加糖、加摩卡每种配料单独计价最终要能计算出总价并打印明细。先圈名词顾客、咖啡、拿铁、美式、配料牛奶、糖、摩卡、订单、总价。再圈动词选择、加、计算、打印。名词里的咖啡是抽象概念具体的是拿铁、美式所以大概率需要一个抽象类Coffee和两个子类配料有多个且可叠加说明它不是继承而是包裹——这时候装饰器的特征已经很明显了。5.2 画图阶段搭骨架和标关系先画抽象类Coffee属性放description和price方法放getDescription()和cost()。两个子类用泛化箭头指过来空心三角。配料类CondimentDecorator继承自Coffee——注意这一步很关键装饰器必须和被装饰对象是同一个类型否则就没法层层包裹。CondimentDecorator里持有一个Coffee类型的成员变量这条关系是关联实线多重度是1。三个具体配料类再泛化到CondimentDecorator。Order类持有客户信息和Coffee是关联关系多重度1对1..*。如果题干说一个订单包含多个咖啡就是1对*。5.3 填代码阶段装饰器的经典写法Java 版本大致长这样public abstract class Coffee { protected String description 未知咖啡; public String getDescription() { return description; } public abstract double cost(); } public abstract class CondimentDecorator extends Coffee { protected Coffee coffee; public CondimentDecorator(Coffee coffee) { this.coffee coffee; } } public class Mocha extends CondimentDecorator { public Mocha(Coffee coffee) { super(coffee); } Override public String getDescription() { return coffee.getDescription() , 摩卡; } Override public double cost() { return coffee.cost() 5.0; } }装饰器的精髓就在coffee.getDescription() , 摩卡和coffee.cost() 5.0这两行——它在调用被装饰对象结果的基础上做增量。考试里如果这个空缺是让你填的写成直接返回摩卡或5.0就完全错了因为那样就丢失了内层信息。5.4 复盘我在这类题上踩过的几个坑第一个坑是多重度写反。有一次题干说一个客户可以有零个或多个订单我下意识写了1..*其实应该是0..*可以和必须是两回事。第二个坑是把装饰器当继承。看到加牛奶加糖第一反应是设计成CoffeeWithMilk extends Coffee结果发现配料可以无限叠加继承层次根本盖不住。只要题干里出现可以任意组合数量不限这类描述就应该往装饰器或者组合的方向想。第三个坑是接口和抽象类互换。某次题干里抽象类已经明确带有属性description我还硬把它写成接口导致getDescription()没法给出默认实现整个类图都跟着错。6. 我自己的六周备考节奏和资料取舍6.1 六周时间怎么分配周次重点任务每日投入第 1 周面向对象基础 三种语言语法对照60 分钟第 2 周UML 六种关系 用例图、类图精练90 分钟第 3 周顺序图、状态图、包图 Visio 手感训练60 分钟第 4 周创建型和结构型模式逐个人写一遍90 分钟第 5 周行为型模式 近五年下午真题刷完120 分钟第 6 周错题回炉 限时模拟120 分钟这个安排的核心逻辑是前两周打地基中间两周练图纸最后两周上手真题。设计模式必须自己手敲一遍光看是记不住的。我当时把策略、观察者、装饰器、工厂方法、模板方法各写了一版能跑通的代码写完之后再看真题代码空缺处几乎不用想。6.2 资料怎么挑别贪多知识点总结类的资料挑一本就够关键是它得有图。纯文字总结对 UML 部分基本没用。真题必须用带图的完整版光看题干和答案选项你练不出画图的手感。视频课如果看建议只挑 UML 和设计模式这两块面向对象的基础部分看书更快。另外提醒一句真题解析里对同一道题的答案有时会有分歧尤其是多重度和关系类型遇到分歧就以题干里的原话为最终依据别迷信任何一家解析。6.3 几个我觉得真正有用的小习惯画类图的时候我习惯先把所有类的名字写成一行从中间那个最核心的类开始画起其他的往两边排。这样出来的图层次比较清楚也不容易漏掉类。做题时准备一张草稿纸专门记不确定的点——比如这里到底是聚合还是组合这个多重度是 1 还是 0..1。做完对照答案时优先解决这些点比大面积重看知识点效率高得多。还有一个是我吃过大亏的画完图一定要回头读一遍题干的每一句话确认每句话都在图上有对应体现。我曾经因为漏读了最后一句系统还需要记录每次操作日志白丢了一个类和一个关联关系这类失分最可惜。最后说个我个人的体会。这三块内容真正难的地方不在记忆在于把它们变成一个整体的直觉——看到可能新增就想到开闭原则看到多种算法就想到策略看到动态叠加就想到装饰器。这个直觉没有捷径就是真题画图、写代码、复盘来回滚几轮自然就有了。你要是现在还在纠结该先背哪个知识点不如直接翻开一套下午真题从读题开始练起。
返回列表