浅谈设计模式

2019-04-23  本文已影响0人  即将和何儒一样发型的男人

设计模式分类

总体来说设计模式分为三大类:

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

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

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

其实还有两类:并发型模式和线程池模式。用一个图片来整体描述一下:


03.png

设计模式六大原则

总原则:开闭原则(Open Close Principle)

开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类等,后面的具体设计中我们会提到这点。

1、单一职责原则

不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,如若不然,就应该把类拆分。

2、里氏替换原则(Liskov Substitution Principle)

里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。—— From Baidu 百科

历史替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。

3、依赖倒转原则(Dependence Inversion Principle)

这个是开闭原则的基础,具体内容:面向接口编程,依赖于抽象而不依赖于具体。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。

4、接口隔离原则(Interface Segregation Principle)

这个原则的意思是:每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。

5、迪米特法则(最少知道原则)(Demeter Principle)

就是说:一个类对自己依赖的类知道的越少越好。也就是说无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。

最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。

6、合成复用原则(Composite Reuse Principle)

原则是尽量首先使用合成/聚合的方式,而不是使用继承。

Java的设计模式

顺序根据Head First Design Patterns

一、策略模式

定义

策略模式定义了一系列的算法,并将每一个算法封装起来,使每个算法可以相互替代,使算法本身和使用算法的客户端分割开来,相互独立。

特点

策略模式体现了面向对象程序设计中非常重要的两个原则:
1. 封装变化的概念。
2. 编程中使用接口,而不是使用的是具体的实现类(面向接口编程)。

举例

我们就以Java中的TreeSet为例,TreeSet仅仅知道它只是接收一个Comparator这种接口类型,但是具体是哪种实现类,TreeSet并不关心,实现类在真正的传入TreeSet之前,TreeSet本身是不知道的,所以我们可以自己去实现Comparator接口,然后在实现类里面去封装好我们自己的规则(这里的规则你可以当做是算法),比如说我们要实现对一个集合的元素排序,但是到底是要升序排序还是降序排序,这个完全由我们来去控制,我们可以把这种变化的内容封装到自己的实现类中,真正运行的时候才知道具体的实现。

组成

  1. 抽象策略角色这个是一个抽象的角色,通常情况下使用接口或者抽象类去实现。对比来说,就是我们的Comparator接口。
  2. 具体策略角色包装了具体的算法和行为。对比来说,就是实现了Comparator接口的实现一组实现类。
  3. 环境角色内部会持有一个抽象角色的引用,给客户端调用。对比来说,就是我们的TreeSet类。说明:TreeSet内部一定会有一个策略类的一个成员变量,这样做的目的在于可以当我们在去创建TreeSet对象的时候,可以接收我们向TreeSet类中传递的具体的策略类。

编写步骤

  1. 定义抽象策略角色(为策略对象定义一个公共的接口)
  2. 编写具体策略角色(实际上就是实现上面定义的公共接口)
  3. 定义环境角色,内部持有一个策略类的引用

案例

实现一个加减乘除的功能。

  1. 定义抽象策略角色

    /*    定义抽象策略角色
     *    类似于Comparator接口
     */
    public interface Strategy{
          /*
           *  实现了两个数可以计算
           */
           public int calc(int num1,int num2);
    }
    
  2. 定义具体策略角色(本例子仅仅演示,只是定义加、减两种具体策略)

    /*    
     *    定义加法策略
     */
    public class AddStrategy implements Strategy{
          /*
           *  实现clac方法,完成两个数的和
           */
           public int calc(int num1,int num2){
                 return num1+num2;
           }
    }
    
    /*    
     *    定义减法策略
     */
    public class SubtractStrategy implements Strategy{
          /*
           *  实现clac方法,完成两个数相减
           */
           public int calc(int num1,int num2){
                 return num1-num2;
           }
    }
    
  3. 环境角色

 /*
  *   环境角色
  *       类似于TreeSet
   */
 public class Environment{
    // 持有对策略类的引用
    private Strategy strategy;

    // 类似于TreeSet
    public Environment(Strategy strategy){
        this.strategy = strategy;
    }

    public int calulate(int a , int b){
        return strategy.calc(a,b);
    }
 }
  1. 测试类

    /*
     *  测试类
     */
     public class Test{
        public static void main(String[] args){
            Environment environment = new Environment(new AddStrategy());
            int result = environment.calulate(20,30);
            Ststem.out.println(result);
        }
     }
    

总结

策略模式的优点:

  1. 策略模式的功能就是通过抽象、封装来定义一系列的算法,使得这些算法可以相互替换,所以为这些算法定义一个公共的接口,以约束这些算法的功能实现。如果这些算法具有公共的功能,可以将接口变为抽象类,将公共功能放到抽象父类里面。

  2. 策略模式的一系列算法是可以相互替换的、是平等的,写在一起就是if-else组织结构,如果算法实现里又有条件语句,就构成了多重条件语句,可以用策略模式,避免这样的多重条件语句。

  3. 扩展性更好:在策略模式中扩展策略实现非常的容易,只要新增一个策略实现类,然后在使用策略实现的地方,使用这个新的策略实现就好了。

