java---设计模式(一)

1,852 阅读15分钟

设计模式

1、设计原则

 1.1、开闭原则

开闭原则 讲的是推荐对类进行扩展而增加功能而不是直接修改原来已经实现的类,提高系统的可用性和可维护性。

 光讲书面表达不好理解,我们不妨假设这样的一种情况:某水果店进购了一些苹果,每斤 10 元。

public class Apple {
    private Double price = 100d;
    public double price() {
        return 100d;
    }
}

 卖了一段时间之后老板说打九折来搞促销,于是你把价格给改了。

public class Apple {
    private Double price = 90d;
    public double price() {
        return price;
    }
}

 这次老板又进了一批新鲜的苹果,给你说新鲜的按原件卖,剩下的继续促销。这时候你突然想起来之前的修改价格时候将原价给涂了,忘记是多少钱了,于是你就瞎报了一个价格导致老板倒闭了。

 在我们现在的复杂的系统中,一个类方法可能会有很多功能调用,这时候想要为某个功能增加新的处理逻辑,直接在原代码上进行修改的话,就可能导致其他调用的地方处理失败,这时候就可以进行功能的拓展来专门使用,这样就不会引起其他业务问题。

public class DiscountApple extends Apple{
    @Override
    public double price() {
        return super.price() * 0.9;
    }
}

 1.2、依赖倒置

依赖倒置 的官话是抽象不应该依赖细节,细节应该依赖于抽象,也就是需要面向抽象编程。

 假设存在这样一种场景:我们手里有两本书,但是不确定要用哪一本,需要通过指定的参数来决定,最开始我们可能会这样写。

public class People {
    public static void main(String[] args) {
        People people = new People();
        people.readBook("java");
        people.readBook("python");
	}
    
    public void readBook(String bookName) {
        if ("java".equals(bookName)) {
            //  处理逻辑
        } else if ("python".equals(bookName)) {
            //  处理逻辑
        }
    }
}

 但是现在手里面多出来了一本 C ,这时候我们就得去重写实现逻辑了,也就是需要修改我们的底层代码,就可能造成其他调用业务的错误,所以底层方法的处理逻辑应该尽可能的不变动,由调用放来进行逻辑的划分。

//	通用方法
public abstract class Book {
    private String bookName;
    public Book(String bookName) {
        this.bookName = bookName;
    }
    public abstract void readBook();
}
//	实现
public class JavaBook extends Book{
    public JavaBook(String bookName) {
        super(bookName);
    }

    @Override
    public void readBook() {
        //  处理逻辑
    }
}
//	实现
public class PythonBook extends Book{
    public PythonBook(String bookName) {
        super(bookName);
    }

    @Override
    public void readBook() {
        //  处理逻辑
    }
}
//	使用
public class People {
    public static void main(String[] args) {
        People people = new People();
        people.readBook(new JavaBook("java"));
        people.readBook(new PythonBook("python"));
    }

    public void readBook(Book book) {
        book.readBook();
    }
}

 这样来进行设计的话,如果出现新的功能拓展,只需要实现新的产品来进行业务调用即可,修改的地方来到了调用者方面,不会去修改底层的实现逻辑,减少类间耦合性、提高系统稳定性。

 1.3、单一职责

单一职责 讲的是 接口方法 的功能要尽可能的单一,也就是尽量设计原子功能,组合实现功能复用。

 现在需要设计一个动物类,要包含他们的特有属性,学过依赖倒置之后我们懂得要先设计整体框架,也就是接口方法设计。

public interface Animal{
    public void fly();
    public void eat();
    public void swim();
}

 接口设计完成,具体的类就只需要实现总接口复写实现具体的功能接口即可。

public class Dog implements Animal{
    @Override
    public void fly() {
        //	具体逻辑实现
    }
    @Override
    public void eat() {
        //	具体逻辑实现
    }
    @Override
    public void swim() {
        //	具体逻辑实现
    }
}

 这样我们就发现一个问题,其实日常中我们见到的狗是不会飞的(哮天犬另当别论),实现了 fly 这个方法也没有什么作用,说明我们的接口设计得不是很好,需要进行重构将原子功能分开,实现类只需要按照具体需求进行多重实现即可。

public abstract Animal{
    public abstract void eat();
}

interface Swim{
    public void swim();
}

