设计模式

设计模式

一,设计模式概述

​ 设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。 使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

​ 《Design Patterns: Elements of Reusable Object-Oriented Software》(即后述《设计模式》一书),由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为"四人组(Gang of Four)",而这本书也就被称为"四人组(或 GoF)"书。
在《设计模式》这本书的最大部分是一个目录,该目录列举并描述了 23 种设计模式。

java的设计模式大体上分为三大类:

创建型模式(5种):工厂方法模式,抽象工厂模式,单例模式,建造者模式,原型模式。

结构型模式(7种):适配器模式,装饰器模式,代理模式,外观模式,桥接模式,组合模式,享元模式。

行为型模式(11种):策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

使用设计模式的好处:

  1. 使程序设计更加标准化,提高软件开发效率。
  2. 提升程序员的思维能力,编程能力,设计能力。
  3. 增强代码的可重用性,可读性,灵活性,可靠性,可维护性等。

总原则:开闭原则

开闭原则:就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。

​ 这样的设计,能够面对需求改变却可以保持相对稳定,从而使系统在第一个版本以后不断推出新的版本;面对需求,对程序的改动是通过增加新的代码进行的,而不是更改现有的代码; 所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类等。

1、单一职责原则 定义:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。 问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。 解决方案:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。

2、里氏替换原则 里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。 里氏替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。

3、依赖倒置原则 定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。 问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。 解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。

4、接口隔离原则 这个原则的意思是:每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度

5、迪米特法则(最少知道原则) 高内聚低耦合 就是说:一个类对自己依赖的类知道的越少越好。也就是说无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。 最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。

6、合成复用原则 尽量使用合成/聚合.避免继承.在新类中应该尽量使用关联关系采用现有的对象,使之成为新对象的一部分.达到现有功能复用的目的. 通过合成聚合的原则可以降低类于类之间的依赖关系.被依赖的类的修改对其他类的影响相对小一些. 合成/聚合原则是动态的.可以自由选择使用现有类的那些方法.而继承是静态的,失去了灵活性.如果父类改变有可能会影响子类的修改,同时破坏了父类的封装性,父类将会暴露不相关的方法给子类.

二,单例模式(Singleton Pattern)

类加载顺序:类加载(classLoader)机制一般遵从下面的加载顺序

如果类还没有被加载:

  1. 先执行父类的静态代码块和静态变量初始化,静态代码块和静态变量的执行顺序跟代码中出现的顺序有关。

  2. 执行子类的静态代码块和静态变量初始化。

  3. 执行父类的实例变量初始化

  4. 执行父类的构造函数

  5. 执行子类的实例变量初始化

  6. 执行子类的构造函数

    同时,加载类的过程是线程私有的,别的线程无法进入。

如果类已经被加载:

静态代码块和静态变量不在重复执行,再创建类对象时,只执行与实例相关的变量初始化和构造方法。

概述:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

何时使用:当系统需要某个类只有一个实例的时候。

2.1 饿汉模式创建单例

概述:在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快。

public class Singleton{
    private static Singleton s = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return s;
    }    
}

缺点:类加载时就实例化了,没有达到Lazy Loading (懒加载) 的效果,如果该实例没被使用,内存就浪费了。

2.2 懒汉模式创建单例

概述:在类加载时不初始化,等到第一次被使用时才初始化。

2.2.1 普通懒汉式(线程不安全,不可用)
public class Singleton{
	private static Singleton singleton;
    private Singleton(){};
    public static Singleton getInstance(){
        if(singleton==null){
            singleton = new Singleton();
        }
        return singleton;
    }
}
2.2.2 同步方法懒汉式
public class Singleton{

    private static Singleton singleton;
    private Singleton(){};
    //同步方法
    public static synchronized Singleton getInstance(){
        if(singleton==null){
            singleton = new Singleton();
        }
        return singleton;
    } 
}