策略模式的缺点:

  1. 客户端必须了解所有的策略,清楚它们的不同:
    如果由客户端来决定使用何种算法,那客户端必须知道所有的策略,清楚各个策略的功能和不同,这样才能做出正确的选择,但是这暴露了策略的具体实现。

  2. 增加了对象的数量:
    由于策略模式将每个具体的算法都单独封装为一个策略类,如果可选的策略有很多的话,那对象的数量也会很多。

  3. 只适合偏平的算法结构:
    由于策略模式的各个策略实现是平等的关系(可相互替换),实际上就构成了一个扁平的算法结构。即一个策略接口下面有多个平等的策略实现(多个策略实现是兄弟关系),并且运行时只能有一个算法被使用。这就限制了算法的使用层级,且不能被嵌套。

参考自 策略模式详解深入解析策略模式

二、观察者(Observer)模式

定义

     在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并自动更新。

大白话

     其实就是发布订阅模式,发布者发布信息,订阅者获取信息,订阅了就能收到信息,没订阅就收不到信息。

结构图

01.png

组成

  • 抽象被观察者角色:也就是一个抽象主题,它把所有对观察者对象的引用保存在一个集合中,每个主题都可以有任意数量的观察者。抽象主题提供一个接口,可以增加和删除观察者角色。一般用一个抽象类和接口来实现。
  • 抽象观察者角色:为所有的具体观察者定义一个接口,在得到主题通知时更新自己。
  • 具体被观察者角色:也就是一个具体的主题,在集体主题的内部状态改变时,所有登记过的观察者发出通知。
  • 具体观察者角色:实现抽象观察者角色所需要的更新接口,一边使本身的状态与制图的状态相协调。

使用场景例子

有一个微信公众号服务,不定时发布一些消息,关注公众号就可以收到推送消息,取消关注就收不到推送消息。

具体实现

  1. 定义一个抽象被观察者接口
 /***
  * 抽象被观察者接口
  * 声明了添加、删除、通知观察者方法
  * @author charming
  *
  */
 public interface Observerable {

     public void registerObserver(Observer o);
     public void removeObserver(Observer o);
     public void notifyObserver();

 }
  1. 定义一个抽象观察者接口
 /***
    * 抽象观察者
    * 定义了一个update()方法,当被观察者调用notifyObservers()方法时,观察者的update()方法会被回调。
    * @author charming
    *
    */
   public interface Observer {
       public void update(String message);
   }
  1. 定义被观察者,实现了Observerable接口,对Observerable接口的三个方法进行了具体实现,同时有一个List集合,用以保存注册的观察者,等需要通知观察者时,遍历该集合即可。
 import java.util.ArrayList;
 import java.util.List;

 /**
    * 被观察者,也就是微信公众号服务
    * 实现了Observerable接口,对Observerable接口的三个方法进行了具体实现
    * @author charming
    *
    */
 public class WechatServer implements Observerable {

     //注意到这个List集合的泛型参数为Observer接口,设计原则:面向接口编程而不是面向实现编程
     private List<Observer> list;
     private String message;

     public WechatServer() {
         list = new ArrayList<Observer>();
     }

     @Override
     public void registerObserver(Observer o) {
   
         list.add(o);
     }

     @Override
     public void removeObserver(Observer o) {
         if(!list.isEmpty())
             list.remove(o);
     }

     //遍历
     @Override
     public void notifyObserver() {
         for(int i = 0; i < list.size(); i++) {
             Observer oserver = list.get(i);
             oserver.update(message);
         }
     }

     public void setInfomation(String s) {
           this.message = s;
           System.out.println("微信服务更新消息: " + s);
           //消息更新,通知所有观察者
           notifyObserver();
     }

}
  1. 定义具体观察者,微信公众号的具体观察者为用户User
 /**
    * 观察者
    * 实现了update方法
    * @author charming
    *
    */
   public class User implements Observer {

       private String name;
       private String message;

       public User(String name) {
          this.name = name;
       }

       @Override
        public void update(String message) {
           this.message = message;
           read();
       }

       public void read() {
           System.out.println(name + " 收到推送消息: " + message);
       }
  }
  1. 编写一个测试类
  • 首先注册了三个用户,ZhangSan、LiSi、WangWu。公众号发布了一条消息"PHP是世界上最好用的语言!",三个用户都收到了消息。

  • 用户ZhangSan看到消息后颇为震惊,果断取消订阅,这时公众号又推送了一条消息,此时用户ZhangSan已经收不到消息,其他用户
    还是正常能收到推送消息。

public class Test {    
  public static void main(String[] args) {
    WechatServer server = new WechatServer();
    
    Observer userZhang = new User("ZhangSan");
    Observer userLi = new User("LiSi");
    Observer userWang = new User("WangWu");
    
    server.registerObserver(userZhang);
    server.registerObserver(userLi);
    server.registerObserver(userWang);
    server.setInfomation("PHP是世界上最好用的语言!");
    
    System.out.println("----------------------------------------------");
    server.removeObserver(userZhang);
    server.setInfomation("JAVA是世界上最好用的语言!");   
  }
}

测试结果


02.png

小结

  • 这个模式是松偶合的。改变主题或观察者中的一方,另一方不会受到影像。
  • JDK中也有自带的观察者模式。但是被观察者是一个类而不是接口,限制了它的复用能力。
  • 在JavaBean和Swing中也可以看到观察者模式的影子。

三、装饰者模式

   很烦,装饰者模式没保存到,由于篇幅过长我就不再贴了,放到后面与代理模式比较吧。

