读书笔记----软件设计原则、设计模式
这个作业属于哪个课程 | 2021软件代码开发技术 |
---|---|
这个作业要求在哪里 | 读书笔记----软件设计原则、设计模式 |
这个作业的目标 | 阅读并归纳总结关于软件设计模式、设计原则的相关书籍,并结合曾经的开发经历进行理解 |
一、参考文献
- [1]郑阿奇.软件秘笈:设计模式那点事[M].北京:电子工业出版社,2011
- [2](美)Schach,S.R. 著;邓迎春等译.软件工程:面向对象和传统的方法(第8版)[M].北京:机械工业出版社,2011
二、归纳总结
1. 设计模式
设计模式是一套反复使用、多人知晓、经过分类编目的代码设计经验的总结。使用设计模式的目的是提高代码的可重用性,让代码更容易被他人理解,使系统质量更加保证,系统更加可靠。
1.1 设计模式的种类
设计模式可分为三类:创建类模式、结构类模式和动作类模式。
- 创建类设计模式:通过创建对象来解决设计问题。
- 结构类设计模式:通过确定实体间关系的简单方式来解决设计问题。
- 动作类设计模式:通过确定对象间通用交互模式来解决设计问题。
1.1.1 创建类模式
模式名称 | 说明 |
---|---|
Abstract factory(抽象工厂) | 创建几个类家庭的一个实例 |
Builder(创建者) | 允许相同结构的过程创建不同的实现 |
Factory method(工厂方法) | 创建几个可能的衍生类和一个实例 |
Prototype(原型) | 要被克隆的一个类 |
Singleton(单元素集合) | 限制一个类的实例化只能为一个实例 |
1.1.2 结构类模式
模式名称 | 说明 |
---|---|
Adapter(适配器) | 适配不同类的接口 |
Bridge(桥) | 从实现中分离出抽象 |
Composite(合成) | 相似类的合成类 |
Decorator(装饰者) | 允许给类动态地添加额外的特性 |
Facade(正面) | 提供简化接口的单类 |
Flyweight(轻量级) | 使用共享机制来有效地支持大量细粒度类 |
Proxy(代理) | 起到接口功能的类 |
1.1.3 动作类模式
模式名称 | 说明 |
---|---|
Chain-of-responsibility(责任链) | 通过类链来处理申请的方式 |
Command(命令) | 在类中封装一个行为 |
Interpreter(编译器) | 应用特定语言元素的方式 |
Mediator(中介) | 为一组接口提供统一的接口 |
Iterator(迭代器) | 频繁地访问集合元素 |
Memento | 捕获并存储对象的内部状态 |
Observer(观察者) | 允许在运行时观察对象的状态 |
State(状态) | 允许在运行时部分改变对象的类型 |
Strategy(策略) | 允许在运行时动态选择算法 |
Template method(模板方法) | 将算法的实现延迟到子类中 |
Visitor(访问者) | 不改变类的情况下给类添加新的操作 |
1.2 设计模式优缺点
设计模式有很多优点:
- 设计模式通过解决通常的设计问题来促进重用。可以通过仔细协调那些可进一步加强重用的特性(例如继承性)来提高设计模式的重用性。
- 设计模式提供高层次的设计文档,因为这些模式把设计抽象列为条件。
- 许多设计模式的实现已经存在,在这些情况中,没必要对程序中已实现设计模式的那部分进行编码和写文档。(当然程序这些部分的测试仍是很重要的。)
- 如果维护程序员对设计模式很熟悉,将更容易领会加入了设计模式的程序,即使他以前从没有看到过该特定的程序。
- 对自动检测设计模式的研究已经开始收获成果。
然而设计模式也有许多缺点:
- 在软件产品中使用23个标准设计模式可能意味者我们正使用的语言不够强大。
- 主要问题是还没有系统化的方式确定应用设计模式的时机和方法
- 要想从设计模式中获取最大利益,需要使用多个互相关联的模式
- 对使用传统范型构建的软件产品进行维护时,改进类和对象基本不可能。类似地,对已有的软件产品改进模式,无论它是传统的还是面向对象的,也是基本不可能。
2.设计原则
2.1 单一职责原则SRP
任何一个软件模块中,应该有且只有一个被修改的原因
这个原则能让类的职责更单一。这样的话,每个类只需要负责自己的那部分,类的复杂度就会降低。如果职责划分得很清楚,那么代码维护起来也更加容易。
2.2 开闭原则OCP
软件实体应该对扩展开放,对修改关闭
如果在项目中添加一个功能的时候,可以直接对代码进行扩展;如果要修改某一部分的功能时应该做的是,尽量少做修改,但是修改的时候,要保留原来的功能,只是在上面扩展出新的功能,就像版本更新一样,更新后,依然支持旧版本。
2.3 里氏替换原则LSP
程序中的父类型都应该可以正确地被子类型替代
程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替代的,即子类应该可以替换任何基类能够出现的地方,并且经过替换后,代码还能正常工作。
2.4 接口隔离原则ISP
多个特定客户端接口要好于一个宽泛用途的接口。
该原则主要强调两点:
- 接口尽量小。
- 接口要高内聚
2.5 依赖倒置原则DIP
模块之间交互应该依赖抽象,而非实现。
2.6 迪米特法则LKP
一个类对于其他类知道的越少越好
三、心得体会
里氏替换原则的定义如下:
Functions that use use pointers or references to base classes must be able to use objects of derived classes without knowing it.
翻译过来就是,所有引用基类的地方必须能透明地使用其子类的对象。
里氏替换原则的意思是,所有基类在的地方,都可以换成子类,程序还可以正常运行。这个原则是与面向对象语言的 继承 特性密切相关的。
这是为什么呢?由于面向对象语言的继承特性,子类拥有父类的所有方法,因此,将基类替换成具体的子类,子类也可以调用父类中的方法(其实是它自己的方法,继承于父类),但是如果要保证完全可以调用,光名称相同不行,还需要满足下面两个条件:
子类中的方法的前置条件必须与超类中被覆写的方法的前置条件相同或更宽松。
子类中的方法的后置条件必须与超类中被覆写的方法的后置条件相同或更严格。
这样的话,调用就没有问题了。否则,我在父类中传入一个 List 类型的参数,子类中重写的方法参数却变为 ArrayList ,那客户端使用的时候传入一个 LinkedList 类型的参数,使用父类的时候程序正常运行,但根据 LSP 原则,替换成子类后程序就会出现问题。同理,后置条件也是如此。
是不是很好理解?
但是这个原则并不是这么简单的,语法上是肯定不能有任何问题。但是,同时,功能上也要和替换前相同。
那么这就有些困难了,因为子类重写父类中的方法天经地义,子类中的方法不同于父类中的方法也是很正常的,但是,如果这么随便重写父类中的方法的话,那么肯定会违背 LSP 原则,可以看下面的例子:
首先有一个父类,水果类:
public class Fruit {
void introduce() {
System.out.println("我是水果父类...");
}
}
其次,它有一个子类,苹果类:
public class Apple extends Fruit {
@Override
void introduce() {
System.out.println("我是水果子类——苹果");
}
}
客户端代码如下:
public static void main(String[] args) {
Fruit fruit = new Fruit();
HashMap map = new HashMap<>();
fruit.introduce();
}
运行结果:
我是水果父类...
那么,如果按照 LSP 原则,所有父类出现的地方都能换成其子类,代码如下:
public static void main(String[] args) {
Apple fruit = new Apple();
HashMap map = new HashMap<>();
fruit.introduce();
}
那么运行结果就会变成:
我是水果子类——苹果
与原来的输出不同,程序的功能被改变了,违背了 LSP 原则。
因此,可以看到, LSP 原则最重要的一点就是:避免子类重写父类中已经实现的方法。这就是 LSP 原则的本质。这里由于 Fruit 父类已经实现了 introduce 方法,因此子类应该避免再对其进行重写,如果需要增加个性化,就应该对父类进行扩展,而不是重写,否则也会违背开闭原则。
一般来讲,程序中父类大多是抽象类,因为父类只是一个框架,具体功能还需要子类来实现。因此很少直接去 new 一个父类。而如果出现这种情况,那么就说明父类中实现的代码已经很好了,子类只需要对其进行扩展就会,尽量避免对其已经实现的方法再去重写。