11丨软件设计的开闭原则:如何不修改代码却能实现需求变更?
11丨软件设计的开闭原则:如何不修改代码却能实现需求变更?
讲述:李智慧
时长12:22大小11.32M
开闭原则
一个违反开闭原则的例子
使用策略模式实现开闭原则
使用适配器模式实现开闭原则
使用观察者模式实现开闭原则
使用模板方法模式实现开闭原则
小结
思考题
赞 13
提建议
精选留言(42)
- 山猫2019-12-16我同意老师通过这个例子简单的描述开闭原则。但如果项目初始就对button按钮需要进行这么复杂的设计,那么这个项目后期的维护成本也是相当之高。
作者回复: 是否要使用各种设计模式设计一个非常灵活的程序,主要是看你的需求场景,而不是看项目的阶段。 如果你的场景就是需要这么灵活,就是要各种复用,应对各种变更,那么你一开始就应该这样设计。 如果你的场景根本不需要一个可复用的button,那么就不需要这样设计。 关键还是看场景。 但是场景也会变化,一开始不需要复用,但是后来又需要复用了,那么在在需要复用的第一个场景,就重构代码,而不是等将来维护困难局面hold不住了再重构。 ps 如果你习惯了这种灵活的设计,你会觉得这种设计并不复杂。对于软件开发而言,复杂的永远是业务逻辑,而不是设计模式。设计模式是可重复的,可重复的东西即使看起来复杂,熟悉了就会觉得很简单。 pps 看起来复杂的设计模式就是用来解决维护困难问题的,正确使用设计模式,看起来复杂了,其实维护简单了,因为关系和边界更清晰了,你不需要在一堆强耦合的代码里搅来搅去。真正维护成本高的其实是你所谓的简单的设计,牵一发动全身,稍不注意就是各种bug。 ppps 重要的话再说一次: 关键还是看场景。 没有银弹,没有一种必然就是好的设计方案,能理解场景的才是真·高手。
共 2 条评论132 - 陈小龙 Cheney2019-12-17希望老师给出几个阶段的代码. 方便对着代码对比学习. 直接看文字感觉抽象模糊了.共 3 条评论22
- 山猫2019-12-19没有想到我的评论老师会有那么多文字进行评论,也没有想到会有那么多赞。 看了老师的评论,其实我也知道这个和所处的场景有很大关系。 在项目开发过程初期,有很多东西是想不到的。譬如一个登陆页面, 第一版可能就是简单的账号口令登陆, 第二版可能就需要加上第三方登陆, 再往后可能需要分类型登陆, 在过一段时间可能有客户端的登陆。 还有一种可能就是这个项目真的就这一个登陆,其他的登录方式又采取微服务的办法。 有时候真的是看每个工程师的经验,还有对客户的熟悉程度。做开发好多年了,过度开发我也写过,预估不足我也写过。 需求每天都在变,虽然设计模式能够照顾到大多数的需求变化,但总有坑死人的客户和打不死的需求。 祝每个开发者好好学习技术,祝每个开发者都不要遇到坑死人的项目。 疯了,累了,痛了,人间喜剧@_@展开共 1 条评论19
- Roy Liang2019-12-17/*适配器模式*/ public class Button { private ButtonServer server; private int token; public Button(int token, ButtonServer server) { this.token = token; this.server = server; } public void press() { server.buttonPressed(token); } } public interface ButtonServer { void buttonPressed(int token); } public class Dialer { public final static int SEND_BUTTON = -99; public void enterDigit(int digit) { System.out.println("enter digit: " + digit); } public void dial() { System.out.println("dialing..."); } } public class DigitButtonDialerAdapter implements ButtonServer { private Dialer dailer = new Dialer(); @Override public void buttonPressed(int token) { dailer.enterDigit(token); } } public class SendButtonDialerAdapter implements ButtonServer { private Dialer dialer = new Dialer(); @Override public void buttonPressed(int token) { dialer.dial(); } }展开共 4 条评论15
- Jesse2019-12-16思考题 匿名内部类,已经数字按钮注册的listener其实就是DigitButtonDailerAdepter适配器的实现,sendButton中注册的listener其实就是SendButtonDailerAdepter适配器的实现。共 2 条评论14
- QQ怪2019-12-29看了2遍,敲了一遍,https://github.com/xqq1994/DesignPatternLearn共 3 条评论9
- Paul Shan2019-12-16开闭原则是移除底层的if else,取而代之的是上层的类结构。不过,我个人以为一开始的if else, 甚至switch 也没什么不妥的,毕竟代码简单直接。引入了很多类,读代码也是负担,而且也很难预料到哪些修改是必要的。当if else数量多于一定的数目,再开始重构。 不知道李老师如何看待这种观点。
作者回复: 当你准备写第一个else的时候,就说明你的代码即将陷入僵化、牢固和脆弱,而且为将来的需求变更引入了一个糟糕的“设计模式”。 如果其他人接手你的代码,他有两个选择,要么继续写更多的else以应对需求变更;要么心理暗骂一声然后重构你的代码。你希望他选择哪个?
共 8 条评论8 - 虢國技醬2020-01-20“开闭原则可以说是软件设计原则的原则,是软件设计的核心原则,其他的设计原则更偏向技术性,具有技术性的指导意义,而开闭原则是方向性的,在软件设计的过程中,应该时刻以开闭原则指导、审视自己的设计:当需求变更的时候,现在的设计能否不修改代码就可以实现功能的扩展?如果不是,那么就应该进一步使用其他的设计原则和设计模式去重新设计。” 读的过程中一直有这种感觉:开闭原则可能是软件设计和实现时最重要的原则;果然和老师最后的总结一样。👍展开
作者回复: 👍
4 - Jonathan Chan2019-12-16求老师后续给出完整代码学习!共 1 条评论3
- x-ray2021-09-14老师展示的这个过程其实还是很好的,设计的程度,其实真的是和经验有关系的,很多场景具有通用性,就是这种场景的需求变化,以后大概率会出现的,这个时候就需要提前去布局去设计。这个代码里我有个疑问,就是main函数,我认为实际的场景应该是一个指令过来,是一个String字符串,而不是直接调用sendButton或者digitalButton,这样的话,这里还是免不了if else处理,当然工厂模式或者map是可以解决这个问题的2
- Winon2020-07-05请教老师,模板方法是否也是另外一种面向过程设计?是否在充血对象模型中,模板方法的使用会相对少?
作者回复: 模板方法常和策略模式结合,为各种策略实现类提供模板和公共处理逻辑。 充血模型也需要策略和模板。
2 - yes2020-01-27我不是想找茬,我就想知道以上的代码怎么对说好的加“*” 和“#” 开闭
作者回复: starButton = new Button(); starButton.addListener( new ButtonListener() { public void buttonPressed() { dialer.enterDigit(STAR); } } );
2 - 夏天2020-05-21【 typescript实现(二)】 /** * 一个电话应用 * 有一系列的数字按钮 * 有一个拨打按钮 */ class Phone { private dialer: Dialer; public digitButtons: Button[]; public sendButton: Button; constructor() { this.dialer = new Dialer(); this.digitButtons = new Array(10); for (let i = 0; i < this.digitButtons.length; i++) { this.digitButtons[i] = new MyButton(i); this.digitButtons[i].addListener( new DigitButtonDailerAdepter(this.dialer, i), ); } this.sendButton = new MyButton(99); // 假设拨打是99 this.sendButton.addListener(new SendButtonDailerAdepter(this.dialer)); } } /** * 启动函数 */ function start() { const phone = new Phone(); phone.digitButtons[9].press(); phone.digitButtons[1].press(); phone.digitButtons[1].press(); phone.sendButton.press(); } start(); // MyButton-onPress // enter digit: 9 // MyButton-onPress // enter digit: 1 // MyButton-onPress // enter digit: 1 // MyButton-onPress // dialing...展开1
- 夏天2020-05-21【 typescript实现(一)】 // 拨号器 class Dialer { public enterDigit(digit: number) { console.log('enter digit: ' + digit); } public dial() { console.log('dialing...'); } } abstract class ButtonLister { abstract buttonPressed(token: number): void; } // 新增两个适配器,适配器去实现接口,然后调用Dialer; class DigitButtonDailerAdepter implements ButtonLister { private dialer: Dialer; private token: number; constructor(dialer: Dialer, token: number) { // super(); this.dialer = dialer; this.token = token; } buttonPressed() { this.dialer.enterDigit(this.token); } } class SendButtonDailerAdepter implements ButtonLister { private dialer: Dialer; constructor(dialer: Dialer) { this.dialer = dialer; } buttonPressed() { this.dialer.dial(); } } abstract class Button { private listeners: ButtonLister[] = []; private token: number; constructor(token: number) { this.listeners = []; this.token = token; } addListener(listener: ButtonLister) { this.listeners.push(listener); } abstract onPress(): void; public press(): void { this.onPress(); this.listeners.forEach((listener) => { listener.buttonPressed(this.token); }); } } // 利用多态实现不同的onPress效果 class MyButton extends Button { onPress() { console.log('MyButton-onPress'); } }展开1
- 张希音2019-12-23以前感觉面试的时候问设计模式都是为了面试而设计的问题,现在经过这么一分析,终于明白了设计模式的重要性,是时候去隔壁补补设计模式的知识漏洞了1
- 唐二毛2019-12-19有一点想不通,在adapter里面还是需要判断呀?这并没有 达到老师说的 避免做switch/if判断的效果,而且判断的逻辑一点不少,还无端弄出这么多类,有必要非得这么做吗?
作者回复: Adapter不需要判断,请看思考题
1 - Paul Shan2019-12-16思考题 Adapter 是在继承接口的时候调用了dialer不同的函数实现了,没有显式的Adapter。1
- java小霸王2022-07-13开闭原则,就是对修改关闭,对扩展开放,扩展就是通过新增接口的实现,对if else功能的分支要特别注意怪味道
- Geek_e54f102022-03-17这个章节干货满满
- 张闯2021-10-08给ButtonListener接口加上@FunctionalInterface注解,就是另一种感觉了。