​ 这种写法是对getInstance()加了锁的处理,保证了同一时刻只能有一个线程访问并获得实例,但是缺点也很明显,因为synchronized是修饰整个方法,每个线程访问都要进行同步,而其实这个方法只执行一次实例化代码就够了,每次都同步方法显然效率低下,为了改进这种写法,就有了下面的双重检查懒汉式

2.2.3 双重检查锁懒汉式
public class Singleton{
    private static volatile Singleton singleton;
    private Singleton(){};
    public static Singleton getInstance(){
        if(singleton==null){
            synchronized(Singleton.class){
                if(singleton==null){
                    singleton = new Singleton();
                } 
            }
        }
        return singleton;
    } 
}

​ 这种写法用了两个if判断,也就是Double-Check,并且同步的不是方法,而是代码块,效率较高。为什么要做两次判断呢?这是为了线程安全考虑,还是那个场景,对象还没实例化,两个线程A和B同时访问静态方法并同时运行到第一个if判断语句,这时线程A先进入同步代码块中实例化对象,结束之后线程B也进入同步代码块,如果没有第二个if判断语句,那么线程B也同样会执行实例化对象的操作了。

2.4 静态内部类创建单例
public class Singleton{
    private Singleton(){}
    private static class SingletonHolder{
        private static Singleton singleton = new Singleton();
    }
    public static Singleton getInstance(){
        return SingletonHolder.singleton;
    }
}

​ 这是很多开发者推荐的一种写法,这种静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成对象的实例化。

​ 同时,因为类的静态属性只会在第一次加载类的时候初始化,也就保证了SingletonInstance中的对象只会被实例化一次,并且这个过程也是线程安全的。

2.5 单例模式优点和缺点

优点:单例类只有一个实例,节省了内存资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能;单例模式可以在系统设置全局的访问点,优化和共享数据,例如前面说的Web应用的页面计数器就可以用单例模式实现计数值的保存。

缺点:单例模式一般没有接口,扩展的话除了修改代码基本上没有其他途径。

三,工厂模式

概述:工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

​ 举例需求:生产汽车。

3.1 简单工厂模式

​ 先看一下简单工厂模式,简单工厂模式也是一种工厂方法模式。简单工厂模式又称静态工厂方法模式。从命名上就可以看出这个模式一定很简单。它存在的目的很简单:定义一个用于创建对象的接口。如果一个一些对象(产品),已经确定了并不易改变和添加新的产品,那么久可以使用简单工厂模式。

Car接口:

/**
 * 所有车的父类
 */
public interface Car {
    void name();
}

宝马:

public class BaoMa implements Car{

    @Override
    public void name() {
        System.out.println("宝马车");
    }
}

奔驰:

public class BenChi implements Car{

    @Override
    public void name() {
        System.out.println("奔驰车");
    }
}

五菱之光:

public class WuLing implements Car{

    @Override
    public void name() {
        System.out.println("五菱之光");
    }
}

CarFactory工厂:

/**
 * 简单工厂模式
 */
public class CarFactory {

    public static Car getCar(String name){
        if(name.equals("宝马")){
            return new BaoMa();
        }else if(name.equals("奔驰")){
            return new BenChi();
        }else if(name.equals("五菱")){
            return new WuLing();
        }
        return null;
    }
}

测试类:

public class CarTest {

    public static void main(String[] args) {
        Car baoma = CarFactory.getCar("宝马");
        baoma.name();

        Car benchi = CarFactory.getCar("奔驰");
        benchi.name();

        Car wuling = CarFactory.getCar("五菱");
        wuling.name();
    }
}
3.2 工厂方法模式

​ 工厂方法模式去掉了简单工厂模式中工厂方法的静态属性,使得它可以被子类继承。这样在简单工厂模式里集中在工厂方法上的压力可以由工厂方法模式里不同的工厂子类来分担。

CarFactory接口:

public interface CarFactory {
    Car pro();//生产方法
}

每一种车都有属于自己独立的 工厂:要去实现 CarFactory接口:

BaoMaFactory:

/**
 * 有一个单独的宝马工厂 专门用来生产宝马车
 */
public class BaoMaFactory implements CarFactory{
    @Override
    public Car pro() {
        return new BaoMa();
    }
}