四、 工厂模式

概念

为创建对象提供过渡接口,以便将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的。

类别

  • 简单工厂模式Simple Factory:不利于产生系列产品;
  • 工厂方法模式Factory Method:又称为多形性工厂;
  • 抽象工厂模式Abstract Factory:又称为工具箱,产生产品族,但不利于产生新的产品;

这三种模式从上到下逐步抽象,并且更具一般性。GOF在《设计模式》一书中将工厂模式分为两类:工厂方法模式(Factory Method)与抽象工厂模式(Abstract Factory)。将简单工厂模式(Simple Factory)看为工厂方法模式的一种特例,两者归为一类。

1. 简单工厂模式

简单工厂模式又称静态工厂方法模式。
从命名上就可以看出这个模式一定很简单。它存在的目的很简单:定义一个用于创建对象的接口。
在简单工厂模式中,一个工厂类处于对产品类实例化调用的中心位置上,它决定哪一个产品类应当被实例化。

先来看看它的组成:

  • 工厂类角色:这是本模式的核心,含有一定的商业逻辑和判断逻辑。在java中它往往由一个具体类实现。
  • 抽象产品角色:它一般是具体产品继承的父类或者实现的接口。在java中由接口或者抽象类来实现。
  • 具体产品角色:工厂类所创建的对象就是此角色的实例。在java中由一个具体类实现。

简单工厂模式又分为三种:

  1. 普通:

    就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。首先看下关系图: 05.png

举例如下:(我们举一个发送邮件和短信的例子)首先,创建二者的共同接口:

 public interface Sender {  
    public void Send();  
}

其次,创建实现类:

public class MailSender implements Sender {  
    @Override  
    public void Send() {  
        System.out.println("this is mailsender!");  
    }  
}


public class SmsSender implements Sender {  
@Override  
    public void Send() {  
        System.out.println("this is sms sender!");  
    }  
}

最后,建工厂类:

public class SendFactory {  
    public Sender produce(String type) {  
        if ("mail".equals(type)) {  
            return new MailSender();  
        } else if ("sms".equals(type)) {  
            return new SmsSender();  
        } else {  
            System.out.println("请输入正确的类型!");  
            return null;  
        }  
    }  
}

我们来测试下:

public class FactoryTest {  
    public static void main(String[] args) {  
        SendFactory factory = new SendFactory();  
        Sender sender = factory.produce("sms");  
        sender.Send();  
    }  
}

输出:this is sms sender!

  1. 多个方法

    是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。关系图: 06.png
    将上面的代码做下修改,改动下SendFactory类就行,如下:
public class SendFactory {  
   public Sender produceMail(){  
        return new MailSender();  
    }  
  
    public Sender produceSms(){  
        return new SmsSender();  
    }  
}  

测试类如下:

public class FactoryTest {  
    public static void main(String[] args) {  
        SendFactory factory = new SendFactory();  
        Sender sender = factory.produceMail();  
        sender.Send();  
    }  
}

输出:this is mailsender!

  1. 多个静态方法
    将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
public class SendFactory {  
  
      public static Sender produceMail(){  
          return new MailSender();  
      }  
  
      public static Sender produceSms(){  
          return new SmsSender();  
      }  
}

public class FactoryTest {  

    public static void main(String[] args) {      
        Sender sender = SendFactory.produceMail();  
        sender.Send();  
    }  
}

输出:this is mailsender!

总体来说,工厂模式适合:凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。在以上的三种模式中,第一种如果传入的字符串有误,不能正确创建对象,第三种相对于第二种,不需要实例化工厂类,所以,大多数情况下,我们会选用第三种——静态工厂方法模式。

2. 工厂方法模式(Factory Method)

简单工厂模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到工厂方法模式,创建一个工厂接口和创建多个工厂实现类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。 07.png
public interface Sender {  
    public void Send();  
}

//两个实现类:
public class MailSender implements Sender {  
    @Override  
    public void Send() {  
        System.out.println("this is mailsender!");  
    }  
}

public class SmsSender implements Sender {  
    @Override  
    public void Send() {  
        System.out.println("this is sms sender!");  
    }  
}

//工厂接口
public interface Provider {  
    public Sender produce();  
}

//两个工厂类:
public class SendMailFactory implements Provider {  
    @Override  
    public Sender produce(){  
        return new MailSender();  
    }  
}

public class SendSmsFactory implements Provider{  
    @Override  
     public Sender produce() {  
       return new SmsSender();  
    }  
}

//测试类:
public class Test {  
    public static void main(String[] args) {  
        Provider provider = new SendMailFactory();  
        Sender sender = provider.produce();  
        sender.Send();  
    }  
}

其实这个模式的好处就是,如果你现在想增加一个功能:发及时信息,则只需做一个实现类,实现Sender接口,同时做一个工厂类,实现Provider接口,就OK了,无需去改动现成的代码。这样做,拓展性较好!

3. 抽象工厂模式

为创建一组相关或者是相互依赖的对象提供一个接口,而不需要指定他们的具体实现类。


08.png

