设计模式的十八般武艺

2,021 阅读16分钟

六大原则

单一职责原则

定义:就一个类而言,应该仅有一个引起它变化的原因。

其实字面意思就已经表达的比较明确,单一,也就是干尽量少的事情。在HDU中可以对耦合和内聚程度的评判有一定的了解。

什么叫做少,其实很难有一个标准。但是在Android的MVC框架中,Activity既作为View,又起着Controller的作用的时候是否显得会很臃肿呢?他需要进行页面的刷新,网络处理,消息处理等等,写起来容易,但是在我们进行维护的时候,是不是会很头疼呢,这就是单一职责原则的对应所在了。 

开放封闭原则

定义:类,模块,函数等应该是可以扩展的,但是不可以修改。

在日常的项目开发中,需求一直是处于一个变动的状态,但是这同样也会成为项目开发的壁垒,如果你的Bean今天是一只猫,明天就需要是一只狗呢?重新打补丁吗?显然是一个很不合适的做法。而开放封闭原则,解决的就是这一类问题。不论是猫,还是狗,他们总会有相同的特征,抽象化也就是这个原则实现的基础。

// 定义一个动物抽象类
public abstract class Animal {
    abstract void do();
}
 
// 猫实现抽象方法
class Cat extends Animal {
    @Override
    void do() {
        System.out.println("喵");
    }
}
 
// 狗实现抽象方法
class Dog extends Animal {
    @Override
    void do() {
        System.out.println("汪");
    }
}

里氏替换原则

定义:所有引用基类(一般来说都是抽象类或接口)的地方必须能透明的使用其子类的对象。

定义的具体含义就是将一个基类对象替换成其子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。

想来直接看定义可能有点难以理解,接下来就用代码来具体实现里氏替换原则了。

/**
 * 基于里氏替换原则实现
 */
class Manage1 {
    private Animal animal;
 
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }
 
    void getSound() {
        animal.do();
    }
}

/**
 * 使用子类实现
 */
class Manage2 {
    private Cat cat;
 
    public void setAnimal(Cat cat) {
        this.cat = cat;
    }
 
    void getSound() {
        cat.do();
    }
}

以上基于两种写法,给予读者评判,如果使用Manage2,如果我们希望获得Dog的声音,那么就需要重新实现Manager3,然后差异就是只是把Cat置换成Dog。而Manage1很好的解决了这个问题,因为不论是Cat还是Dog都是Animal的子类。

依赖倒置原则

定义:高层模块不应该依赖底层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

两个概念:

  • 抽象/高层模块:接口或者抽象类,一般只定义了有什么操作,但是没有具体实现。
  • 细节/底层模块:实现接口或者继承抽象类而产生的,一般来说就是我们通过new产生出来的对象。

这里的代码与开闭原则、里氏替换原则一致,详细见于上文。

DogCat作为Animal的子类,对于do()函数,父类只是一个抽象的方法,而子类完成了具体的实现。而当类Manage1希望与类Cat发生关联时,就是通过类Animal来完成,好处就是Manage1不再关注了类CatDog的具体实现。

迪米特原则

定义:一个软件实体应当少的与其他实体发生相互作用。

其实和上面的内容差不多,同样都是为了降低耦合程度,直接用Demo证明就清楚明了了。

电话网络形式:

  • 打电话的人 --> 接电话的人
  • 打电话的人 --> 中间服务商转接 --> 接电话的人

第一种已经被时代抛弃了,虽然我们并不知觉,但是第一种电话网络模式,如果用在现代社会,那么带来的后果就是你再也看不见太阳了,头顶密密麻麻的电话线,更何况那是座机互联的时代,能够靠电话线来解决,但是这个时代呢?移动互联的时代呢,不能再靠着电话线来解决问题,而是中间商的介入就改变了这个现状。

(1)第一种电话网络模式

/**
 * 打电话的人
 */
class Call {
    private Receiver receiver;
 
    public void setReceicer(Receiver receiver) {
        this.receiver = receiver;
    }
 
    public void call() {
        receiver.receive();
    }
}

/**
 * 接电话的人
 */
class  Receiver{
    
    private String number;
    Receiver(String number){
         this.number = number;
    }
    
    public void receive() {
        System.out.println("接通电话");
    }
}

class Main{
    public static void main(String[] args) {
        Call call = new Call();
        call.setReceicer(new Receiver("电话号码"));
        call.call();
    }
}