interface Fly{
    public void fly();
}

class dog extends Animal implements Swim{
    @Override
    public void eat() {
        //	具体逻辑实现
    }
    @Override
    public void swim() {
        //	具体逻辑实现
    }
}

 但是这个原则也存在很大的毛病,将每个功能都完成拆开之后将会导致类的数量增加,增加了内存负荷也增大了系统的复杂度,所以需要按照实际的情况进行合适的功能拆分,原则只是一种规范指导,但不是必要的。

 1.4、接口隔离

接口隔离 讲的是用多个专门的接口,而不是使用一个总接口,也就是需要将不同功能的接口分开进行设计,和 单一职责 是一个道理,在前者的基础上才能对后者进行规范,参照前面的例子就可以。

 1.5、迪米特法则

迪米特法则 说的是一个类只需要知道和自己相关的,强调只有在业务上有交集的类才允许交叉使用。例如经理只需要听会计汇报经费情况,经费实际管理只会与会计产生交集,而不会直接和经理打交道,也就是说在经理这个类中不应该出现经费这个实体。

public class Manager{
    public void check(Accountant accountant) {
        accountant.check();
    }
}

class Accountant{
    private Fund fund;
    public void check() {
        fund.caculate();
    }
}

 1.6、里氏替换

里氏替换原则 是在子类、父类之间的关系,父类能够使用的地方也一定能用其子类去替换,这就强调了子类在进行继承扩展时不能把父类已经实现的方法覆盖了,不然替换的时候无法完成功能的正常。

public class People{
    public void eat() {
        //	业务处理
    }
}

class Child extends People{
    public void eat() {
        super.eat();
    }
    public void childEat() {
        //	业务处理
    }
}

 1.7、合成复用

 这个就很显而易见了,强调多利用类的组合来实现功能的拓展,而不是使用继承来拓展。如果一个类进行很多次的功能拓展的话就会导致他的类继承级别非常的深,对于后期的维护调整很麻烦。而使用组合方式的话,需要替换功能的之后直接将组合进来的接口用其另外的实现类替换即可,和用叉子吃饭、勺子吃饭是一个道理。


2、单例模式

单例模式(Singleton Pattern) 是指确保一个类在任何情况下都绝对只有一个实例,并且提供一个全局访问点。是一种 创建型 模式,在日常生活中应用非常广泛, 例如 j2EE 标准中的 ServletContext、ServletContextConfig,Spring 框架应用中的 ApplicationContext、数据库连接池的表现也都是单例模式。

 2.1、饿汉式单例

 见名知意就是说在类加载的时候就进行实例的创建,绝对的线程安全,不存在访问的安全问题。但是他也存在很大的问题,如果一个系统中存在大量的单例,而且使用都比较靠后,这样一开始就创建对象实例会给系统的内存带来很大的压力,浪费大量内存 ''占着茅坑不拉屎''。

public class HungrySingleton{
    private static final HungrySingleton hungrySingleton;
    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton() {}
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

 2.2、懒汉式单例

