软件设计知识是一名软件开发人员必须要懂的知识,最近几天今天看了bob大叔的《敏捷软件开发》一书和软件设计相关的一些blog和资料,自己做了一个学习笔记
设计目标
正确性、健壮性、灵活性、可重用性、高效性
降低复杂性
所谓复杂性,就是任何使得软件难于理解和修改的因素。
复杂性的来源主要有两个:代码的含义模糊和互相依赖
- 模糊指的是,代码里的重要信息看不出来;
- 依赖指的是,某个模块的代码,不结合其他模块的代码,就无法理解
危害:复杂性的危害在于,它会递增。如果做错了一个决定,导致后面的代码都基于前面的错误实现,只会越来越复杂。"常听人说,我们先把产品做出来,后面再改进",这很难做到。
==关键==:找出易变化的部分,合理抽象。
用抽象构建框架、用实现扩展细节
代码抽象三原则:
1. Don’t Repeat Yourself (DRY)
- 系统的每一个功能都应该有唯一的实现。也就是说,如果多次遇到同样的问题,就应该抽象出一个共同的解决方法,不要重复开发同样的功能
不要重复自己
2. You Ain’t Gonna Need It (YAGNI)
- 定义:你不会需要它,只考虑和设计必须的功能,避免过度设计
- 这是"极限编程"提倡的原则,指的是你自以为有用的功能,实际上都是用不到的。因此,除了最核心的功能,其他功能一概不要部署,这样可以大大加快开发
- 你会发现DRY原则和YAGNI原则并非完全兼容。前者追求"抽象化",要求找到通用的解决方法;后者追求"快和省"
3.Rule Of Three
称为"三次原则",指的是当某个功能第三次出现时,才进行"抽象化"。
这样做有几个理由:
省事。如果一种功能只有一到两个地方会用到,就不需要在"抽象化"上面耗费时间了。
容易发现模式。"抽象化"需要找到问题的模式,问题出现的场合越多,就越容易看出模式,从而可以更准确地"抽象化"。
防止过度冗余。如果一种功能同时有多个实现,管理起来非常麻烦,修改的时候需要修改多处。在实际工作中,重复实现最多可以容忍出现一次,再多就无法接受了。
小结:综上所述,"三次原则"是DRY原则和YAGNI原则的折衷,是代码冗余和开发成本的平衡点,值得我们在"抽象化"时遵循
面向对象的S.O.L.I.D原则
1. SRP职责单一原则
- 核心思想是一个类只做一件事,把事情做好,其只有一个引起它变化的原因,职责过多,引起它变化的原因就越多,将导致责任依赖增加耦合性
- 遵循单一职责原的优点有:
- 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
- 提高类的可读性,提高系统的可维护性;
- 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
- 应用场景:迭代器
2. 里氏替换原则
- 定义:所有引用基类的地方必须能透明地使用其子类的对象,替换之后,代码还能正常工作。它是使代码符合开闭原则的重要保证.
- 问题:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障
- 解决:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法
- 换个说法是,子类可以扩展父类的功能,但不能改变父类原有的功能(不能破坏继承体系)
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格
3. 接口隔离原则
- 定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
- 规约:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少,但不要过度。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 示例:人们对电脑有不同的使用方式:如上网、看电影、写文档、玩游戏、通讯、计算和存储等.如果都把这些功能都定义在电脑的抽象类里面。那各种功能类型的电脑(如上网本、服务器、PC、智能学习机)都要现实这些接口。所以,应该把这些接口隔离开,这样不同功能的电脑只需要实现自己需要的接口.
接口隔离2.jpg
接口隔离1.jpg
4. 依赖倒置原则
- 定义:高层模块不应该依赖底层模块的实现,而是依赖高层抽象。而且,二者都应该依赖于抽象。抽象不应该依赖细节;细节应该依赖抽象
- 问题:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险
- 解决:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率
- 核心思想:面向接口编程,在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
- 好处:可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。
- 应用模式:工厂模式
- 规范:实际编程中,最好做到如下3点:
- 低层模块尽量都要有抽象类或接口,或者两者都有。
- 变量的声明类型尽量是抽象类或接口。
- 使用继承时遵循里氏替换原则
如果依赖的是一个稳定的具体类,那么可以直接依赖它
- 层次化
- 方式1:高层模块依赖底层实现模块,这种依赖性是传递的
依赖倒置1.png
- 方式2
- 解除传递依赖关系
- 接口所有权倒置:客户拥有抽象接口,它的服务者从这些抽象接口中派生,底层模块实现了在高层模块声明并被高层模块调用的接口
依赖倒置2.png
5. 开放/封闭原则
- 对扩展开放,对修改关闭。如果有新的需求和变化可以对现有代码进行扩展,以适应新的情况.而不是对原有代码进行修改.
- 解决:关键在于抽象、预测和刺激变化
- 应用模式:装饰者模式(如java.io包),不改变原有代码扩展对象的行为
其他原则
1. Keep it sample,stupid(Kiss)
- 保持简单、直接,不要复杂化
- 模块分成接口和实现。接口要简单,实现可以复杂。
- 好的 class 应该是"小接口,大功能",大量的功能隐藏在简单接口之下,对用户不可见,用户感觉不到这是一个复杂的 class.比如Unix 的文件读写接口
2. Program to an interface, not an implementation
- 面向接口编程,而不是实现
- 工厂模式、策略模式等等
3. 高内聚低耦合
- 将模块间的耦合降到最低,努力让一个模块做到精益求精.内聚意味着独立和重用,耦合意味着多米诺骨牌效应.
4. 迪米特法则
又称最少知识原则,该原则告诉我们要降低耦合。
定义:一个对象应该对其他对象保持最少的了解
问题:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
解决:尽量降低类与类之间的耦合
对于对象 ‘O’ 中一个方法’M’,M 应该只能够访问以下对象中的方法:
- 对象O本身
- 参数对象
- 与对象O直接相关的对象
- 方法中创建或实例化的对象
应用模式:外观模式,提供一个统一的接口来访问子系统中的一群接口。外观定义了一个高层接口,让系统更容易使用
5. 好莱坞原则
- 你不要找我,我会找你 。 高层组件对待底层组件的方式:别调用我们,我们会调用你
- 应用场景:
- 观察者模式,以通知替代轮询
- 工厂模式
- Ioc依赖注入,DI控制反转设计的基础,所有组件都是被动的,初始化和调用都由容器负责
- 模板模式
6. 无环依赖原则
- 包、服务之间的依赖结构必须是一个直接的无环图形,不能出现循环依赖
- 打破循环依赖关系,解决关系耦合问题:
- 使用依赖倒置原则和接口隔离原则
- 创建新的包,将共同类抽象出来放在新的包里
7. 减少抛异常
- 除了那些必须告诉用户的错误,其他错误尽量在软件内部处理掉,不要抛出
总结
- 如何去遵守这些原则。对这些原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。