例如:
Q3和Q7有不同的轮胎、发动机、制动系统。虽然生产的零件不同,型号不同。但是根本上都有共同的约束,就是轮胎、发动机、制动系统。设计如下:
1. 需要一个抽象工厂,里面有三个接口分别为生产轮胎、发动机、制动系统,抽象类
2. 需要三个抽象产品分别为轮胎、发动机、制动系统,抽象接口
3. 需要实现上面的三个抽象接口,定义出每个接口不通的对象,比如:普通轮胎和越野轮胎
4. 需要两个具体类继承自上面的抽象类,实现具体的工厂,比如:生产Q3的工厂和生产Q7的工厂
5. 在客户端new出对应的具体工厂并调用对应的生产方法

//1.抽象工厂
public abstract class CarFactory {
 /**
 * 生产轮胎
 * 
 * @return 轮胎
 * */
    public abstract ITire createTire();

/**
 * 生产发动机
 * 
 * @return 发动机
 * */
    public abstract IEngine createEngine();

/**
 * 生产制动系统
 * 
 * @return 制动系统
 * */
    public abstract IBrake createBrake();

}

//2.三个产品抽象接口
public interface ITire {
/**
 * 轮胎 
 */
    void tire();
}

public interface IEngine {
/**
 *发动机 
 */
    void engine();
}

public interface IBrake {
/**
 *制动系统 
 */
    void brake();
}

//3.根据抽象接口定义不同的对象
public class NormalBrake implements IBrake{
    @Override
    public void brake() {
        System.out.println("普通制动");
    }
}
public class SeniorBrake implements IBrake{
    @Override
    public void brake() {
        System.out.println("高级制动");
    }
}  //后面的定义省略。。。。。。。。。。。。。

//4.实现具体的工厂类
public class Q3Factory extends CarFactory{

    @Override
    public ITire createTire() {
        return new NormalTire();
    }

    @Override
    public IEngine createEngine() {
        return new DomesticEngine();
    }

    @Override
    public IBrake createBrake() {
        return new NormalBrake();
    }
}

//5.客户端使用
public class Client {
    public static void main(String[] args) {
        //A车厂
        CarFactory factoryQ3 = new Q3Factory();
        factoryQ3.createTire().tire();
        factoryQ3.createEngine().engine();
        factoryQ3.createBrake().brake();
    }
}

//输出
普通轮胎
国产发动机
普通制动

4. 工厂方法模式和抽象工厂模式区别

工厂方法模式:
一个抽象产品类,可以派生出多个具体产品类。
一个抽象工厂类,可以派生出多个具体工厂类。
每个具体工厂类只能创建一个具体产品类的实例。

抽象工厂模式:
多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。
一个抽象工厂类,可以派生出多个具体工厂类。
每个具体工厂类可以创建多个具体产品类的实例,也就是创建的是一个产品线下的多个产品。

区别:
工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个。
工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个。
工厂方法创建 "一种" 产品,他的着重点在于"怎么创建",也就是说如果你开发,你的大量代码很可能围绕着这种产品的构造,初始化这些细节上面。也因为如此,类似的产品之间有很多可以复用的特征,所以会和模版方法相随。
抽象工厂需要创建一些列产品,着重点在于"创建哪些"产品上,也就是说,如果你开发,你的主要任务是划分不同差异的产品线,并且尽量保持每条产品线接口一致,从而可以从同一个抽象工厂继承。

对于java来说,你能见到的大部分抽象工厂模式都是这样的:
---它的里面是一堆工厂方法,每个工厂方法返回某种类型的对象。

比如说工厂可以生产鼠标和键盘。那么抽象工厂的实现类(它的某个具体子类)的对象都可以生产鼠标和键盘,但可能工厂A生产的是罗技的键盘和鼠标,工厂B是微软的。

这样A和B就是工厂,对应于抽象工厂;
每个工厂生产的鼠标和键盘就是产品,对应于工厂方法;

用了工厂方法模式,你替换生成键盘的工厂方法,就可以把键盘从罗技换到微软。但是用了抽象工厂模式,你只要换家工厂,就可以同时替换鼠标和键盘一套。如果你要的产品有几十个,当然用抽象工厂模式一次替换全部最方便(这个工厂会替你用相应的工厂方法)

所以说抽象工厂就像工厂,而工厂方法则像是工厂的一种产品生产线

五、单例模式

定义

单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。

许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

好处

1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。
2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。

基本的实现思路

单例模式要求类能够有返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称)。

单例的实现主要是通过以下两个步骤:

将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。

注意事项

单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。

单例模式的八种写法

1、饿汉式(静态常量)[可用]

  public class Singleton {
      private final static Singleton INSTANCE = new Singleton();

      private Singleton(){}

      public static Singleton getInstance(){
           return INSTANCE;
      }
 }

优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

2、饿汉式(静态代码块)[可用]

 public class Singleton {

   private static Singleton instance;

   static {
       instance = new Singleton();
   }

   private Singleton() {}

   public static Singleton getInstance() {
       return instance;
   }
 }

这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。

3、懒汉式(线程不安全)[不可用]

 public class Singleton {

     private static Singleton singleton;

     private Singleton() {}

     public static Singleton getInstance() {
         if (singleton == null) {
             singleton = new Singleton();
         }
         return singleton;
     }
 }

这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

4、懒汉式(线程安全,同步方法)[不推荐用]

 public class Singleton {

     private static Singleton singleton;

     private Singleton() {}

     public static synchronized Singleton getInstance() {
         if (singleton == null) {
             singleton = new Singleton();
         }
         return singleton;
     }
 }

