GOF23--23种设计模式(一)

发布时间 2023-12-04 21:49:28作者: 回忆也交给时间

一.什么是设计模式

设计模式(Design  Pattern)是前辈们对代码开发经验的总结,是解决一系列特定问题的套路。

它不是语法规定,而是一套用来提高代码复用性,可读性,可维护性,稳健性,安全性的解决方案

设计模式的雏形:

1995年,GOF(Gang  of  Four,四人/四人帮)合作出版了《设计模式:可复用的面向对象软件的基础》一书,共收集了23种设计模式,从此有了设计模式的历程碑,人称【GoF设计模式】

设计模式的本质是面向对象设计的实际应用,是对类的封装,继承,多态,以及类的关联,和组合关系的充分理解

使用设计模式有以下优点:

  • 可以提高程序员的思维能力,编程和设计能力
  • 使得程序设计更加标准化,代码编制更容易加工,从而缩短软件开发周期
  • 使设计代码重用性高,可读性高,可靠性高,灵活性,可维护性强

OOP的七大原则:

开闭原则:对扩展开放,对修改关闭

  • 指的是在面向对象的设计中,当有新的需求时,不会优先改变源码,而是通过其它方式(继承,多态等)在源码的基础上拓展新功能

里氏替换原则:继承必须确保父类的所拥有的性质在子类依旧成立

  • 指的是在程序设计中,对于子类继承父类,子类中父类的属性和方法都能正常使用,子类需要新的需求就自己写,不要直接重写父类的方法,如果为了重写父类已有的方法而继承,对于程序的复用性会大打折扣

依赖倒置原则:面向接口编程,不要面向实现编程

  • 指的是在程序设计中,不应该力求于怎么实现这个功能,应该先思考有那些方法,各自负责什么,实现的细节交由实现类,抽象功能交给接口,更深层次就是面向抽象编程,不要面向实现编程

单一职责原则:控制类的粒度,将对象解耦,提高其内聚性

  • 指的是一个类就专注于实现好一个功能就行了,就像一个方法就实现一个细节一样,如用户登录,想要一个方法负责密码校对又负责检测用户名是否存在,就是一个方法干了多件事,可以把检测用户是否存在抽象为另一个方法,然后调用它,这样类的粒度就低了,粒度越高,代码越可能出现问题

接口隔离原则:为各个类建立它们专需的接口

  • 指的是在程序设计的时候,需要对一个接口对应一个或多个实现类,它们负责的模块可以很小,但是需要专一,不要多个功能都冗余在一个接口内部,应该实现专一功能,然后可以多个实现类来实现更小的细节

迪米特法则:只与你的朋友交谈,不和陌生人说话

  • 指的是两个类需要交流时应该通过一个中间类,不要让它们两个类直接交流,如用户登录时需要密码校对(A类的功能),校对前需要进行用户名检测是否村在(B类的功能),它们之间有耦合关系,但是程序设计中不能将B类塞到A类中,而是需要一个C类,将B类和A类组合,然后实现此功能,好处是,A类B类保持纯粹,坏处是多了一个开销C类

合成复用原则:尽量优先使用组合和聚合的方式实现类之间的关系,其次才考虑继承来实现

  • 指的是在类的关系中多用组合和聚合的方式设计类,组合优于继承,如果你只想使用父类的方法,而很少或根本不再设计新的方法属性,就肯定要使用组合,如果是需要大面积更改父类方法,或者重构父类,则使用继承

注意:OOP的七大原则,多用于设计阶段,需要分清设计和实现的区别

 二.工厂模式

实现了创建者和调度者的分离

原来的调度者即是创建者,类就在自己的项目中,且可看源码,所以要使用的时候可以直接new出来,这种方式创建对象需要自己十分的了解这个类,如需要哪些参数,清楚内部的实现细节

在大型项目的设计中,都是面向接口编程,对于调度者,它只知道此接口的内容,和有一些实现类,并不知道实现类的具体细节,如果自己创建对象,很大概率会被抽象接口给整蒙,所以工厂模式出现了,它用于实现对象的创建,创建对象的细节都由工厂模式解决(也就是架构师),普通开发者只用知道自己使用的实现类是那个工厂提供的,然后在工厂内拿取对象,不必自己创建,而只是利用工厂调度

详细工厂的分类:

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式

