文章目录
一、设计模式概述
1、设计模式概述
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
2、设计模式分类
设计模式主要分为创建型模式、结构型模式、行为型模式,另外还有一些其他的类型。
(1)创建型模式
- 工厂模式(Factory Pattern)
- 抽象工厂模式(Abstract Factory Pattern)
- 单例模式(Singleton Pattern)
- 建造者模式(Builder Pattern)
- 原型模式(Prototype Pattern)
(2)结构型模式
- 适配器模式(Adapter Pattern)
- 桥接模式(Bridge Pattern)
- 过滤器模式(Filter、Criteria Pattern)
- 组合模式(Composite Pattern)
- 装饰器模式(Decorator Pattern)
- 外观模式(Facade Pattern)
- 享元模式(Flyweight Pattern)
- 代理模式(Proxy Pattern)
(3)行为型模式
- 责任链模式(Chain of Responsibility Pattern)
- 命令模式(Command Pattern)
- 解释器模式(Interpreter Pattern)
- 迭代器模式(Iterator Pattern)
- 中介者模式(Mediator Pattern)
- 备忘录模式(Memento Pattern)
- 观察者模式(Observer Pattern)
- 状态模式(State Pattern)
- 空对象模式(Null Object Pattern)
- 策略模式(Strategy Pattern)
- 模板模式(Template Pattern)
- 访问者模式(Visitor Pattern)
(4)J2EE 模式
- MVC 模式(MVC Pattern)
- 业务代表模式(Business Delegate Pattern)
- 组合实体模式(Composite Entity Pattern)
- 数据访问对象模式(Data Access Object Pattern)
- 前端控制器模式(Front Controller Pattern)
- 拦截过滤器模式(Intercepting Filter Pattern)
- 服务定位器模式(Service Locator Pattern)
- 传输对象模式(Transfer Object Pattern)
(5)其他
- 并发型模式
- 线程池模式
二、设计模式的七大原则
1、设计模式的七大原则
(1)单一职责原则(Single Responsibility Principle,SRP)
对类来说,一个类应该只负责一个职责,如果A类负责两个职责:职责1和职责2。当职责1因需求变更而改变类A时,可能会对职责2造成影响导致错误,所以需要将类A的粒度分解为A1,A2。
(2)接口隔离原则(Interface Segregation Principle,ISP)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。
(3)依赖倒转原则(Dependence Inversion Principle,DIP)
这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
- 依赖倒转(倒置)的中心思想是面向接口编程
- 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java中,抽象指的是接口或抽象类,细节就是具体的实现类
- 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
(4)里氏代换原则(Liskov Substitution Principle,LSP)
里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
(5)开闭原则(Open Close Principle,OCP)
开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。想要达到这样的效果,我们需要使用接口和抽象类,用抽象构建框架,用实现扩展细节。当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
(6)迪米特法则,又称最少知道原则(Demeter Principle,DP)
迪米特法则又叫最少知道原则,是指一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
(7)合成复用原则(Composite Reuse Principle,CRP)
合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。
2、设计原则详解
(1)单一职责原则
如以下代码,有一个类SRP1,类中有一个方法eat()
public class SRP1 {
public void eat(String param){
System.out.println(param + "是吃饭的工具");
}
}
下面是main方法类
public class SingleResponsibilityPrinciple {
public static void main(String[] args){
SRP1 srp1 = new SRP1();
srp1.eat("筷子");
srp1.eat("铁锅");
srp1.eat("洗洁精");
}
}
当我们在启动类中调用SRP1类中的方法时,得到如下结果
筷子是吃饭的工具
铁锅是吃饭的工具
洗洁精是吃饭的工具
这个结果与生活中的实际情况有所出入,这是因为我们在调用方法的时候,只有一个SRP1类,这个类里面也只有一个方法eat(),也就是说这个示例违反的单一职责原则,而且如果后期因为需求更改而对SRP1类做了修改,会影响整个调用这个类中的方法的代码。
为了避免违反单一职责原则,我们可以将代码做如下的改动,这次设计三个类SRP2_1,SRP2_2,SRP2_3,每个类里面分别有一个方法,具体代码如下:
类SRP2_1
public class SRP2_1 {
public void eat(String param){
System.out.println(param + "是吃饭的工具");
}
}
类SRP2_2
public class SRP2_2 {
public void make(String param){
System.out.println(param + "是做饭的工具");
}
}
类SRP2_3
public class SRP2_3 {
public void wash(String param){
System.out.println(param + "是洗碗的工具");
}
}
然后我们再次在main方法类中调用上述三各类中的方法
public class SingleResponsibilityPrinciple {
public static void main(String[] args){
SRP2_1 srp2_1 = new SRP2_1();
srp2_1.eat("筷子");
SRP2_2 srp2_2 = new SRP2_2();
srp2_2.make("铁锅");
SRP2_3 srp2_3 = new SRP2_3();
srp2_3.wash("洗洁精");
}
}
得到结果如下
筷子是吃饭的工具
铁锅是做饭的工具
洗洁精是洗碗的工具
上述结果与生活实际情况相同,而且代码也遵守了单一职责原则,即一个类只负责一种职责,如SRP2_1 类负责说明eat()职责,SRP2_2类负责说明make()职责,SRP2_3负责说明wash()职责,这样不仅实现了单一职责原则,而且当某个需求更改而修改了代码时,只会对相对应的代码有影响,而不会对其他的功能造成影响。
但是上述的代码在实际运行时,消耗的资源会比较大,因为每次调用都会创造一个新的对象,既占用存储,又消耗资源。于是,我们可以对其进行优化,具体代码如下:
public class SRP3 {
public void eat(String param){
System.out.println(param + "是吃饭的工具");
}
public void make(String param){
System.out.println(param + "是做饭的工具");
}
public void wash(String param){
System.out.println(param + "是洗碗的工具");
}
}
在一个类SRP3中有设置三个方法,分别执行不同的职责。下面时main方法类
public class SingleResponsibilityPrinciple {
public static void main(String[] args){
SRP3 srp3 = new SRP3();
srp3.eat("筷子");
srp3.make("铁锅");
srp3.wash("洗洁精");
}
}
运行的结果如下:
筷子是吃饭的工具
铁锅是做饭的工具
洗洁精是洗碗的工具
这样就从方法的层面实现了单一职责原则,一个方法只负责一种职责,相比于一个类负责一种职责,这种实现方式更节约资源,有利于系统的运行。
单一职责原则的优点和注意事项:
- 降低类的复杂度,一个类只负责一项职责;
- 提高类的可读性,可维护性;
- 降低变更引起的风险;
- 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则,只有类中方法数量足够少,可以在方法级别保持单一职责原则
(2)接口隔离原则
有如下接口类,接口中有三个方法,分别为method1()、method2()和method3()
public interface interface1 {
void method1();
void method2();
void method3();
}
有两个类分别实现这个接口
ISP1 类
public class ISP1 implements interface1 {
@Override
public void method1() {
System.out.println("ISP111实现接口中interface1的方法method1");
}
@Override
public void method2() {
System.out.println("ISP111实现接口中interface1的方法method2");
}
@Override
public void method3() {
System.out.println("ISP111实现接口中interface1的方法method3");
}
}
ISP2类
public class ISP2 implements interface1 {
@Override
public void method1() {
System.out.println("ISP222实现接口中interface1的方法method1");
}
@Override
public void method2() {
System.out.println("ISP222实现接口中interface1的方法method2");
}
@Override
public void method3() {
System.out.println("ISP222实现接口中interface1的方法method3");
}
}
在main方法类中调用上述方法如下
public class InterfaceSegregationPrinciple {
public static void main(String[] args) {
ISP1 isp1 = new ISP1();
isp1.method1();
isp1.method2();
ISP2 isp2 = new ISP2();
isp2.method1();
isp2.method3();
}
}
输出接口如下
ISP111实现接口中interface1的方法method1
ISP111实现接口中interface1的方法method2
ISP222实现接口中interface1的方法method1
ISP222实现接口中interface1的方法method3
可以看到,在main方法类中,我们分别创造了ISP1类和ISP2类的对象,并且调用了其类中的两个方法。这里需要注意的是,创造的两个对象在调用方法的时候都没有调用全部三个方法,而是只用到了其中的两个方法,但是在ISP1类和ISP2类中,继承了接口interface,都重写了接口中的三个方法。
尽管调用没有用到全部方法,但实现接口的时候会实现全部接口,这就违背了接口隔离原则。我们需要的是使用到那些接口中的方法,就实现对应的接口。对此,我们可以对代码做如下的改动,将那一个接口类拆分为三个接口类,分别如下:
interface2_1
public interface interface2_1 {
void method1();
}
interface2_2
public interface interface2_2 {
void method2();
}
interface2_3
public interface interface2_3 {
void method3();
}
然后分别创建类ISP3和ISP4实现对应接口,ISP实现接口interface2_1和接口interface2_2,ISP4实现接口interface2_1和接口interface2_3,代码如下:
ISP3类
public class ISP3 implements interface2_1,interface2_2 {
@Override
public void method1() {
System.out.println("ISP333实现接口中interface2_1的方法method1");
}
@Override
public void method2() {
System.out.println("ISP333实现接口中interface2_2的方法method2");
}
}
ISP4类
public class ISP4 implements interface2_1,interface2_3 {
@Override
public void method1() {
System.out.println("ISP444实现接口中interface2_1的方法method1");
}
@Override
public void method3() {
System.out.println("ISP444实现接口中interface2_3的方法method3");
}
}
main方法代码如下
public class InterfaceSegregationPrinciple {
public static void main(String[] args) {
ISP3 isp3 = new ISP3();
isp3.method1();
isp3.method2();
ISP4 isp4 = new ISP4();
isp4.method1();
isp4.method3();
}
}
输出结果如下:
ISP333实现接口中interface2_1的方法method1
ISP333实现接口中interface2_2的方法method2
ISP444实现接口中interface2_1的方法method1
ISP444实现接口中interface2_3的方法method3
从上面的代码可以得到,将一个接口拆分成三个接口后,每个接口里只有一个方法,所以在调用方法的时候,可以避免没有调用全部方法却实现了接口中的全部方法的操作,如ISP3类只需要方法method1()和method2(),所以只实现了接口interface2_1和接口interface2_2,也只重写了方法method1()和method2()。
(3)依赖倒转原则
首先看下下面没有使用依赖倒转原则的示例。
创建一个Email类,其含有一个方法getInfo()。
public class Email {
public String getInfo(){
return "Email的信息是:你好!!";
}
}
创建一个Person类,Person类依赖Email类。
public class Person {
public void getEmail(Email email){
System.out.println(email.getInfo());
}
}
main方法类启动程序
public class DependenceInversionPrinciple {
public static void main(String[] args) {
Person person = new Person();
person.getEmail(new Email());
}
}
输出结果为
Email的信息是:你好!!
上面的代码虽然成功的输出了结果,但是具有局限性,比如此时Person类要以短信的方式接收消息,就需要在创建一个短信类Message,而且还需要在Person类下创建一个新的用于接收短信信息的方法getMessage(),这样就会让代码显得冗余复杂,而且效率低。
于是我们可以采用依赖倒置原则进行优化,具体操作如下:
创建一个接口类
public interface Receive {
public String getInfo();
}
将Email类实现这个Receive接口类
public class Email implements Receive{
@Override
public String getInfo() {
return "Email的信息是:你好!!";
}
}
Person类也做响应的修改
public class Person {
public void getReceive(Receive receive){
System.out.println(receive.getInfo());
}
}
main方法类不变,输出结果是一致的。这样做的好处是,如果增加了新的接收信息的方式,只需要再新建一个该接收方式的类去实现那个接口就可以了,如信息短信接收方式,则创建一个Message类实现Receive接口
public class Message implements Receive{
@Override
public String getInfo() {
return "Message的信息是:充话费了!!";
}
}
在main方法中调用如下
public class DependenceInversionPrinciple {
public static void main(String[] args) {
Person person = new Person();
//邮件方式
person.getReceive(new Email());
//短信方式
person.getReceive(new Message());
}
}
得到结果
Email的信息是:你好!!
Message的信息是:充话费了!!
使用依赖倒转原则的优点和注意事项:
- 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好;
- 变量的声明类型尽量是抽象类或接口,这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化;
- 继承时遵循里氏替换原则。
(4)里氏替换原则
先看一下不遵循里氏替换原则的示例
新建一个CountA 类,其含有一个方法funA();
public class CountA {
public int funA(int a, int b){
return a - b;
}
}
再新建一个CountB类继承CountA类,重写了CountA类的funA()方法,并对其进行了修改,另外其还有自己的方法funB(),在funB()中调用了funA()方法
public class CountB extends CountA{
public int funA(int a, int b){
return a + b;
}
public int funB(int a, int b){
return funA(a, b);
}
}
main方法类中的调用代码
public class LiskovSubstitutionPrinciple {
public static void main(String[] args) {
CountA countA = new CountA();
System.out.println("10 - 5 = " + countA.funA(10,5));
CountB countB = new CountB();
System.out.println("10 - 5 = " + countB.funA(10,5));
System.out.println("10 - 5 = " + countB.funB(10,5));
}
}
输出结果为
10 - 5 = 5
10 - 5 = 15
10 - 5 = 15
从结果可以看出,main方法类本意仍是想通过CountB类调用CountA类中的方法,但是当用CountB类的对象调用A类的方法时出现了错误,这是因为在CountB类重写父类CountA类的方法funA()时对其进行了修改,这样在CountB类中调用时就会出现错误,如果CountB类不重写CountA类中的方法或者重写了但没有对其进行修改,就不会出现上面的情况,如
public class CountB extends CountA{
// public int funA(int a, int b){
// return a + b;
// }
public int funB(int a, int b){
return funA(a, b);
}
}
执行结果为
10 - 5 = 5
10 - 5 = 5
10 - 5 = 5
处理上述解决办法外,我们还可以利用下面的方法解决
新建一个基础类Base类
public class Base {
}
将CountA类和CountB类分别继承Base类
CountA类
public class CountA extends Base{
public int funA(int a, int b){
return a - b;
}
}
CountB类
public class CountB extends Base{
private CountA countA = new CountA();
//funA变为CountB自己的方法
public int funA(int a, int b){
return a + b;
}
public int funB(int a, int b){
return countA.funA(a, b);
}
}
main方法类
public class LiskovSubstitutionPrinciple {
public static void main(String[] args) {
CountA countA = new CountA();
System.out.println("10 - 5 = " + countA.funA(10,5));
CountB countB = new CountB();
System.out.println("10 + 5 = " + countB.funA(10,5));
System.out.println("10 - 5 = " + countB.funB(10,5));
}
}
输出结果
10 - 5 = 5
10 + 5 = 15
10 - 5 = 5
(5)开闭原则
先看一个不遵循开闭原则的示例
创建一个积累Shape类
public class Shape {
int type;
}
创建两个类继承Shape类
ShapeA类
public class ShapeA extends Shape{
ShapeA(){
super.type = 1;
}
}
ShapeB 类
public class ShapeB extends Shape{
ShapeB(){
super.type = 2;
}
}
创建一个DrawShape类
public class DrawShape {
public void draw(Shape shape){
if (shape.type == 1){
drawA(shape);
}else if (shape.type == 2){
drawB(shape);
}
}
public void drawA(Shape shape){
System.out.println("这是图形AAAA");
}
public void drawB(Shape shape){
System.out.println("这是图形BBBB");
}
}
在main方法中调用
public class OpenClosePrinciple {
public static void main(String[] args) {
DrawShape drawShape = new DrawShape();
drawShape.draw(new ShapeA());
drawShape.draw(new ShapeB());
}
}
输出结果为
这是图形AAAA
这是图形BBBB
上面的代码,虽然也能实现功能,但是如果需要进行功能拓展时就比较麻烦,不仅要增加相应的功能类,而且还需要在DrawShape类中修改好几处对应的代码,如果代码非常多的话修改起来就很麻烦,并且整个代码看起来就非常的冗杂。
针对上面的代码,我们可以结合开闭原则进行优化,具体代码如下
将基类Shape修改为抽象类,并且创建一个抽象方法
abstract class Shape {
int type;
public abstract void draw();
}
ShapeA 类修改
public class ShapeA extends sjyz.kbyz.improve.Shape {
ShapeA(){
super.type = 1;
}
@Override
public void draw() {
System.out.println("这是图形AAAA");
}
}
ShapeB类修改
public class ShapeB extends sjyz.kbyz.improve.Shape {
ShapeB(){
super.type = 2;
}
@Override
public void draw() {
System.out.println("这是图形BBBB");
}
}
DrawShape 类修改
public class DrawShape {
public void drawShape(Shape shape){
shape.draw();
}
}
main方法类
public class OpenClosePrinciple {
public static void main(String[] args) {
DrawShape drawShape = new DrawShape();
drawShape.drawShape(new ShapeA());
drawShape.drawShape(new ShapeB());
}
}
上述操作的输出结果相同,但是在代码上比之前的要更优化更合理,如果需要扩展功能时,只需要新增一个对应的功能类继承基类即可。
(6)迪米特法则
使用迪米特法则注意事项:
- 迪米特法则的核心是降低类之间的耦合
- 但是注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有依赖关系