设计模式
前言
这段时间学习了关于JAVA设计模式的课程,看完视频后感觉虽然都有点印象,但是掌握的不深,所以整理一下前面的笔记和上网找了一下相关资料
设计模式:是一套被反复使用、多数人知晓的、经过分类编目的、 代码设计经验的总结。
设计模式用处:它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。
设计模式目的:提高代码的可重用性、代码的可读性和代码的可靠性。
一、单一职责原则
1、定义
不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。
2、 问题由来
类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。
3、单一职责优点:
降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
提高类的可读性。复杂性降低,自然其可读性会提高。 提高系统的可维护性。可读性提高,那自然更容易维护了。
变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响
例子:打电话:
下面展示一些 内联代码片
。
public interface IPhone{
//拨通电话
public void dial(String phoneNumber);
//通话
public void chat(Object o);
//通话完毕,挂断电话
public void hangup();
}
打电话分成了:1.协议管理,2、数据传送。
//public interface IPhone{
}
public interface IConnectionManager extends IPhone{ //拨通电话
public void dial(String phoneNumber);
//通话完毕,挂断电话
public void hangup();
}
public interface IDataTransfer extends IPhone{
//通话
public void chat(IConnectionManager con);
}
二、里式替换原则
1.定义:
(1).如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
(2).所有引用基类的地方必须能透明地使用其子类的对象。
2.含义:
1.子类必须完全实现父类的方法。
2. 子类中可以增加自己特有的方法。
3. 当子类覆盖或实现父类的方法时,方法的输入参数(方法的形参)要比父类方法的输入参数更宽松。
4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
3.优缺点:
优点:
- 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
- 提高代码的重用性;
- 提高代码的可扩展性,子类可形似于父类,但异于父类,保留了自己独特的个性;其实很多开源框架的扩展都 是通过继承父类实现的。
- .提供产品或者项目的开放性。
缺点:
- 继承是侵入性的,只要继承就必须拥有父类的所有方法和属性;
- 降低了代码的灵活性。子类必须拥有父类的属性和方法,让子类中多了约束
- 增加了耦合,当父类的常量、变量或者方法被修改了,需要考虑子类的修改,所以一旦父类有了变动,很可能 会造成非常糟糕的结果,要重构大量的代码。
4.注意事项
当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。
继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。
例子:
这是个简单的减法运算
class A{
public int func1(int a, int b){
return a-b;
}
}
public class Client{
public static void main(String[] args){
A a = new A();
System.out.println("100-50="+a.func1(100, 50));
System.out.println("100-80="+a.func1(100, 80));
}
}
需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。即类B需要完成两个功能
class B extends A{
public int func1(int a, int b){
return a+b;
}
public int func2(int a, int b){
return func1(a,b)+100;
}
}
public class Client{
public static void main(String[] args){
B b = new B();
System.out.println("100-50="+b.func1(100, 50));
System.out.println("100-80="+b.func1(100, 80));
System.out.println("100+20+100="+b.func2(100, 20));
}
}
类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。
三、依赖倒置原则
1.定义
高层模块不应该依赖低层模块,两者都应该依赖其抽象; 抽象不应该依赖细节, 细节应该依赖抽象。
个人理解:
类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
解决:
将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。
2.作用
依赖倒置原则可以降低类间的耦合性。
依赖倒置原则可以提高系统的稳定性。
依赖倒置原则可以减少并行开发引起的风险。 依赖倒置原则可以提高代码的可读性和可维护性。
3.实现方法
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 尽量不要覆写基类的方法
- 使用继承时结合里氏替换原则。
核心思想
面向接口编程,而不是面向实现
例子:母亲给孩子讲故事
class Book{
public String getContent(){
return "很久很久以前有一个阿拉伯的故事……";
}
}
class Mother{
public void narrate(Book book){
System.out.println("妈妈开始讲故事");
System.out.println(book.getContent());
}
}
public class Client{
public static void main(String[] args){
Mother mother = new Mother();
mother.narrate(new Book());
}
}
这时候如果不想读书,而是读报纸的话
class Newspaper{
public String getContent(){
return "林书豪38+7领导尼克斯击败湖人……";
}
}
必须要修改Mother才能读。
class Mother{
public void narrate(Book book){
System.out.println("妈妈开始讲故事");
System.out.println(Newspaper.getContent());
}
}
原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。
引入一个抽象的接口IReader
interface IReader{
public String getContent();
}
Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,他们各自都去实现IReader接口
class Newspaper implements IReader {
public String getContent(){
return "林书豪17+9助尼克斯击败老鹰……";
}
}
class Book implements IReader{
public String getContent(){
return "很久很久以前有一个阿拉伯的故事……";
}
}
class Mother{
public void narrate(IReader reader){
System.out.println("妈妈开始讲故事");
System.out.println(reader.getContent());
}
}
public class Client{
public static void main(String[] args){
Mother mother = new Mother();
mother.narrate(new Book());
mother.narrate(new Newspaper());
}
}
四、接口隔离原则
1.定义
客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
2.实现方法:
(1)接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
(2)为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
(3)了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业 务逻辑。
(4)提高内聚,减少对外交互。
(5)使接口用最少的方法去完成最多的事情。
3.接口隔离原则和单一职责的区别:
(1).单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
(2).单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象 和程序整体框架的构建。
4.接口隔离原则的优点
- 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
- 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使 设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
- 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
- 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫 设计冗余的代码。
例子:对美女的定义
建立一个接口:美女
实现美女接口
给个名字
提要求:
展示美女
这就把美女的东西展示出来了
但是要知道,人类的审美时时刻刻在变化
这时候就发现我们的接口太死了,有一些有缺点但是也好看的
这时候就要衍生新的接口了
对于这个美女,虽然没有气质,但是外貌和脸身材都非常棒,所以也是个美女
实现他们
五、迪米特法则
1.定义
又叫作最少知识原则,它要求一个对象应该对其他对象有最少的了解。通俗的说,一个类应该对自己需要耦合或调用的 类知道的最少,被耦合或调用的类的内部是如何复杂都与我无关,我就知道你提供的public方法就好。
2.实现方法
- 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
- 在类的结构设计上,尽量降低类成员的访问权限。 3. 在类的设计上,优先考虑将一个类设置成不变类。
- 在对其他类的引用上,将引用其他对象的次数降到最低。
- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
- 谨慎使用序列化(Serializable)功能。
3.优点
(1)降低了类之间的耦合度,提高了模块的相对独立性。
(2)由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
4.缺点:
过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。
六、开闭原则
1.定义
一个软件实体应 该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。开闭原则是面向对象程序设计的终极目标,它使软件实体拥有一定的适应性和灵活性的同时具备稳定性和延续性。
2.作用
- 对软件测试的影响 软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。
- 可以提高代码的可复用性粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。
- 可以提高软件的可维护性遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。
3.认识:
遵循设计模式前面5大原则,以及使用23种设计模式的目的就是遵循开闭原则。
总结
1.单一职责原则告诉我们实现类要职责单一;
2.里氏替换原则告诉我们不要破坏继承体系;
3.依赖倒置原则告诉我们要面向接口编程;
4.接口隔离原则告诉我们在设计接口的时候要精简单一;
5.迪米特法则告诉我们要降低耦合。
开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。
对这六个原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。
上述部分代码参考自:链接: link.