 针对饿汉式存在的缺点,演化出了 懒汉式单例 写法,特点是类只有在需要使用的情况下会进行实例初始化。

public class LazySingleton{
    private static LazySingleton lazySingleton;
	private LazySingleton() {}
    public static LazySingleton getInstance() {
        if(lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

 但是这样的话就会有一点缺陷,多线程情况下就会导致创建多个实例,就违背单例的原则。如果说线程 A 判断为空进来刚开始创建对象时间片就被切给线程 B ,线程 B 检查时发现对象也为空进入分支创建对象,这样实例就被多次创建了。

 代码存在安全隐患并且不能自己优化,就只能从代码层面添加线程安全的手段,最简单的方式就是直接使用 synchronized 字段来添加互斥锁,一次只能由一个线程进入实例创建的代码块,也就是所谓的双重检查锁模式。

public class LazySingleton{
    private static LazySingleton lazySingleton;
	private LazySingleton() {}
    public static LazySingleton getInstance() {
        if(lazySingleton == null) {
            synchronized(LazySingleton.class) {
                if(lazySingleton == null) {
                	lazySingleton = new LazySingleton();    
                }
            }
        }
        return lazySingleton;
    }
}

 2.3、内部类单例

 使用上锁方式来确保线程安全固然有效,但是还是会存在线程竞争时的资源浪费,可以使用静态内部类必须只有在使用时才会加载的原理来进行优化,这样就兼顾了饿汉式的内存缺陷和懒汉式的安全缺陷。

public class LazySingleton{
    private LazySingleton() {}
    public static LazySingleton getInstance() {
        return Lazy.instance;
    }
    private static class Lazy{
        private static final LazySingleton instace = new LazySingleton();
    }
}

 2.4、反射破坏单例

 单例的本质就是将构造函数进行私有化,也就防止了其他类在外部进行创建类实力,但是在 java 中有一群 “变态” 可以无视权限规则来访问类的信息,这就是反射,也就是当我们使用反射来创建单例类的实例时,还是可以创建的,如果想要阻止反射创建的话,就可以在构造参数中抛出异常,这样反射是就会出错而禁止创建对象实例。

public class LazySingleton{
    private LazySingleton() {
        if(Lazy.instance != null) {
            throw new RuntimeException("禁止创建多个对象实例");
        }
    }
    public static LazySingleton getInstance() {
        return Lazy.instance;
    }
    private static class Lazy{
        private static final LazySingleton instace = new LazySingleton();
    }
}

 2.5、序列化破坏单例

 一个单例对象创建好之后,有时需要将对象序列化然后写入磁盘,下次使用的时候再从磁盘中读取并进行反序列化,将其转换为内存对象。但是一般的反序列化对象会被重新分配内训,即重新创建实例,这样就违背了单例的目的。

 解决方法就是实现序列化接口并重写 readResolve 方法,通过追踪反序列化的 JDK 源码可知,虽然反序列化之后的单例还是存在,但是实际在过程中创建了两次,只是新创建额对象实例没有进行返回,也不是一个根本的方法。

public class LazySingleton implements Serializable{
    private static LazySingleton lazySingleton;
	private LazySingleton() {}
    public static LazySingleton getInstance() {
        if(lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
    private Object readResolve() {
        return lazySingleton;
    }
}

 2.6、枚举式单例

 先看看具体实现我们再来分析他的实现原理。

public enum EnumSingleton{
    INSTANCE;
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object object) {
        this.data = data;
    }
    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}

 通过反编译字节码文件我们会发现,枚举是单例模式在静态代码块中就会给 INSTANCE 进行赋值,和饿汉式一个模子;再回去分析 JDK 源码中关于反序列化的那一部分,会发现枚举类型可以通过类名和对象类找到一个唯一的枚举对象,也就是说不会被重复加载;枚举只提供一个 protected 的构造器,而且 JDK 不支持枚举的反射创建,也就避免反射违背单例的状态。解决之前单例创建的所有问题,不妨不碍于是一个 优雅 的创建方式。


3、工厂模式

 3.1、简单工厂

 简单工厂模式是指由一个 工厂 对象决定创建那种产品的实例,客户端只需要传入想要创建实例的参数,工厂就会返回对应的创建实例,隐藏了具体的实现过程,和名字一样适用于创建简单的、对象较少的场景,但是它并不属于 23 种设计模式。而且所有的创建都集中在一个工厂中,使得工厂的职责变重,不利于扩展。

  • 产品

    public interface IBook {
        void read();
    }
    
    public class JavaBook implements IBook {
        @Override
        public void read(){
            //	业务逻辑
        }
    }
    
    public class PythonBook implements IBook {
        @Override
        public void read() {
            //	业务逻辑
        }
    }
    
  • 工厂

    public class BookFactory {
        public static IBook create(Class<? extends IBook> clazz) {
            if(clazz == null) return null;
            try {
                return clazz.newInstance();
            } catch(Exception e) {
                e.printStackTrace();
            }
        }
    }
    

 简单工厂在 JDK 也有很多都得应用场景,例如我们经常使用的日志 logback 创建日志对象时用的就是简单工厂进行创建的,它内部重载了 getLogger 就是用类型名来进行创建的。

 3.2、工厂方法

 是指创建一个对象的接口,但让实现这个接口的类来实例化具体的哪个类,让类的实例化延迟到子类中进行,相比于简单工厂方式,用户只需要知道哪个工厂可以创建哪种产品,然后交给工厂去创建就可以了。如果需要添加新的产品进来,直接添加具体实现类就行,不会影响到其他代码实现。

  • 顶层接口

    public interface IFactory {
        IBook create();
    }
    
  • 子类工厂实现

    public class JavaFactory implements {
        public IBook create() {
            return new JavaBook();
        }
    }
    
    public class PythonFactory implements {
        public IBook create() {
            return new PythonBook();
        }
    }
    

 工厂的优点很明显,能够隐藏实例的具体创建过程等细节,添加新的产品很方便,直接实现接口返回具体的产品就可以了。其缺点也显而易见,如果工厂实现很多,就会导致类数量增加,增加了系统的复杂度,同时也提高了系统的理解难度,而且只适用于创建单一的种类的产品。

 3.3、抽象工厂

 抽象工厂实际上就是在工厂方法的基础上解决了只能生产一类产品的缺点,具体实现就是在顶层接口制定规则时将所有需要生产的产品都声明一个方法,交有具体的工厂实现子类去处理。在很多人对与抽象工厂的讲解中,都会出现产品族和产品等级这个例子,其实产品族就可以看作是一个实现了抽象工厂的具体工厂,他可以生产所有的产品;而产品等级就是两个不同的实现类生产的同一类产品,他们之间是一个平行的关系。

  • 抽象工厂

    public interface IFactory {
        IBook createBook();
        IVideo createVideo();
    }
    
  • 子类工厂

    public class JavaFactory implements IFactory{
        public IBook createBook() {
            return new JavaBook();
        }
        public IVideo createVideo() {
            return new JavaVideo();
        }
    }
    
    public class PythonFactory implements IFactory{
        public IBook createBook() {
            return new PythonBook();
        }
        public IVideo createVideo() {
            return new PythonVideo();
        }
    }
    

 最大的优点就是可以创建不同的产品,但是同样存在着结构难理解、拓展产品困难的问题,一旦父类的规则改变,之前所有的实现子类都需要进行改变,违背了开闭原则。但是只要不是频繁变动修改,周期性的维护变动还是可以被支持的。


4、原型模式

原型模式 是指原型实例指定创建对象的种类,并且通过拷贝这些原型实例的数据创建新的对象,属于创建型模式。简单的来说就是将一个对象重新赋值一份出来,其实和 new 个对象出来进行赋值就行,但是那样显得比较孬,也不太美观。

 如果我们在方法内部就实现一个克隆的方法,每次调用都会返回一个新的对象,且对象的数据和当前实例是一致的,这个就是所谓的原型模式,解决的是大量重复对象的创建。

  • 接口定义

    public interface IPrototype<T> {
        T clonePrototype();
    }
    
  • 子类实现

    public class JavaPrototype implements IPrototype{
        private String name;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        @Override
        public JavaPrototype clonePrototype() {
            JavaPrototype prototype = new JavaPrototype();
            prototype.setName(this.name);
            return prototype;
        }
    }
    

 这样的话当前实例只要调用一次 clonePrototype 方法就会返回一个新的对象,且这个对象还有着和当前实例一样都得数据内容。不过这种实现方式也有点儿问题,如果当前实例内部字段存在其他对象的引用,那么产生的新实例也是共享的同一个对象引用,这样就没有达到 重新复制 的效果,这也是 java 中 浅拷贝深拷贝 的区别。

 解决这个问题的方法有很多,其中一个是每个引用对象都去实现 Cloneable ,来进行克隆,但是这样的话每个应用类型都必须实现,很麻烦。简单一点就是让当前类重新序列化回来,那样的话只要没有复写序列化的 readResolve 方法,那么返回的就会是一个新的内存数据对象。介于 JDK 已经实现了 Cloneable 来帮助我们进行克隆,那么我们也不要自定义个接口方法了,直接使用内置对象来实现即可。

public class JavaPrototype implements Cloneable, Serializable {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public JavaPrototype clone() {
        try {
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            ObjectOutputStream output = new ObjectOutputStream(outputStream);
            output.writeObject(this);
            ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
            ObjectInputStream input = new ObjectInputStream(inputStream);
            return (JavaPrototype) input.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}

 优点是基于二进制流的拷贝比直接用 new 创建对象的速度要快,有不少的性能提升;缺点是要克隆的类需要实现一个克隆方法,而且这个方法处于类的内部,不能够直接进行修改。而且如果有很多级的引用嵌套的话还需要支持深拷贝才行。