BenChiFactory:

public class BenChiFactory implements CarFactory{

    @Override
    public Car pro() {
        return new BenChi();
    }
}

如果想扩展新的车,我们不用改动老代码,直接添加我们需要的工厂以及工厂要生产的类 就可以了。

四,适配器模式

​ 概述:适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁

举个栗子1:当我们给手机充电时,由于手机充电口是5V,而插座提供的是220V交流电,因此我们通常需要使用充电器将220V交流电转换成可供手机充电用的5V直流电,这个充电器就是一个适配器。

举个栗子2:我们现在的轻薄本电脑没有提供网线接口,不能直接插网线,需要通过一个网线转接口来连接网线。完成上网的功能。

电脑,网线,网线转接口(适配器)

作用:适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。

三种适配器:类的适配器模式、对象的适配器模式、接口的适配器模式。

4.1 类适配器

类适配:创建新类,继承源类,并实现新接口。

接口:AdapterInterface

public interface AdapterInterface {
    void charuwangxian();//无线网卡
}

网线 源类:

public class WangXian {

    public void conn(){
        System.out.println("连接网线上网");
    }
}

适配器类:

/**
 * 实例化一个适配器对象
 * 实现适配器接口
 */
public class AdapterInterfaceImpl extends WangXian implements AdapterInterface{

    @Override
    public void charuwangxian() {
        //继承网线类,所以可以直接使用父类的方法
        super.conn();
    }
}

客户端 电脑类:

public class Computer {

    public static void main(String[] args) {
        /*WangXian wangXian = new WangXian();
        wangXian.conn();*/

        AdapterInterfaceImpl adapterInterface = new AdapterInterfaceImpl();
        adapterInterface.charuwangxian();
    }
}
4.2 对象适配器

对象适配:创建新类持源类的实例,并实现新接口。

4.3 接口适配器

接口适配:创建新的抽象类实现旧接口方法。

​ 如果我们的客户类 直接实现接口,就意味着 必须将接口中的方法全部实现,但是,接口的中方法不一定是我们都需要的,所以这样就会显的代码冗余。

怎么解决:

​ 我们自己定义一个抽象类(只作为父类 让子类继承),让抽象类实现接口,并且重写接口中的所有方法,但是不用写具体的实现。让我们的客户类,直接继承抽象类,这样我们可以选择接口中需要的方法,去实现,避免了实现全部方法的问题。

五,代理模式(Proxy Pattern)

概述:为其它对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

优点:代理模式可以屏蔽用户真正请求的对象,是用户程序和正在的对象解耦。使用代理来担当那些创建耗时的对象的替身。 一个用户不想或者不能够直接引用一个对象(或者设计者不希望用户直接访问该画图对象),而代理对象可以在客户端和目标对象之间起到中介的作用。而且这个代理对象中,我们可以做更多的操作。

代理分类:我们有多种不同的方式来实现代理。如果按照代理创建的时期来进行分类的话, 可以分为两种:静态代理、动态代理。静态代理是由程序员创建或特定工具自动生成源代码,在对其编译。动态代理是在程序运行时通过反射机制动态创建的。

5.1 静态代理

概述:静态代理就是写死了在代理对象中执行这个方法前后执行添加功能的形式,每次要在接口中添加一个新方法,则需要在目标对象中实现这个方法,并且在代理对象中实现相应的代理方法。

5.2 动态代理

概述:动态代理是在程序运行时通过反射机制动态创建的

jdk的动态代理:

cglib动态代理:

原理:Jdk的动态代理,是使用反射技术获得类的加载器并且创建实例,根据类执行的方法在执行方法的前后发送通知。在代理对象Proxy的新建代理实例方法中,必须要获得类的加载器、类所实现的接口、还有一个拦截方法的句柄。在句柄的invoke中如果不调用method.invoke则方法不会执行。在invoke前后添加通知,就是对原有类进行功能扩展了。创建好代理对象之后,proxy可以调用接口中定义的所有方法,因为它们实现了同一个接口,并且接口的方法实现类的加载器已经被反射框架获取到了。

