JAVA 设计模式(一)—— 设计模式简介

一、设计模式概述

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)迪米特法则

使用迪米特法则注意事项:

  • 迪米特法则的核心是降低类之间的耦合
  • 但是注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有依赖关系
发布了75 篇原创文章 · 获赞 10 · 访问量 2907

猜你喜欢

转载自blog.csdn.net/baidu_27414099/article/details/104420744