解决上面第三种实现方式的线程不安全问题,做个线程同步就可以了,于是就对getInstance()方法进行了线程同步。

缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。

5、懒汉式(线程安全,同步代码块)[不可用]

 public class Singleton {

     private static Singleton singleton;

     private Singleton() {}

     public static Singleton getInstance() {
         if (singleton == null) {
             synchronized (Singleton.class) {
                 singleton = new Singleton();
             }
         }
         return singleton;
     }
 }

由于第四种实现方式同步效率太低,所以摒弃同步方法,改为同步产生实例化的的代码块。但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。

6、双重检查[推荐用]

 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;
     }
 }

Double-Check概念对于多线程开发者来说不会陌生,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。

优点:线程安全;延迟加载;效率较高。

7、静态内部类[推荐用]

 public class Singleton {

     private Singleton() {}

     private static class SingletonInstance {
         private static final Singleton INSTANCE = new Singleton();
     }

     public static Singleton getInstance() {
         return SingletonInstance.INSTANCE;
     }
 }

这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。

类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

优点:避免了线程不安全,延迟加载,效率高。

8、枚举[推荐用]

 public enum Singleton {
     INSTANCE;
     public void whateverMethod() {

     }
 }

借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。可能是因为枚举在JDK1.5中才添加,所以在实际项目开发中,很少见人这么写过。

总结

1、单例模式理解起来简单,但是具体实现起来还是有一定的难度。

2、synchronized关键字锁定的是对象,在用的时候,一定要在恰当的地方使用(注意需要使用锁的对象和过程,可能有的时候并不是整个对象及整个过程都需要锁)。

采用类的静态方法,实现单例模式的效果,也是可行的,此处二者有什么不同?

首先,静态类不能实现接口。(从类的角度说是可以的,但是那样就破坏了静态了。因为接口中不允许有static修饰的方法,所以即使实现了也是非静态的)

其次,单例可以被延迟初始化,静态类一般在第一次加载是初始化。之所以延迟加载,是因为有些类比较庞大,所以延迟加载有助于提升性能。

再次,单例类可以被继承,他的方法可以被覆写。但是静态类内部方法都是static,无法被覆写。

最后一点,单例类比较灵活,毕竟从实现上只是一个普通的Java类,只要满足单例的基本需求,你可以在里面随心所欲的实现一些其它功能,但是静态类不行。

参考自单例模式的八种写法比较

六、命令模式

定义

命令模式(Command Pattern):将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。
Command Pattern: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

举例

假设某个公司需要设计一个多用功能的遥控器。基本的需求如下:

该遥控器有可以控制风扇,白炽灯,热水器等等的多对开关,而且可能还有其他的电器,暂时不做其功能,但是希望可以保留接口,用的时间可以方便的扩展。

除上面的需求之外,还需要有个按钮,可以撤销上一步的操作。基本功能如下图:


09.png

在设计遥控器时,风扇,白炽灯,热水器的开关方法已经定义好,其名字各不相同。不妨设置其方法为如下:


10.png

由于各种电器的开关方法都不一样,而且还存在一个待扩展的电器,如果没有学习命名模式之前,我们在设置扩展的开关时,会出现的问题是什么呢?假设现在有电视,冰箱还可能会用到遥控器,那么我们会在最后一个开关上写if else,当然如果哪一天有多了一个大门也加入了我们的遥控的行列,这样我们继续加if else ,很显然随着电器的高速发展,会有多个需要遥控可以控制的。

举个例子,如果我们是需要遥控的客户,现在有一款遥控如果有遥控可以进行扩展,一种是可以扩展指定类型的,像上面的,只能再去扩展电视和冰箱中的一种,偶尔有一天你看到隔壁邻居的门,也可以使用遥控了,所以你去把你的高级遥控器,拿到扩展店时,扩展工程师说了,现在只能扩展电视和冰箱,不支持对大门的遥控扩展.

我们肯定是希望,可以自由的扩展,大门可以使用遥控了,就对大门扩展,车门使用遥控了,就对车门扩展……其实也就是一种松耦合的实现。

我们可以先定义好我们的风扇,白炽灯,热水器。然后定义其分别的开关命令,每个命令都有自己对应的电器引用,而且会在命令的Excute中包装电器的开或者关,最后需要把命令安装到遥控器上面,在遥控器上每个按钮都对应有自己的激发方法,其代码如下:

namespace RemoteControl
{
    class Program
    {
        static void Main(string[] args)
        {
            //家中的电器
            Fan fan=new Fan();
            Light light=new Light();
            Heater heater=new Heater();

            //电器分别对应的命令
            FanOffCommand fanOffCommand=new FanOffCommand(fan);
            FanOnCommand fanOnCommand=new FanOnCommand(fan);
            LightOnCommand lightOnCommand=new LightOnCommand(light);
            LightOffCommand lightOffCommand=new LightOffCommand(light);
            HeaterOnCommand heaterOnCommand=new HeaterOnCommand(heater);
            HeaterOffCommand heaterOffCommand=new HeaterOffCommand(heater);
            RemoteControl remoteControl = new RemoteControl();

            //设置遥控器
            remoteControl.SetCommand(0, fanOnCommand, fanOffCommand);
            remoteControl.SetCommand(1, lightOnCommand, lightOffCommand);
            remoteControl.SetCommand(2, heaterOnCommand, heaterOffCommand);
            //分别测试遥控器的命令
            remoteControl.OnButtonWasPress(1);
            remoteControl.OffButtonWasPress(1);
            remoteControl.OnButtonWasPress(0);
            remoteControl.OffButtonWasPress(0);
            Console.ReadKey();
        }
    }