六,观察者模式(Observer)

监听器 监视

概述:属于行为型模式 观察者模式很好理解,类似于邮件订阅和RSS订阅,当我们浏览一些博客或wiki时,经常会看到RSS图标,就这的意思是,当你订阅了该文章,如果后续有更新,会及时通知你。其实,简单来讲就一句话:当一个对象变化时,其它依赖该对象的对象都会收到通知,并且随着变化!对象之间是一种一对多的关系。

定义:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。

该模式包含四个角色

Ø 抽象被观察者角色:也就是一个抽象主题,它把所有对观察者对象的引用保存在一个集合中,每个主题都可以有任意数量的观察者。抽象主题提供一个接口,可以增加和删除观察者角色。一般用一个抽象类和接口来实现。

Ø 具体被观察者角色:也就是一个具体的主题,在集体主题的内部状态改变时,所有登记过的观察者发出通知。

Ø 抽象观察者角色:为所有的具体观察者定义一个接口,在得到主题通知时更新自己。

Ø 具体观察者角色:实现抽象观察者角色所需要的更新接口,观察到被观察者的操作。

案例:公众号推送消息。

观察者模式优缺点:

优点:

  1. 观察者和被观察者是抽象耦合的。
  2. 建立一套触发机制。

缺点:

  1. 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
  2. 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
  3. 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

使用场景:

​ Ø 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。

​ Ø 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。 Ø 一个对象必须通知其他对象,而并不知道这些对象是谁。

七,策略模式(strategy)

概述:策略设计模式把"算法"(也就是软件应用系统中的业务规则或者待实现的功能等)和"环境"(封装软件应用系统在实际应用时的场景)相互分离,其中的"环境"程序类主要是负责维护和查询"算法"程序类,而各种"算法"则由具体的策略程序类加以封装和实现,并且策略程序类可以通过面向对象设计方法中的继承机制来产生出层次性的策略程序类,可以根据应用的需要进行替换。

案例:淘宝促销活动,jdbc数据库连接等。
原来的代码:

if(msg.equals("6.18")){

​	System.out.println("6.18促销活动");

}else  if(msg.equals("11.11")){

​	System.out.println("11.11促销活动");

}else  if(msg.equals("12.12")){

​	System.out.println("12.12促销活动");

}

不好:不符合开闭原则。不利于后期的维护。

使用策略模式解决:让每一个算法单独成类。通过环境对象来控制具体调用的算法。

策略模式优缺点:

优点:

​ 1、算法可以自由切换(策略类自由切换)。

​ 2、避免使用多重条件判断。

​ 3、扩展性良好(符合开闭原则)。

缺点:

​ 1、策略类会增多。

​ 2、所有策略类都需要对外暴露。

​ 3、客户端必须知道所有的策略类,才能确定要调用的策略类。

策略模式使用场景:

  1. 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
  2. 一个系统需要动态地在几种算法中选择一种。
  3. 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。

八,模板方法模式(template)

概述:在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但**调用将以抽象类中定义的方式进行。**这种类型的设计模式属于行为型模式。

意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

主要解决:一些方法通用,却在每一个子类都重新写了这一方法。

何时使用:有一些通用的方法。

如何解决:将这些通用算法抽象出来。

关键代码:在抽象类实现,其他步骤在子类实现。

应用实例: 1:、在造房子的时候,地基、走线、水管都一样,只有在建筑的后期才有加壁橱加栅栏等差异。 2、西游记里面菩萨定好的 81 难,这就是一个顶层的逻辑骨架。程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。

炒菜案例:番茄炒蛋

优点:

​ 1、封装不变部分,扩展可变部分。

​ 2、提取公共代码,便于维护。

​ 3、行为由父类控制,子类实现。

缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。

使用场景:

​ 1、有多个子类共有的方法,且逻辑相同

​ 2、重要的、复杂的方法,可以考虑作为模板方法。

注意事项:为防止恶意操作,一般模板方法都加上 final 关键词。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值