理论上,工厂模式满足:开闭原则,依赖倒转原则,迪米特法则

但是,实际工作中以效率和业务开发为主,不一定完全满足,这取决于效率和理论的冲突

工厂模式的核心本质:

实例化对象不在使用new关键字,用工厂代替

将选择实现类,创建实现类对象统一管理和控制,从而将调用者和实现类解耦

简单工厂模式

简单工厂模式也叫静态工厂模式,指的是工厂中的代码块都是写死的,动态的拓展类需要在工厂中新增代码块来完成对象的创建

接口,Animal:

public interface Animal {
    void getName();
}

 

通过此接口拓展出的实现类:

  • cat类
public class Cat implements Animal{
    @Override
    public void getName() {
        System.out.println("猫类,实现Animal接口");
    }
}

 

  • dog类
public class Dog implements Animal{
    @Override
    public void getName() {
        System.out.println("狗类,实现Animal类");
    }
}

 

普通的创建对象的方式,通过new关键字实现:

//普通的创建对象方式
Cat cat = new Cat();
Dog dog = new Dog();

 

这种方式使用的前提是创建者对类的内部结构要熟悉,清楚需要什么参数才能创建对象,我们例子的实现类简单,肯定用new关键字很适用,但是这一期主要讲工厂模式,所以我们看看工厂模式怎么创建对象

简单构造一个简单工厂来创建对象(重理解):

public class AnimalFactory {
    public static Animal getAnimal(String name){
        if (name.equals("cat")){
            return new Cat();
        } else if (name.equals("dog")) {
            return new Dog();
        }else {
            return null;
        }
    }
}

 

如上就是普通工厂的写法,它是讲已有的类先写入工厂中,这就导致工厂的实现类被写死了,如果新增一个拓展类,就需要改变普通工厂的源码,这很显然不符合开闭原则

工厂模式拿取对象:

//工厂模式创建对象
Animal cat1 = AnimalFactory.getAnimal("cat");
Animal dog1 = AnimalFactory.getAnimal("dog");

 

新建一个Mouse实现类:

public class Mouse implements Animal {
    @Override
    public void getName() {
        System.out.println("老鼠类,实现于Animal类");
    }
}

 

需要改变普通工厂模式的代码:

public class AnimalFactory {
    public static Animal getAnimal(String name){
        if (name.equals("cat")){
            return new Cat();
        } else if (name.equals("dog")) {
            return new Dog();
        }else if (name.equals("mouse")) {
            //新增的拓展实现类
            return new Mouse();
        }else {
            return null;
        }
    }
}

 

每次新增拓展类都需要改变普通工厂类的原因:普通工厂是拿取对象的必经之路,是和其它实现类的唯一联系

普通工厂模式生产对象略图:

 工厂方法模式

工厂方法模式支持实现类的横向拓展,它在普通工厂模式的基础上,增加工厂模式接口,对于每个实现类有专门的接口,

也就是说实现类,实现接口的具体细节,而工厂实现类实现的是工厂模式的创建对象

  • 优点是可以横向拓展业务,不需要改变已经有的工厂模式来融入
  • 缺点是代码量直接翻倍,冗余比较大

接口Animal:

public interface Animal {
    void getName();
}

 

Animal工厂接口:

public interface AnimalFactory {
    Animal getAnimal();
}

 

接口实现类:

public class Cat implements Animal {
    @Override
    public void getName() {
        System.out.println("猫类,实现Animal接口");
    }
}

 

工厂接口实现类:

public class CatFactory implements AnimalFactory{
    @Override
    public Animal getAnimal() {
        return new Cat();
    }
}

 

如上,每个实现类都有它专有的工厂实现类,使得每个实现类都是专门的工厂来加工的,它们各个工厂实现类都是独立存在的互相解耦,所以要创建对象现在就需要去找它们对应的工厂

这样构建工厂的好处是,横向的新增业务,如果现在新增一个业务只需要实现类实现Animal接口,它对应的工厂实现工厂接口,和其它工厂是独立存在的,不需要改变已有的工厂

能实现横向拓展的关键在于,接口和工厂接口都不是关键路径了,而是约束实现类的组成

工厂方法模式创建对象:

//方法工厂模式拿取对象
Animal cat = new CatFactory().getAnimal();
Animal dog = new DogFactory().getAnimal();

 