    /// <summary>
    /// 风扇类
    /// </summary>
   public class Fan
    {
        public void FanOn()
        {
            Console.WriteLine("风扇开了");
        }
        public void FanOff()
        {
            Console.WriteLine("风扇关了");
        }
    }
    /// <summary>
    /// 灯类
    /// </summary>
   public class Light
   {
       public void LightOn()
       {
           Console.WriteLine("灯亮了");
       }
       public void LightOff()
       {
           Console.WriteLine("灯灭了");
       }
   }
    /// <summary>
    /// 热水器类
    /// </summary>
   public class Heater
   {
       public void HeaterOn()
       {
           Console.WriteLine("加热中");
       }
       public void HeaterOff()
       {
           Console.WriteLine("停止加热");
       }
   }

    /// <summary>
    /// 命令接口
    /// </summary>
   public interface ICommand
   {
       void Excute();
   }

   public class FanOnCommand : ICommand
   {
       Fan fan;
       public FanOnCommand(Fan fan)
       {
           this.fan = fan;
       }
        public void Excute()
        {
            this.fan.FanOn();
        }
    }

   public class FanOffCommand : ICommand
   {
       Fan fan;
       public FanOffCommand(Fan fan)
       {
           this.fan = fan;
       }
        public void Excute()
        {
            this.fan.FanOff();
        }
   }

   public class LightOnCommand : ICommand
   {
       Light light;
       public LightOnCommand(Light light)
       {
           this.light = light; 
       }
       public void Excute()
       {
           light.LightOn();
       }

   }

   public class LightOffCommand : ICommand
   {
       Light light;
       public LightOffCommand(Light light)
       {
           this.light = light;
       }
       public void Excute()
       {
           this.light.LightOff();
       }
   }

   public class HeaterOnCommand : ICommand
   {
       Heater heater;
       public HeaterOnCommand(Heater heater)
       {
           this.heater = heater;
       }
       public void Excute()
       {
           this.heater.HeaterOn();
       }
   }

   public class HeaterOffCommand : ICommand
   { 
       Heater heater;
       public HeaterOffCommand(Heater heater)
       {
           this.heater = heater;
       }
       public void Excute()
       {
           this.heater.HeaterOff();
       }
   }

   public class NoCommand : ICommand
   {
       public void Excute()
       { }
   }

   public class RemoteControl
   {
      private ICommand[] onCommands;
      private ICommand[] offCommands;
      public RemoteControl()
      {
         ICommand noCommand=new NoCommand();
          onCommands = new ICommand[4];
          offCommands = new ICommand[4];
          for (int i = 0; i < 4; i++)
          {
              onCommands[i] = noCommand;
              offCommands[i] = noCommand;
          }
      }

      public void SetCommand(int slot, ICommand onCommand, ICommand offCommand)
      {
          onCommands[slot] = onCommand;
          offCommands[slot] = offCommand;
      }
      public void OnButtonWasPress(int slot)
      {
          onCommands[slot].Excute();
      }
      public void OffButtonWasPress(int slot)
      {
          offCommands[slot].Excute();
      }
     
   }
}

这样基本上就实现了我们的现有的三种电器的遥控。需要注意的是,在开始初始化遥控器时,对每个命令初始化成了NoCommand,也就是什么都不执行。在命令的初始化经常使用,同时这也解决了我们的在扩展前什么都不做的难题。看了风扇,白炽灯,热水器的遥控实现,进一步的扩展任何的电器,相信都不是什么难事。但是还有个功能没有实现,就是撤销到上一步的操作,接下来我们就来实现撤销操作。

撤销操作就想我们遥控中的返回一样。基本上就是灯亮着,突然按了一下关灯,然后再按一下返回键,灯就亮了。其他的电器同样的道理。下面先看一下灯的撤销原理,命令除了执行外还有一个撤销,所以我们需要先都命令的接口添加一个方法。

/// <summary> 
/// 命令接口 
/// </summary> 
public interface ICommand 
{ 
    void Excute(); 
    void Undo(); 
}

对于开灯需要做的修改如下:

public class LightOnCommand : ICommand 
   { 
       Light light; 
       public LightOnCommand(Light light) 
       { 
           this.light = light; 
       } 
       public void Excute() 
       { 
           light.LightOn(); 
       } 
       /// <summary> 
       /// 调用命令的反命令 
       /// </summary> 
       public void Undo() 
       { 
           light.LightOff(); 
       } 
   }

其他命令同理,代码会在源码中一并给出。也就是每个命令都有自己的反命令,在Undo方法里面也就是调用反命令的Excute方法。每当按下一个按钮时,就去记录其命令的名称,如果按撤销的话,就执行命名的Undo方法。下面给出主要代码:

public void OnButtonWasPressed(int slot) 
   { 
       onCommands[slot].Excute(); 
       backCommand=onCommands[slot]; 
   } 
   public void OffButtonWasPressed(int slot) 
   { 
       offCommands[slot].Excute(); 
       backCommand = offCommands[slot]; 
   } 
   public void BackButtonWasPressed() 
   { 
       backCommand.Undo(); 
   }

