常见的设计模式(一)单例和工厂设计模式

676 阅读8分钟

我最开始听到设计模式还在大学的课上,这段代码用了什么样的设计模式,实现了什么功能,觉得设计模式听起来就很厉害的一个东西,那我们就来看看设计模式到底是个什么样子的。

一、什么是设计模式

大家都在用设计模式,什么是设计模式呢?其实,设计模式是一种代码的写法,再说好听一点就是一种对问题的解决方案,本质是还是要落在代码的实现上。 那就介绍一下大家常听到23种设计模式吧。 设计模式分为:创建型, 行为型,结构型。

  • 创建型:为了对象的创建,为对象的创建的性能、灵活性提供方案的代码写法,如单例、抽象工厂、建造者、工厂、原型
  • 行为型:为了达到具体的业务目的的代码写法,如模板方法、命令模式、迭代器模式、观察者、中介者模式、备忘录模式、访问者模式
  • 结构型:通过对结构的设计达到性能、可扩展性、业务目的的代码写法,如适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式

设计模式不是绝对,都要结合业务场景来实现

看了这么多设计模式,那我们就来说一说我们最常用的设计模式吧。

二、单例模式

问题来了,什么是单例模式呢? 单例模式:一个类只有一个实例,并且可以自己自动创建实例。 单例顾名思义就是只有一个实例,而且这个实例必须是自己创建的,单例模式也是所有设计模式中使用最多也是最简单的模式。虽然简单,但是实现方式却是不少的。下面我们就来看看都有哪些实现方式吧。

2.1 饿汉式 和 懒汉式

public class SingleInstance {

    private SingleInstance() {}

    private static SingleInstance singleInstance = new SingleInstance();

    public static SingleInstance getInstance() {
        return singleInstance;
    }

}

只能自己创建实例,肯定需要把无参构造函数私有化。然后在自己创建一个实例。这就是饿汉式,为什么叫饿汉式呢?是因为外部并没有使用,它自己就已经把实例初始化好了,下面来看看懒汉式的写法,就会明白了。

public class SingleInstance {

    private SingleInstance() {}

    private static SingleInstance singleInstance = null;

    public static SingleInstance getInstance() {
        if (singleInstance == null) {
            singleInstance = new SingleInstance();
        }
        return singleInstance;
    }

}

懒汉式并不会在一开始就会初始化,而是在第一次调用的时候才初始化。基本上区别就是这样的,也是我们最常用的单例写法。

2.2 双重检查模式

我们先看代码实现

public class SingleInstance {

    private SingleInstance() {}

    private static SingleInstance singleInstance = null;

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

懒汉式在多线程的环境下,可能会造成多个实例对象,于是就有了上述双重检查模式在多线程下保持唯一实例。 第一次检查是为了判断实例有没有被初始化,避免进入加锁区域,损耗资源。第二次检查是为了避免重复创建实例。可能有多个线程都通过了第一次检查,在竞争创建实例的代码块,当某一个线程竞争成功,成功创建实例之后,保证其他线程不会再次创建实例,保证实例的唯一性。 但是这个写法并不能保证不出错。因为Java存在指令重排这种优化性能的方式。但是在多线程模式就容易出错。

new SingleInstance();

这个操作分为三个步骤。

  • 分配内存空间
  • 初始化对象
  • 对象指向内存空间 如果出现指令重排,那么为了优化性能,会先将对象指向内存空间,再去初始化对象。在多线程环境下,某个线程就会拿到没有被初始化对象,这样使用起来就会出错。 为什么会拿到没有被初始化的对象呢?当某个线程执行完对象指向内存空间这条指令后,其他线程竞争成功,此时singleInstance == null不被满足,就会获取到没有初始化的对象。 那么为了解决这个问题,我们就需要加上volatile关键字,来防止指令重排。

2.3 静态类模式

我们先来看代码实现。

public class SingleInstance {

    private SingleInstance() {}

    public static SingleInstance getInstance() {
      return SingleHolder.instance;
    }

    private static class SingleHolder {
        private static final SingleInstance instance = new SingleInstance();
    }
}

这种方法其实和饿汉式加载有点类似,那为啥这个和饿汉式有区别呢?因为他也兼顾了延迟加载和线程安全。外部类加载时,不会立即去加载内部类,只有当第一次使用时,才会去加载内部类,这就保证了延迟初始化。在多线程环境中虚拟机会保证一个类的构造器方法被正确地加载,同步;如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的init方法,其他线程都需要阻塞等待,直到活动线程执行init方法完毕,简单的说 在同一个类加载器下,一个类型只会被初始化一次。这就保证了线程安全。

这种实现有一个缺点就是不能外部传递参数,因为实例化是放在内部类中的

2.4 枚举模式

public enum SingleInstance {
	INSTANCE;
	public void doSomething() {
		
	}
}

值得一说的是,枚举也能保证唯一实例和线程安全。

2.5 集合模式

根据Key获取对象,多种相同属性的对象,注册到一个map中。 来看看代码实现吧。

 private static Map<String, SingleInstance> map = new HashMap<>();

    public static void register(String key, SingleInstance singleInstance) {
        if (!map.containsKey(key)) {
            map.put(key, singleInstance);
        }
    }