工厂方法模式生产对象略图:

三.抽象工厂模式

抽象工厂模式也是工厂模式的一种,但是它的特点和普通工厂模式,工厂方法模式的机制都是不同的

抽象工厂模式围绕一个超级工厂,其它工厂的创建都是由这个超级工厂约束的

定义:抽象工厂模式提供了一个创建一系列相关或相互依赖对象的接口,无需指定它们具体的类

优点:

  • 具体产品在应用层隔离,无需关心创建细节
  • 将一个系列的产品统一到一起实现

缺点:

  • 产品簇新增产品困难
  • 增加了系统抽象性和理解难度

产品接口:

phone

//产品接口,具体的实现细节交给厂商
public interface PhoneProduct {
    void getPhoneName();
    void getNumber();
    void getProduct();
}

 

router

//产品接口,具体的实现细节交给厂商
public interface RouterProduct {
    void getRouterName();
    void getRouterNumber();
    void getRouterProduct();
}

 

抽象工厂接口,工厂都需要实现此接口:

//抽象工厂,所有的工厂都需要实现这个超级工厂
public interface AbstractFactory {
    PhoneProduct phone();
    RouterProduct router();
}

 

普通工厂:

XiaoMi:

public class MiFactory implements AbstractFactory{
    @Override
    public PhoneProduct phone() {
        return new MiPhone();
    }

    @Override
    public RouterProduct router() {
        return new MiRouter();
    }
}

 

HuaWei:

public class HWFactory implements AbstractFactory{
    @Override
    public PhoneProduct phone() {
        return new HuaWeiPhone();
    }

    @Override
    public RouterProduct router() {
        return new HuaWeiRouter();
    }
}

 

抽象工厂模式生产对象略图:

三种工厂模式总结

简单工厂模式:虽然某种程度上不符合设计模式,但是实际应用最多

工厂方法模式:不修改已有类的情况下,通过新增工厂实现类的拓展

抽象工厂模式:不可以新增产品,但是可以新增产品簇或者说,不建议修改已经写好的抽象工厂接口,但是实现抽象工厂接口的普通工厂可以横向拓展

四.单例模式

单例模式指的是在创建对象的时候,只允许全局存在一个对象,从而达到资源共享的目的

实现单例模式的方式一共有两种:

  • 饿汉式单例
  • 懒汉式单例

饿汉式单例

饿汉式单例的特点是将一个类的构造器私有化,不让外部的程序手动的创建对象

而这个类的对象则使用静态方法获取,由程序加载初始化的时候就开始创建,然后伴随程序的结束为止

//饿汉式单例模式
public class HungryInstance {
    //私有化构造器,不允许外部类任意创建对象
    private HungryInstance(){

    }
    //创建静态对象,在类初始化时就被创建对象
    private static HungryInstance hungry=new HungryInstance();
    //外部类利用方法拿取对象,不由外部类自主创建对象
    public static   HungryInstance getHungry(){
        return hungry;
    }
}

 

饿汉式单例模式有一个缺点,也就是此类的对象是静态的,它和程序加载顺序有关系,静态的代码块会和程序初始化一起加载,所以有可能此类如果所需空间很大但是使用不平凡,会白占很多空间

如我们此类需要申请一片内存空间:

private String[] s1=new String[1000];
private String[] s2=new String[1000];
private String[] s3=new String[1000];
private String[] s4=new String[1000];

 

如上,这片空间会在程序初始化就被占用,且一直存在到程序结束,如果这个单例本身使用很少,内存开销就很不合算

懒汉式单例

懒汉式单例也需要将构造器私有,避免外部类创建对象

懒汉式不是再使用静态属性来创建对象,而是通过方法调用,由方法创建

如果没使用此方法就并不会存在此对象,如果使用了此方法就创建一个对象

然后加一个检测机制,调用此方法时,如果对象存在就直接返回对象,避免创建,如果不存在,则当场创建一个

//懒汉式单例
public class LazyInstance {
    //私有化构造器,避免外部类创建对象
    private LazyInstance(){

    }
    private LazyInstance lazy;
    //y由调用方法创建对象,被调用才会被创建,没被调用对象就不存在
    public LazyInstance getLazy(){
        if (lazy==null){
             lazy = new LazyInstance();
            return lazy;
        }else {
            return lazy;
        }
    }
}

 