以上是对遥控器对命令的撤销,需要注意两点1、通过记住命令执行之前的状态,然后去恢复到原来的状态。2、在每次执行之后要记住执行的那个命令。也即记住命令和记住状态。

除了一次执行一个命令和撤销一个命令,当然还可以一次执行多个命令。下面给出主要代码:

public class MutlipleCommand : ICommand 
   { 
       ICommand[] commands; 
       ICommand[] backCommands; 
       public MutlipleCommand(ICommand[] commands) 
       { 
           this.commands = commands; 
           backCommands = new ICommand[commands.Length]; 
       }


       public void Excute() 
       { 
           for (int i = 0; i < commands.Length; i++) 
            { 
              commands[i].Excute(); 
              backCommands[i] = commands[i]; 
            } 
           
       }

       public void Undo() 
       { 
           for (int i = 0; i < commands.Length; i++) 
           { 
               backCommands[i].Undo(); 
           } 
       } 
   }

总结

11.png

命令模式主要通过中介Command实现了发出命令者和命令的执行者,也即Invoke类和Receiver的松耦合。

参考自命令模式

七、适配器模式

定义

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

1、类的适配器模式

12.png

核心思想就是:有一个Source类,拥有一个方法,待适配,目标接口是Targetable,通过Adapter类,将Source的功能扩展到Targetable里,看代码:

public class Source {  
  
    public void method1() {  
        System.out.println("this is original method!");  
    }  
}

public interface Targetable {  
  
    /* 与原类中的方法相同 */  
    public void method1();  
  
    /* 新类的方法 */  
    public void method2();  
} 

public class Adapter extends Source implements Targetable {  
  
    @Override  
    public void method2() {  
        System.out.println("this is the targetable method!");  
    }  
}

//Adapter类继承Source类,实现Targetable接口,下面是测试类:
public class AdapterTest {  
  
    public static void main(String[] args) {  
        Targetable target = new Adapter();  
        target.method1();  
        target.method2();  
    }  
}


输出:

this is original method!
this is the targetable method!

这样Targetable接口的实现类就具有了Source类的功能。

2、对象的适配器模式

基本思路和类的适配器模式相同,只是将Adapter类作修改,这次不继承Source类,而是持有Source类的实例,以达到解决兼容性的问题。看图:


13.png

只需要修改Adapter类的源码即可:

public class Wrapper implements Targetable {  
  
    private Source source;  
      
    public Wrapper(Source source){  
        super();  
        this.source = source;  
    }  
    @Override  
    public void method2() {  
        System.out.println("this is the targetable method!");  
    }  
  
    @Override  
    public void method1() {  
        source.method1();  
    }  
}

public class AdapterTest {  
  
    public static void main(String[] args) {  
        Source source = new Source();  
        Targetable target = new Wrapper(source);  
        target.method1();  
        target.method2();  
    }  
}
输出与第一种一样,只是适配的方法不同而已。

3、接口的适配器模式

第三种适配器模式是接口的适配器模式,接口的适配器是这样的:有时我们写的一个接口中有多个抽象方法,当我们写该接口的实现类时,必须实现该接口的所有方法,这明显有时比较浪费,因为并不是所有的方法都是我们需要的,有时只需要某一些,此处为了解决这个问题,我们引入了接口的适配器模式,借助于一个抽象类,该抽象类实现了该接口,实现了所有的方法,而我们不和原始的接口打交道,只和该抽象类取得联系,所以我们写一个类,继承该抽象类,重写我们需要的方法就行。看一下类图:


14.png

这个很好理解,在实际开发中,我们也常会遇到这种接口中定义了太多的方法,以致于有时我们在一些实现类中并不是都需要。看代码:

public interface Sourceable {  
      
    public void method1();  
    public void method2();  
}

public abstract class Wrapper2 implements Sourceable{  
      
    public void method1(){}  ;
    public void method2(){}  ;
}

public class SourceSub1 extends Wrapper2 {  
    public void method1(){  
        System.out.println("the sourceable interface's first Sub1!");  
    }  
}

public class SourceSub2 extends Wrapper2 {  
    public void method2(){  
        System.out.println("the sourceable interface's second Sub2!");  
    }  
}

public class WrapperTest {  
  
    public static void main(String[] args) {  
        Sourceable source1 = new SourceSub1();  
        Sourceable source2 = new SourceSub2();  
          
        source1.method1();  
        source1.method2();  
        source2.method1();  
        source2.method2();  
    }  
}

测试输出:

the sourceable interface's first Sub1!
the sourceable interface's second Sub2!

达到了我们的效果!

总结

类的适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。

对象的适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个Wrapper类,持有原类的一个实例,在Wrapper类的方法中,调用实例的方法就行。

接口的适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类Wrapper,实现所有方法,我们写别的类的时候,继承抽象类即可。

八、模板方法模式(略)

九、迭代器与组合模式(略)

十、状态模式(略)

十一、代理模式(极其重要)

与装饰者模式的区别

装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。换句话 说,用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。因此,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例。并且,当我们使用装饰器模 式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。

举个例子

例如:有婴儿,婴儿会吃饭和走动,如以下类