代码虽然看着很轻松,但是折射到现实情况的时候,每一个拨打电话和接收电话的人之间都等于连接了一条电话线。

(2)第二种电话网络模式

/**
 * 转接
 */
public abstract class Manager {
    abstract void link();
}
 
/**
 * 打电话的人
 */
class Call {
    private Manager manager;
    
    public void setManager(Manager manager) {
        this.manager = manager;
    }
 
    public void call() {
        manager.link();
    }
}
 
/**
 * 接电话的人
 */
class  Receiver{
    
    private String number;
    Receiver(String number){
         this.number = number;
    }
    
    public void receive() {
        System.out.println("接通电话");
    }
}

class CMCC extends Manager {
    private String number;
  
    CMCC(String number){
      this.number = number;
    }

    public void link() {
        System.out.println("连接接电话的人");
        Receiver receiver = new Receiver();
        receiver.receive();
    }
}

class Main{
    public static void main(String[] args) {
        Call call = new Call();
        call.setManager(new CMCC("电话号码"));
        call.call();
    }
}

这个时候两个实体通过加入中间商的形式降低了耦合度。也就像我们生活中的各个电话厂商,你不会因为厂商不同而担心不能拨通电话,因为中间的处理过程厂商会帮你解决。

接口隔离原则

定义:一个类对另一个类的依赖应该建立在最小的接口上

一个接口内要实现的函数数量可控,有那么一点像数据库里的第一范式。让我们从Demo看看使用原则的与否的不同之处。

正常实现

// 特征
interface Character {
    void look();
    void nature();
}

class Dog implements Character{
    @Override
    void look() {
        System.out.println("能看");
    }
    @Override
    void nature() {
        System.out.println("淘气");
    }
}

基于接口隔离原则实现

// 外观
interface Facade {
     void look();
}
// 内在
interface Inherent {
    void nature();
}

class Dog implements Inherent{
    @Override
    void nature() {
        System.out.println("淘气");
    }
}

几个常用的设计模式

单例模式

定义:保证一个类仅有一个实例,并提供用于一个访问它的全局访问点。

四种写法及其优缺点

(1) 饿汉模式

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

在类加载时就已经完成了初始化。

优点:1. 保障了线程同步的问题;2. 获取对象的效率高。

缺点:1. 降低了类加载时速度;2. 如果一直不使用,会内存的浪费。

(2) 懒汉模式

  1. 线程不安全
public class Singleton {
    private static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance == null) instance = new Singleton();
        return instance;
    }
}

缺点:存在线程同步问题

  1. 线程安全
public class Singleton {
    private static Singleton instance;
    private Singleton(){}
    public static synchronized Singleton getInstance(){
        if(instance == null) instance = new Singleton();
        return instance;
    }
}

缺点:每一次都需要同步,存在一定的开销问题。

懒汉模式相较于饿汉模式,不会存在不使用的问题。虽然不再在加载时消耗资源,但是实例化时同样会有一定的时间开销。

(3) 双重检查模式/DCL

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

使用volatile关键词是对正确性的一种保障。

相较于懒汉模式而言,这又是一种升级。因为不在将synchronized套在了函数上,也就不会每次调用都对整个函数同步了,提高了资源的利用率。但是同样存在失效的情况。

存在失效的原因(对使用volatile的解释)

因为JVM的加载顺序是一个无序状态,他可能进行过指令优化的重排操作,那这种情况就是我们不可控制的,而volatile起着不被忽略的作用,保证了我们的instance不被指令重排。这也就是他的优化方法。

(4) 静态内部类单例模式

public class Singleton {
    private Singleton(){}
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

    private static class SingletonHolder {
        private volatile static Singleton instance = new Singleton();
    }
}

这是最常用的方法,也是对DCL的一种升级。

优缺点和前面都差不多,就不再复述了。

模式分析

使用这个模式时说明系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。

缺点:

  • 单例类的扩展有很大的困难
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。
  • 如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失

工厂模式

工厂模式分为三种:

(1)简单工厂模式

(2)工厂方法模式

(3)抽象工厂模式

所以接下来将从这三种模式中进行分析和比较。

简单工厂模式

写法

/**
 * 工厂类
 */
public class Factory {
    public static Product createProduct(String type){
        Product product = null;
        switch (type){
            case "鸡翅":
                product = new ChickenWing();
                break;
            case "汉堡":
                product = new Hamburger();
                break;
        }
        return product;
    }
}