懒汉式单例也有自己的一个问题,那就是多线程的情况下,检测机制太简单,单例会被破坏

原因是上面方法创建对象的操作不是原子性,创建对象的过程:1.分配内存空间,2.执行构造方法,初始化对象,3.把对象指向这个空间

创建对象的顺序是123,132都可能,如果多个线程同时来拿对象只有还没进行到第3步,都会默认没有对象,但实际情况是已经有线程正在创建了,所以就会导致多个线程创建了多个对象

解决方式,加锁(synchronized)

    //由调用方法创建对象,被调用才会被创建,没被调用对象就不存在
    public static LazyInstance getLazy() {
        if (lazy == null) {
            //加上线程同步机制,当对象不存在时将此类资源锁住
            synchronized (LazyInstance.class) {
                if (lazy == null) {
                    lazy = new LazyInstance();
                    return lazy;
                }
            }
        }
        return lazy;
    }

 

加上同步机制后,在创建对象时,会将类资源锁住,先获得锁的线程就就去创建对象,其它线程只能等待此线程释放锁

当对象创建完成后,其它线程先后获得锁,但是对象此时已经被最先拿到锁的线程创建了,所以其它线程都不能创建对象而是直接返回已经创建好的对象

静态内部类单例

这是使用了Java静态内部类的特点,它可以直接拿到外部类的静态资源,然后又不会直接被初始化加载,它和饿汉式有异曲同工之妙

饿汉式是在程序加载时就初始化一个对象出来,而它需要在被调用时才能拿到对象,由于创建对象的类中,又是final修饰,所以在调用方法的时候不会多创建对象

//静态内部类
public class StaticClass {
    //私有化构造器
    private StaticClass(){

    }
    //返回静态内部类的属性
    public static StaticClass getInstance(){
        return InnerClass.sc;
    }
    //静态内部类负责创建外部类的对象
    public static class InnerClass{
        private static final StaticClass sc = new StaticClass();
    }
}

上面三种方式的缺点

只要有反射机制存在,以上三种方式创建对象都是不安全的

 反射机制使得私有的构造器依旧可以被拿到,反射机制面前就没有私有的属性了,我们可以使用反射机制来创建对象

//通过反射拿取类的构造器
Constructor<LazyInstance> lazy = LazyInstance.class.getDeclaredConstructor(null);
//设置构造器的熟悉为可访问
lazy.setAccessible(true);
//通过反射拿取构造器创建对象
LazyInstance lazy1 = lazy.newInstance();
LazyInstance lazy2 = lazy.newInstance();
//展示hashcode
System.out.println(lazy1);//LazyInstance@4554617c
System.out.println(lazy2);//LazyInstance@74a14482

 

如上,通过反射机制将构造器再次变为公有属性以后,已经可以通过外部类继续创建对象

所以这种基于类的单例模式大多都是不安全的,关键在于Java的反射机制使得构造器无法真正的私有化

但是如果有能拒绝反射机制的方式,阁下又如何应对呢?接下来的枚举类值得一看

枚举类单例

枚举类的特点:

枚举类的构造器都是私有的(无论是否显式表达,都是私有的),因此枚举类不能对外创建对象

can't deserialize enum" :不能通过反射拿取枚举类
枚举类直接拒绝反射机制,从根本上杜绝了反射更改构造器属性为公有的情况
public enum EnumInstance {
    //实例对象
    Instance;
    //私有构造器,不管是否显示私有化都是私有的,改为公有编译错误
    private EnumInstance(){

    }
    //拿取对象实例方法
    public EnumInstance getInstance(){
        return Instance;
    }
}

 试试用反射取改变构造器属性为公有

//枚举的构造器不是无参构造,Idea和JavaP命令都反编译为无参构造,而真正的构造器为参数为String和int
Constructor<EnumInstance> ei = EnumInstance.class.getDeclaredConstructor(String.class, int.class);
//设置构造器为公共属性
ei.setAccessible(true);
//通过构造器创建对象
EnumInstance e1 = ei.newInstance();
EnumInstance e2 = ei.newInstance();
//展示hashcode
System.out.println(e1);
System.out.println(e2);

指向如上代码报错:

 意思是不能使用反射创建枚举对象