//婴儿类
public class Child implements Human
{
    public void eat()
    {
        System.out.println("eat something....");
    }

    @Override
    public void run()
    {
        System.out.println("Child run very slow");
    }
}

突然有一天,家长发现不行,孩子不能随便吃东西,而且吃饭前一定要洗手。但是孩子太小(被委托方),不会自己洗手。家长(Client 端)又没法照顾孩子。那简单,找个保姆照顾孩子! 让保姆类和婴儿类共同实现同一个接口,让保姆类全程管理小孩,同时在家长眼里,只要看到保姆在帮孩子洗手就可以了。于是,有以下内容。

//保姆类
public class BabySitter implements Human
{

    @Override
    public void eat()
    {

    }

    @Override
    public void run()
    {

    }

}

现在保姆已经有了,孩子也有了,怎么把孩子跟保姆关联起来。让保姆给相应的孩纸洗手。于是保姆类更改如下

//保姆类
public class BabySitter implements Human
{
    private Human human;

    public BabySitter(Human human)
    {
        this.human = human;
    }

    @Override
    public void eat()
    {
        // 添加washHand的方法
        this.washHandForChild();
        human.eat();
    }

    @Override
    public void run()
    {

    }

    public void washHandForChild()
    {
        System.out.println("help the child to wash his hands");
    }
}

保姆与婴儿类关联

好,那么家长就是给孩纸找了个保姆助手(装饰器),让他附加了一些婴儿做不了事。同时家长也没有强迫孩纸自己学会洗手(不更改Child类)

//客户端
public class Client
{
    public static void main(String[] args)
    {
        Human human = new BabySitter(new Child());
        human.eat();
    }
}

家长客户端代码

以上就是一个简单的装饰模式,来看一下这一块完整的类图。


15.png

装饰模式的一个很重要特点就是,在客户端可以看到抽象对象的实例,如Human human = new BabySitter(new Child()); 因为装饰模式通过聚合方式,把内容整合到装饰类里面了。

装饰者模式能够使用装饰类对抽象对象进行装饰。假如来了个OldMan类手脚不利索。保姆类BabySitter同样能够胜任这个OldMan的饭前洗手操作。

代理模式类图如下:

16.png

由该类图可知,以上BabySitter代码应该如下:

//保姆类
public class BabySitter implements Human
{    
    private Child child;

    public BabySitter()
    {
        child = new Child();
    }

    @Override
    public void eat()
    {
        // 添加washHand的方法
        this.washHandForChild();
        human.eat();
    }

    @Override
    public void run()
    {

    }

    public void washHandForChild()
    {
        System.out.println("help the child to wash his hands");
    }
}

//客户端
public class Client
{
    public static void main(String[] args)
    {
        Human human = new BabySitter();
        human.eat();
    }
}

装饰者模式和代理模式的最后运行的结果都是一样的,由代理模式代码可知,客户端不关心代理类了哪个类。但代码控制了客户端对委托类的访问。客户端代码表现为 Human human = new BabySitter( );

所以资料上都说了,装饰模式主要是强调对类中代码的拓展,而代理模式则偏向于委托类的访问限制。两者都一样拥有抽象角色(接口)、真实角色(委托类)、代理类 。

由于代理类实现了抽象角色的接口,导致代理类无法通用。如有天,一个有钱人养了只小猩猩,他要一个保姆在猩猩吃东西前,帮猩猩洗手....保姆根本不懂猩猩的特性(跟猩猩类不是同一类型的,保姆属于Human类,而猩猩可能属于Animal类型。),但洗手这个方法是不变的,用水洗。能不能找一个代理说既可以照看人吃饭前洗手也可以照看猩猩吃饭前洗手?

要实现这种功能,必须让代理类与特定的接口分离。在代理的时候能够了解每个委托的特性,这就有可能了。这时候动态代理就出现了。

动态代理
关于动态代理模式里面有两种实现,一种是jdk实现,一种是cglib来实现。
下面来整jdk来实现动态代理的Java实例。
jdk动态代理模式里面有个拦截器的概念,在jdk中,只要实现了InvocationHandler这个接口的类就是一个拦截器类。
还使用了些反射的相关概念。
拦截器的概念不了解没关系,假如写了个请求到action,经过拦截器,然后才会到action。然后继续有之后的操作。
拦截器就像一个过滤网,一层层的过滤,只要满足一定条件,才能继续向后执行。
拦截器的作用:控制目标对象的目标方法的执行。

拦截器的具体操作步骤:
1.引入类:目标类和一些扩展方法相关的类。
2.赋值:调用构造函数给相关对象赋值
3.合并逻辑处理:在invoke方法中把所有的逻辑结合在一起。最终决定目标方法是否被调用。

总结

动态代理静态代理的区别,代理涉及到两个关联词代理类和委托类。静态代理一个代理类针对一个委托类!动态代理一个代理类可利用反射机制代理多个委托类

我们根据加载被代理类的时机不同,将代理分为静态代理和动态代理。如果我们在代码编译时就确定了被代理的类是哪一个,那么就可以直接使用静态代理;如果不能确定,那么可以使用类的动态加载机制,在代码运行期间加载被代理的类这就是动态代理,比如RPC框架和Spring AOP机制。

参考自代理模式和装饰模式

上一篇下一篇

猜你喜欢

热点阅读