/**
 * 抽象产品类
 */
public abstract class Product {
    public abstract void use();
}

/**
 * 具体产品类
 */
public class Hamburger extends Product {
    @Override
    public void use() {
        System.out.println("汉堡制作完成");
    }
}

public class ChickenWing extends Product {
    @Override
    public void use() {
        System.out.println("鸡翅制作完成");
    }
}

模式分析

将类的创建细节与使用者隔离,使用者只需要知道对应的参数,将其送入工厂中即可完成创建。就比如我想吃汉堡了,那我就告诉工厂,汉堡这个关键参数,那么工厂就会将汉堡这个好吃的家伙送给我。但是这个工厂要注意,他可以制造鞋子、袜子、零食。。。你所能想到的想要找他做的他都得学会。

缺点:

  • 工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
  • 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。

工厂方法模式

写法

/**
 * 抽象工厂类
 */
public abstract class Factory {
    public abstract <T extends Product> T  createProduct(Class<T> clazz);
}

/**
 * 具体工厂类
 */
public class KFC extends Factory {
    @Override
    public <T extends Product> T createProduct(Class<T> clazz) {
        Product product = null;
        try{
            product = (Product) Class.forName(clazz.getName()).newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
        return (T) product;
    }
}

模式分析

与简单工厂模式不一样的地方在于我们创建了专门的工厂,也就是说比如我们今天想吃汉堡,但是同时有肯德基、麦当劳、汉堡王等好几家公司可以生产,就轮到了我们选择谁来进行制作的问题了。但是这样的模式依旧存在一个问题,那就是我们需要专门跑到肯德基、麦当劳又或者是汉堡王的门店去,我们才能点餐。

缺点:

在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。

抽象工厂模式

写法

public class FactoryProducer {
   public static AbstractFactory getFactory(String factory){
      if(factory.equalsIgnoreCase("KFC")){
         return new KFC();
      } 
      return null;
   }
}

模式分析

再次与上述的工厂方法模式进行比较,这次我们不需要再到肯德基门店去就可以买汉堡了,为什么呢?因为我们现在手上有饿了么,有美团了。我在搜索栏中输入了肯德基,他就告诉我了有这样的一个工厂,这样我们就能远程遥控获得这样的一个我们想要的油炸食品了。

缺点:

在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改

观察者模式

写法

/**
 * 抽象观察者类
 */
public interface IObserver {
    void update(String message);
}

/**
 * 抽象主题类
 */
public interface ISubject {
    void add(IObserver observer);
    void remove(IObserver observer);
    void notify(String message);
}

/**
 * 具体观察者类
 */
public class Observer implements IObserver {
    @Override
    public void update(String message) {
        System.out.println(message);
    }
}

/**
 * 具体主题类
 */
public class Subject implements ISubject {
    List<IObserver> list = new ArrayList<>();

    @Override
    public void add(IObserver observer) {
        list.add(observer);
    }

    @Override
    public void remove(IObserver observer) {
        list.remove(observer);
    }

    @Override
    public void notify(String message) {
        for(IObserver observer: list){
            observer.update(message);
        }
    }
}

模式分析

抛去模式中的接口类,就剩下了主题和观察者,这个模式的发生就是基于主题的变更与对观察者的通知。

这个模式在我的 helper 工具包中也有使用,就是基于对系统服务的监听,发现变化后,对订阅此变化的观察者们发出通知,并由观察者自己作出相应的动作。

缺点:

  1. 一个主题存在多个观察者,而通知的方式是通过轮询,这样的通知会有一定的时间消耗。
  2. 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。

代理模式

写法

静态代理

/**
 * 抽象主题类
 * 中心主题,买东西。
 */
public interface IShop {
    void buy();
}

/**
 * 真实主题类
 * 也就是我们购买者
 */
public class Person implements IShop {
    @Override
    public void buy() {
        System.out.println("购买");
    }
}

/**
 * 代理类
 * 持有被代理者
 */
public class StaticPurchase implements IShop {
    private IShop shop;