    public static SingleInstance getService(String key) {
        return map.get(key);
    }

通过map存储多个实例,直接读取,这也算是个单例的实现方式。

2.6 优点和缺点

优点: 单例模式的最大优点就是只有一个实例不占用过多的内存,当一个对象需要频繁的创建和销毁时,此时用单例模式是一个好的选择。 缺点:扩展困难,扩展的唯一办法就是修改代码。

三、工厂设计模式

3.1 简单工厂模式

工厂模式一般用于想要隐藏对象的创建过程,在对象类型不多的情况下。可以实现代码分离,结构清晰,实现简单。 我们先来看看代码实现吧。 首先我们需要一个接口来规定行为。

interface Phone {
    fun show()
    fun play()
}

再来几个实现类,实现我们想要的具体行为。

class AndroidPhone : Phone {
    override fun show() {
      println("this is AndroidPhone")
    }

    override fun play() {
        println("you can play android")
    }
}
class WindowsPhone : Phone {
    override fun show() {
        println("this is WindowsPhone")
    }

    override fun play() {
       println("you can play windows")
    }
}

最后建一个工厂类,用于生成对象的工厂。

class PhoneFactory {
    companion object {
        fun createPhone(type:String) : Phone {
            return when (type) {
                "windows" -> {
                    WindowsPhone()
                }

                "android" -> {
                    AndroidPhone()
                }

                else -> {
                    AndroidPhone()
                }
            }
        }
    }
}

这么一个简单的工厂模式就建立好了。 最后测试一下就好了。

fun main() {
    val androidPhone = PhoneFactory.createPhone("android")
    val windowsPhone = PhoneFactory.createPhone("windows")

    androidPhone.play()
    androidPhone.show()
    windowsPhone.play()
    windowsPhone.show()
}

3.2 工厂方法

对于想要扩展的业务场景来说,简单工厂模式就显得简单了起来,每次扩展都要去修改工厂类违反了开闭原则而且会导致工厂类庞大无比,工厂方法就只需要扩展就好了,遵循开闭原则。 我们来看看代码实现吧。

当然我们需要定义一个行为类和一个工厂类

abstract class PhoneStyle {
    abstract fun getStyle()
}
abstract class PhoneStyleFactory {
    abstract fun getStyle():PhoneStyle
}

为了实现具体的业务,我们还需要有具体的实现类。

class WindowsPhoneStyle:PhoneStyle() {
    override fun getStyle() {
        println("this is windows")
    }
}
class WindowsPhoneStyleFactory: PhoneStyleFactory() {
    override fun getStyle(): PhoneStyle {
        return WindowsPhoneStyle()
    }
}

这里演示就只实现一种吧。再来看看使用吧。

fun main() {
    val windowsPhoneStyleFactory = WindowsPhoneStyleFactory()

    val style = windowsPhoneStyleFactory.getStyle()
    style.getStyle()
}

如果我们要实现其他的,只需要重新继承抽象类,实现具体类就可以了。使用具体工厂类来完成相关业务。一个工厂只做一件业务。

但是当业务多了起来的话,我们的工厂类就会越发多了起来。

3.3 抽象工厂

抽象工厂和工厂方法的不同的就是,它规定可以一个工厂可以做一系列的业务,工厂方法是一个工厂只做一个业务。所以他的实现和工厂方法有些类似。 来看看代码实现吧。 同样的我们需要抽象类,这里沿用之前的

abstract class PhoneStyle {
    abstract fun getStyle()
}
abstract class PhoneExt {
   abstract fun show()
}
abstract class WindowPhoneFactory {
   abstract fun getStyle():PhoneStyle
   abstract fun getExt():PhoneExt
}

再看具体的实现类

class WindowsPhoneStyle:PhoneStyle() {
    override fun getStyle() {
        println("this is windows")
    }
}
class WindowPhoneExt : PhoneExt() {
    override fun show() {
        println("this is windows")
    }
}
class WindowPhone23 : WindowPhoneFactory() {
    override fun getStyle(): PhoneStyle {
        return WindowsPhoneStyle()
    }

    override fun getExt(): PhoneExt {
       return WindowPhoneExt()
    }
}

这样同系列的Phone会在一个工厂里被执行出来,当然这里也可以换成不同的如其它相关的,不用非要聚合在一起,设计模式只是一种写法,遵循这种规范就可以了。具体实现可以由业务而来。

fun main() {
    val windowsPhoneStyleFactory = WindowPhone23()

    val style = windowsPhoneStyleFactory.getStyle()
    val ext = windowsPhoneStyleFactory.getExt()
    ext.show()
    style.getStyle()
}

在调用代码时,同样不用关心内部是如何实现的。而且扩展也方便,这里的扩展时针对同一类型的如上述代码所写的style 和 ext ,如果想要扩展新的业务,那么就需要去修改抽象工厂,如果抽象工厂有多个实现类的话,那么这个多个实现类都要修改实现新的业务,这严重违反了开闭原则。

四、总结

所有的工厂模式都为了隐藏内部实现,让对象的实例化处在自己的控制之下,而不是暴露给使用者。 简单工厂模式实现了这样的诉求,但是随着业务扩展,内部代码就会变得庞大无比,而且违反了开闭原则。 工厂方法模式解决了工厂类会庞大的问题也遵循了开闭原则,但是工厂类会越来越多。抽象工厂模式是把相同的类型的工厂聚合到了一起(一个工厂可以负责多个相同类型业务),在维护和扩展这些时会很简单,但是需要新增一个新的类型时,违反了开闭原则,修改起来也很繁琐。 所以选择合适的实现方法是比较重要的。