    WhoBuy(IShop shop){
        this.shop = shop;
    }
    @Override
    public void buy() {
        shop.buy();
    }
}

动态代理

/**
 * 动态代理类
 */
public class DynamicPurchase implements InvocationHandler {
    private Object object;
    DynamicPurchase(Object object){
        this.object = object;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = method.invoke(object, args);
        if(method.getName().equals("buy")){
            System.out.println("买上");
        }
        return result;
    }
}

/**
 * 客户端类
 */
public class User {
    public static void main(String[] args) {
        Shop person = new Person();
        DynamicPurchase dynamicPurchase = new DynamicPurchase(person);
        ClassLoader loader = person.getClass().getClassLoader();
        Shop purchase = (Shop) Proxy.newProxyInstance(loader, new Class[]{Shop.class}, dynamicPurchase);
        purchase.buy();
    }
}

模式分析

你可以看到上述文中和之前出现了不一样的地方,就是分为了两份代码,也就是动态代理和静态代理。那么就出现了这样的一个问题,何为静,何为动?

我们拿身边的代购举例,分为两种:

  1. 静就相当于我们需要找到专门的代理商,就是你微信里加的好友,你专门找到了他,然后跟他说想买Dior745,这样等他出国时就会帮你去买了。

  2. 动就是你只需要知道自己想要买Dior745,但是你这个时候手头没有静中的代理商,那你就找了淘宝,你只用知道自己想要的,不用再去思考代理商的问题了。也就是我们上文中所使用到的Proxy的代理技术。

缺点:

1.由于在客户端和真实主题之间增加了代理对象,因此 有些类型的代理模式可能会造成请求的处理速度变慢。 2. 实现代理模式需要额外的工作,有些代理模式的实现 非常复杂。

适配器模式

类适配器:

对象适配器:

写法

类适配器

public interface MP4{
    void play();
}

public class MP4Player implements MP4{
    public void play(){
        // doSomething
    }
}

public interface Player{
      void action();
}

public class Adapter extends MP4Player implements Player{
    public void action(){
        play();
    }
}

对象适配器

public class PlayerAdapter implements Player{
    public MP4 mp4;
    
    public PlayerAdapter (MP4 mp4){
        this.mp4 = mp4;
    }     

    public void action(){
        if(mp4!= null){
             mp4.play();
        }
    }
}

模式分析

适配器模式简单来说就是为两个互不兼容的两者提供了合作的桥梁。

想一想我们在Android中使用的RecyclerView中为了进行数据的适配是不是都会加上一个Adapter,因为我们从网络获取的数据Bean是无法直接和XML文件中的每个View需要填充的数据项进行对应的。而适配器就是将两者进行了沟通协作。

类适配器模式和对象适配器模式的区别是什么?

类适配器使用了继承的方式来完成、对象适配器使用了依赖的关系来完成任务。拿代码来说的话就是类适配器继承了MP4Player,而对象适配器依赖就是MP4这个接口类的使用。

缺点

  • 类适配器模式 对于Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口。
  • 对象适配器模式 与类适配器模式相比,要想置换适配者类的方法就不容易。如果一定要置换掉适配者类的一个或多个方法,就只好先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。

策略模式

写法

/**
 * 上下文
 * 根据传入策略给出解决方法
 */
public class Context {
    private Strategy strategy;
    Context(Strategy strategy){
        this.strategy = strategy;
    }

    void solve(){
        strategy.solve();
    }
}

/**
 * 两种策略方法
 * 1. 普通用户
 * 2. Vip用户
 */
public class CustomStrategy implements Strategy {
    @Override
    public void solve() {
        System.out.println("普通用户");
    }
}

public class VipCustomStrategy implements Strategy {
    @Override
    public void solve() {
        System.out.println("Vip用户");
    }
}

/**
 * 抽象策略角色
 */
public interface Strategy {
    void solve();
}

模式分析

在掘金平台上看到最多的模式,没有之一。那我们是如何通过策略模式来干掉我们日常开发中的if-else的呢?

就拿上面的代码来说好了,如果使用if-else来完成任务。

if (level == "普通用户") {
    // ...
}else if (level == "Vip用户") {
    // ...
}

显然的很麻烦,而且会随着逻辑的复杂化而繁琐起来,就比如我今天Vip用户要分等级了,白银应该9折,黄金8折。。。。还ok,在Vip用户里继续加入if-else,第二天产品经理说要白银有一、二、三段了。你是不是想砍死你的产品经理了??

而策略模式去完成的时候只是多了一个或者多个类,虽然类增多了,但是从至少让我们的代码从屎山变成了一堆堆屎。

缺点:

  1. 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
  2. 策略模式将造成产生很多策略类。

参考文献