• Java常用设计模式


    前言

    设计模式是对大家实际工作中写的各种代码进行高层次抽象的总结,其中最出名的当属 Gang of Four (GoF)的分类了,他们将设计模式分类为 23 种经典的模式,根据用途我们又可以分为三大类,分别为创建型模式、结构型模式和行为型模式。

    设计模式的作用:

    • 提高代码复用率,降低开发成本和周期;
    • 提高代码可维护性、可拓展性;
    • 使代码更加优雅、更容易被他人理解。
       

    设计模式的类型总共分为:3大类、23种具体设计模式,具体如下:

    在设计模式进行设计时需要遵循以下七个原则:

    第一大类:创建型模式

       创建型模式的作用就是创建对象,说到创建一个对象,最熟悉的就是 new 一个对象,然后 set 相关属性。但是,在很多场景下,我们需要给客户端提供更加友好的创建对象的方式,尤其是那种我们定义了类,但是需要提供给其他开发者用的时候。

    1.1 简单工厂模式

      和名字一样简单,非常简单,直接上代码吧。

    public class FoodFactory {

        public static Food makeFood(String name) {

            if (name.equals("noodle")) {

                Food noodle = new LanZhouNoodle();

                noodle.addSpicy("more");

                return noodle;

            else if (name.equals("chicken")) {

                Food chicken = new HuangMenChicken();

                chicken.addCondiment("potato");

                return chicken;

            else {

                return null;

            }

        }

    }

    其中,LanZhouNoodle 和 HuangMenChicken 都继承自 Food。

    简单地说,简单工厂模式通常就是这样,一个工厂类 XxxFactory,里面有一个静态方法,根据我们不同的参数,返回不同的派生自同一个父类(或实现同一接口)的实例对象。

    我们强调职责单一 原则,一个类只提供一种功能,FoodFactory 的功能就是只要负责生产各种 Food。

    1.2 工厂模式

    简单工厂模式很简单,如果它能满足我们的需要,我觉得就不要折腾了。之所以需要引入工厂模式,是因为我们往往需要使用两个或两个以上的工厂。

     展开源码

    其中,ChineseFoodA、ChineseFoodB、AmericanFoodA、AmericanFoodB 都派生自 Food。

    客户端调用:

    1

    2

    3

    4

    5

    6

    7

    8

    public class APP {

        public static void main(String[] args) {

            // 先选择一个具体的工厂

            FoodFactory factory = new ChineseFoodFactory();

            // 由第一步的工厂产生具体的对象,不同的工厂造出不一样的对象

            Food food = factory.makeFood("A");

        }

    }

    虽然都是调用 makeFood("A")  制作 A 类食物,但是,不同的工厂生产出来的完全不一样。

    第一步,我们需要选取合适的工厂,然后第二步基本上和简单工厂一样。

    核心在于,我们需要在第一步选好我们需要的工厂 。比如,我们有 LogFactory 接口,实现类有 FileLogFactory 和 KafkaLogFactory,分别对应将日志写入文件和写入 Kafka 中,显然,我们客户端第一步就需要决定到底要实例化 FileLogFactory 还是 KafkaLogFactory,这将决定之后的所有的操作。

    如图:

     

    1.3 抽象工厂模式

    当涉及到产品族 的时候,就需要引入抽象工厂模式了。

    一个经典的例子是造一台电脑。我们先不引入抽象工厂模式,看看怎么实现。

    因为电脑是由许多的构件组成的,我们将 CPU 和主板进行抽象,然后 CPU 由 CPUFactory 生产,主板由 MainBoardFactory 生产,然后,我们再将 CPU 和主板搭配起来组合在一起,如下图:

    这个时候的客户端调用是这样的:

    // 得到 Intel 的 CPU

    CPUFactory cpuFactory = new IntelCPUFactory();

    CPU cpu = intelCPUFactory.makeCPU();

    // 得到 AMD 的主板

    MainBoardFactory mainBoardFactory = new AmdMainBoardFactory();

    MainBoard mainBoard = mainBoardFactory.make();

    // 组装 CPU 和主板

    Computer computer = new Computer(cpu, mainBoard);

    单独看 CPU 工厂和主板工厂,它们分别是前面我们说的工厂模式 。这种方式也容易扩展,因为要给电脑加硬盘的话,只需要加一个 HardDiskFactory 和相应的实现即可,不需要修改现有的工厂。

    但是,这种方式有一个问题,那就是如果 Intel 家产的 CPU 和 AMD 产的主板不能兼容使用 ,那么这代码就容易出错,因为客户端并不知道它们不兼容,也就会错误地出现随意组合。

    下面就是我们要说的产品族 的概念,它代表了组成某个产品的一系列附件的集合:

     当涉及到这种产品族的问题的时候,就需要抽象工厂模式来支持了。我们不再定义 CPU 工厂、主板工厂、硬盘工厂、显示屏工厂等等,我们直接定义电脑工厂,每个电脑工厂负责生产所有的设备,这样能保证肯定不存在兼容问题。

    这个时候,对于客户端来说,不再需要单独挑选 CPU厂商、主板厂商、硬盘厂商等,直接选择一家品牌工厂,品牌工厂会负责生产所有的东西,而且能保证肯定是兼容可用的。

     

    public static void main(String[] args) {

        // 第一步就要选定一个“大厂”

        ComputerFactory cf = new AmdFactory();

        // 从这个大厂造 CPU

        CPU cpu = cf.makeCPU();

        // 从这个大厂造主板

        MainBoard board = cf.makeMainBoard();

          // 从这个大厂造硬盘

        HardDisk hardDisk = cf.makeHardDisk();

        // 将同一个厂子出来的 CPU、主板、硬盘组装在一起

        Computer result = new Computer(cpu, board, hardDisk);

    }

    当然,抽象工厂的问题也是显而易见的,比如我们要加个显示器,就需要修改所有的工厂,给所有的工厂都加上制造显示器的方法。这有点违反了对修改关闭,对扩展开放 这个设计原则

    1.4 单例模式

    单例模式用得最多,错得最多。

    饿汉模式最简单:

    public class Singleton {

        // 首先,将 new Singleton() 堵死

        private Singleton() {};

        // 创建私有静态实例,意味着这个类第一次使用的时候就会进行创建

        private static Singleton instance = new Singleton();

        public static Singleton getInstance() {

            return instance;

        }

    }

    饱汉模式最容易出错:

    public class Singleton {

        // 首先,也是先堵死 new Singleton() 这条路

        private Singleton() {}

        // 和饿汉模式相比,这边不需要先实例化出来,注意这里的 volatile,它是必须的

        private static volatile Singleton instance = null;

        public static Singleton getInstance() {

            if (instance == null) {

                // 加锁

                synchronized (Singleton.class) {

                    // 这一次判断也是必须的,不然会有并发问题

                    if (instance == null) {

                        instance = new Singleton();

                    }

                }

            }

            return instance;

        }

    }

    双重检查,指的是两次检查 instance 是否为 null。

    volatile 在这里是需要的,希望能引起读者的关注。

    很多人不知道怎么写,直接就在 getInstance() 方法签名上加上 synchronized,这就不多说了,性能太差。

    嵌套类最经典,以后大家就用它吧:

    public class Singleton3 {

        private Singleton3() {}

        // 主要是使用了 嵌套类可以访问外部类的静态属性和静态方法 的特性

        private static class Holder {

            private static Singleton3 instance = new Singleton3();

        }

        public static Singleton3 getInstance() {

            return Holder.instance;

        }

    }

    最后,我们说一下枚举,枚举很特殊,它在类加载的时候会初始化里面的所有的实例,而且 JVM 保证了它们不会再被实例化,所以它天生就是单例的。

    虽然我们平时很少看到用枚举来实现单例,但是在 RxJava 的源码中,有很多地方都用了枚举来实现单例。

    1.5 建造者模式

    经常碰见的 XxxBuilder 的类,通常都是建造者模式的产物。建造者模式其实有很多的变种,但是对于客户端来说,我们的使用通常都是一个模式的:

    Food food = new FoodBuilder().a().b().c().build();

    Food food = Food.builder().a().b().c().build();

    套路就是先 new 一个 Builder,然后可以链式地调用一堆方法,最后再调用一次 build() 方法,我们需要的对象就有了。

    来一个中规中矩的建造者模式:

    class User {

        // 下面是“一堆”的属性

        private String name;

        private String password;

        private String nickName;

        private int age;

        // 构造方法私有化,不然客户端就会直接调用构造方法了

        private User(String name, String password, String nickName, int age) {

            this.name = name;

            this.password = password;

            this.nickName = nickName;

            this.age = age;

        }

        // 静态方法,用于生成一个 Builder,这个不一定要有,不过写这个方法是一个很好的习惯,

        // 有些代码要求别人写 new User.UserBuilder().a()...build() 看上去就没那么好

        public static UserBuilder builder() {

            return new UserBuilder();

        }

        public static class UserBuilder {

            // 下面是和 User 一模一样的一堆属性

            private String  name;

            private String password;

            private String nickName;

            private int age;

            private UserBuilder() {

            }

            // 链式调用设置各个属性值,返回 this,即 UserBuilder

            public UserBuilder name(String name) {

                this.name = name;

                return this;

            }

            public UserBuilder password(String password) {

                this.password = password;

                return this;

            }

            public UserBuilder nickName(String nickName) {

                this.nickName = nickName;

                return this;

            }

            public UserBuilder age(int age) {

                this.age = age;

                return this;

            }

            // build() 方法负责将 UserBuilder 中设置好的属性“复制”到 User 中。

            // 当然,可以在 “复制” 之前做点检验

            public User build() {

                if (name == null || password == null) {

                    throw new RuntimeException("用户名和密码必填");

                }

                if (age <= 0 || age >= 150) {

                    throw new RuntimeException("年龄不合法");

                }

                // 还可以做赋予”默认值“的功能

                  if (nickName == null) {

                    nickName = name;

                }

                return new User(name, password, nickName, age);

            }

        }

    }

    核心是:先把所有的属性都设置给 Builder,然后 build() 方法的时候,将这些属性复制 给实际产生的对象。


    看看客户端的调用:

    public class APP {

        public static void main(String[] args) {

            User d = User.builder()

                    .name("foo")

                    .password("pAss12345")

                    .age(25)

                    .build();

        }

    }

    说实话,建造者模式的链式 写法很吸引人,但是,多写了很多“无用”的 builder 的代码,感觉这个模式没什么用。不过,当属性很多,而且有些必填,有些选填的时候,这个模式会使代码清晰很多。我们可以在 Builder 的构造方法 中强制让调用者提供必填字段,还有,在 build() 方法中校验各个参数比在 User 的构造方法中校验,代码要优雅一些。

    1.6 原型模式

    原型模式很简单:有一个原型实例 ,基于这个原型实例产生新的实例,也就是“克隆”了。

    Object 类中有一个 clone() 方法,它用于生成一个新的对象,当然,如果我们要调用这个方法,java 要求我们的类必须先实现 Cloneable 接口 ,此接口没有定义任何方法,但是不这么做的话,在 clone() 的时候,会抛出 CloneNotSupportedException 异常。

    java 的克隆是浅克隆,碰到对象引用的时候,克隆出来的对象和原对象中的引用将指向同一个对象。通常实现深克隆的方法是将对象进行序列化,然后再进行反序列化。

    1.7 创建型模式 - 总结:

    创建型模式总体上比较简单,它们的作用就是为了产生实例对象,算是各种工作的第一步了,因为我们写的是面向对象 的代码,所以我们第一步当然是需要创建一个对象了。

    简单工厂模式最简单;

    工厂模式在简单工厂模式的基础上增加了选择工厂的维度,需要第一步选择合适的工厂;

    抽象工厂模式有产品族的概念,如果各个产品是存在兼容性问题的,就要用抽象工厂模式。

    单例模式就不说了,为了保证全局使用的是同一对象,一方面是安全性考虑,一方面是为了节省资源;

    建造者模式专门对付属性很多的那种类,为了让代码更优美;

    原型模式用得最少,了解Object 类中的 clone() 方法相关的知识即可。

    第二大类:结构型模式

    创建型设计模式,注重于对象的创建,而结构型设计模式,更加注重于,类与类之间的关系,将类与类按照某种布局组合起来形成一个更加强大的结构,它描述两种不同的事物:类和对象,根据这一点可分为类结构型和对象结构型。

    • 类结构型通过继承机制来组织接口和类
    • 对象结构型通过组合或者聚合来组合对象

    2.1 代理模式

    代理 ,就是对客户端隐藏真实实现,由代理来负责客户端的所有请求。当然,代理只是个代理,它不会完成实际的业务逻辑,而是一层皮而已,但是对于客户端来说,它必须表现得就是客户端需要的真实实现。

    public interface FoodService {

        Food makeChicken();

        Food makeNoodle();

    }

    public class FoodServiceImpl implements FoodService {

        public Food makeChicken() {

              Food f = new Chicken()

              f.setChicken("1kg");

              f.setSpicy("1g");

              f.setSalt("3g");

            return f;

        }

        public Food makeNoodle() {

            Food f = new Noodle();

            f.setNoodle("500g");

            f.setSalt("5g");

            return f;

        }

    }

    // 代理要表现得“就像是”真实实现类,所以需要实现 FoodService

    public class FoodServiceProxy implements FoodService {

        // 内部一定要有一个真实的实现类,当然也可以通过构造方法注入

        private FoodService foodService = new FoodServiceImpl();

        public Food makeChicken() {

            System.out.println("我们马上要开始制作鸡肉了");

            // 如果我们定义这句为核心代码的话,那么,核心代码是真实实现类做的,

            // 代理只是在核心代码前后做些“无足轻重”的事情

            Food food = foodService.makeChicken();

            System.out.println("鸡肉制作完成啦,加点胡椒粉"); // 增强

            food.addCondiment("pepper");

            return food;

        }

        public Food makeNoodle() {

            System.out.println("准备制作拉面~");

            Food food = foodService.makeNoodle();

            System.out.println("制作完成啦")

            return food;

        }

    }

    客户端调用,注意,我们要用代理来实例化接口:

    // 这里用代理类来实例化

    FoodService foodService = new FoodServiceProxy();

    foodService.makeChicken();

     

    我们发现没有,代理模式说白了就是做 “方法包装” 或做 “方法增强” 。在面向切面编程中,其实就是动态代理的过程。

    比如 Spring 中,我们自己不定义代理类,但是 Spring 会帮我们动态来定义代理,然后把我们定义在 @Before、@After、@Around 中的代码逻辑动态添加到代理中。

    说到动态代理,又可以展开说,Spring 中实现动态代理有两种,一种是如果我们的类定义了接口,如 UserService 接口和 UserServiceImpl 实现,

    那么采用 JDK 的动态代理,感兴趣的读者可以去看看 java.lang.reflect.Proxy 类的源码;另一种是我们自己没有定义接口的,Spring 会采用 CGLIB 进行动态代理,它是一个 jar 包,性能还不错。

    2.2 适配器模式

    说完代理模式,说适配器模式,是因为它们很相似,这里可以做个比较。

    适配器模式做的就是,有一个接口需要实现,但是我们现成的对象都不满足,需要加一层适配器来进行适配。

    适配器模式总体来说分三种:默认适配器模式、对象适配器模式、类适配器模式。先不急着分清楚这几个,先看看例子再说。

    默认适配器模式:

    首先,我们先看看最简单的适配器模式默认适配器模式(Default Adapter) 是怎么样的。

    我们用 Appache commons-io 包中的 FileAlterationListener 做例子,此接口定义了很多的方法,用于对文件或文件夹进行监控,一旦发生了对应的操作,就会触发相应的方法。

    public interface FileAlterationListener {

        void onStart(final FileAlterationObserver observer);

        void onDirectoryCreate(final File directory);

        void onDirectoryChange(final File directory);

        void onDirectoryDelete(final File directory);

        void onFileCreate(final File file);

        void onFileChange(final File file);

        void onFileDelete(final File file);

        void onStop(final FileAlterationObserver observer);

    }

    此接口的一大问题是抽象方法太多了,如果我们要用这个接口,意味着我们要实现每一个抽象方法,如果我们只是想要监控文件夹中的文件创建 和文件删除 事件,可是我们还是不得不实现所有的方法,很明显,这不是我们想要的。

    所以,我们需要下面的一个适配器 ,它用于实现上面的接口,但是所有的方法都是空方法 ,这样,我们就可以转而定义自己的类来继承下面这个类即可。

    public class FileAlterationListenerAdaptor implements FileAlterationListener {

        public void onStart(final FileAlterationObserver observer) {

        }

        public void onDirectoryCreate(final File directory) {

        }

        public void onDirectoryChange(final File directory) {

        }

        public void onDirectoryDelete(final File directory) {

        }

        public void onFileCreate(final File file) {

        }

        public void onFileChange(final File file) {

        }

        public void onFileDelete(final File file) {

        }

        public void onStop(final FileAlterationObserver observer) {

        }

    }

    比如我们可以定义以下类,我们仅仅需要实现我们想实现的方法就可以了:

    public class FileMonitor extends FileAlterationListenerAdaptor {

        public void onFileCreate(final File file) {

            // 文件创建

            doSomething();

        }

        public void onFileDelete(final File file) {

            // 文件删除

            doSomething();

        }

    }

    当然,上面说的只是适配器模式的其中一种,也是最简单的一种,无需多言。下面,再介绍**“正统的”** 适配器模式。

    对象适配器模式:

    来看一个《Head First 设计模式》中的一个例子,看看怎么将鸡适配成鸭,这样鸡也能当鸭来用。因为,现在鸭这个接口,我们没有合适的实现类可以用,所以需要适配器。

    public interface Duck {

        public void quack(); // 鸭的呱呱叫

        public void fly(); // 飞

    }

    public interface Cock {

        public void gobble(); // 鸡的咕咕叫

        public void fly(); // 飞

    }

    public class WildCock implements Cock {

        public void gobble() {

            System.out.println("咕咕叫");

        }

        public void fly() {

            System.out.println("鸡也会飞哦");

        }

    }

    鸭接口有 fly() 和 quare() 两个方法,鸡 Cock 如果要冒充鸭,fly() 方法是现成的,但是鸡不会鸭的呱呱叫,没有 quack() 方法。这个时候就需要适配了。

    // 毫无疑问,首先,这个适配器肯定需要 implements Duck,这样才能当做鸭来用

    public class CockAdapter implements Duck {

        Cock cock;

        // 构造方法中需要一个鸡的实例,此类就是将这只鸡适配成鸭来用

          public CockAdapter(Cock cock) {

            this.cock = cock;

        }

        // 实现鸭的呱呱叫方法

        @Override

          public void quack() {

            // 内部其实是一只鸡的咕咕叫

            cock.gobble();

        }

          @Override

          public void fly() {

            cock.fly();

        }

    }

    客户端调用很简单了:

    public static void main(String[] args) {

        // 有一只野鸡

        Cock wildCock = new WildCock();

        //成功将野鸡适配成鸭

        Duck duck = new CockAdapter(wildCock);

    }

    到这里,大家也就知道了适配器模式是怎么回事了。无非是我们需要一只鸭,但是我们只有一只鸡,这个时候就需要定义一个适配器,由这个适配器来充当鸭,但是适配器里面的方法还是由鸡来实现的。

    类适配器模式:

    看到这个图,大家应该很容易理解的吧,通过继承的方法,适配器自动获得了所需要的大部分方法。这个时候,客户端使用更加简单,直接 Target t = new SomeAdapter(); 就可以了。

    适配器模式总结

    1.类适配和对象适配的异同

    一个采用继承,一个采用组合;

    类适配属于静态实现,对象适配属于组合的动态实现,对象适配需要多实例化一个对象。

    总体来说,对象适配用得比较多。

    2.适配器模式和代理模式的异同

    比较这两种模式,其实是比较对象适配器模式和代理模式,在代码结构上,它们很相似,都需要一个具体的实现类的实例。

    但是它们的目的不一样,代理模式做的是增强原方法的活;

    适配器做的是适配的活,为的是提供“把鸡包装成鸭,然后当做鸭来使用”,而鸡和鸭它们之间原本没有继承关系。

    2.3 桥梁模式

    理解桥梁模式,其实就是理解代码抽象和解耦。

    我们首先需要一个桥梁,它是一个接口,定义提供的接口方法。

    public interface DrawAPI {

        public void draw(int radius, int x, int y);

    }

    然后是一系列实现类:

    public class RedPen implements DrawAPI {

        @Override

        public void draw(int radius, int x, int y) {

            System.out.println("用红色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);

        }

    }

    public class GreenPen implements DrawAPI {

        @Override

        public void draw(int radius, int x, int y) {

            System.out.println("用绿色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);

        }

    }

    public class BluePen implements DrawAPI {

        @Override

        public void draw(int radius, int x, int y) {

            System.out.println("用蓝色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);

        }

    }

    定义一个抽象类,此类的实现类都需要使用 DrawAPI:

    public abstract class Shape {

        protected DrawAPI drawAPI;

        protected Shape(DrawAPI drawAPI) {

            this.drawAPI = drawAPI;

        }

        public abstract void draw();

    }

    定义抽象类的子类:

    // 圆形

    public class Circle extends Shape {

        private int radius;

        public Circle(int radius, DrawAPI drawAPI) {

            super(drawAPI);

            this.radius = radius;

        }

        public void draw() {

            drawAPI.draw(radius, 00);

        }

    }

    // 长方形

    public class Rectangle extends Shape {

        private int x;

        private int y;

        public Rectangle(int x, int y, DrawAPI drawAPI) {

            super(drawAPI);

            this.x = x;

            this.y = y;

        }

        public void draw() {

            drawAPI.draw(0, x, y);

        }

    }

    最后,我们来看客户端演示:

    public static void main(String[] args) {

        Shape greenCircle = new Circle(10new GreenPen());

        Shape redRectangle = new Rectangle(48new RedPen());

        greenCircle.draw();

        redRectangle.draw();

    }

    桥梁模式的优点也是显而易见的,就是非常容易进行扩展。

    2.4 装饰器模式

    装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。

    使用场景:

    • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
    • 需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。

    先定义一个Food的抽象类:

    public abstract class Food {

        protected String desc;

        public abstract String cook();

    }

    然后实例化2个实物对象,比如鸡肉和鸭肉:

    public class Chicken extends Food {

        public Chicken() {

            this.desc = "鸡肉";

        }

        @Override

        public String cook() {

            return this.desc;

        }

    }

    public class Duck extends Food {

        public Duck() {

            this.desc = "鸭肉";

        }

        @Override

        public String cook() {

            return this.desc;

        }

    }


    再定义一个装饰的抽象类,这个是继承了Food:

    public abstract class FoodDecoration extends Food {

        @Override

        public abstract String cook();

    }

    再定义两个不同的装饰类,一个是烤,一个是蒸:

    public class RoastFood extends FoodDecoration {

        private Food food;

        public RoastFood(Food f) {

            this.food = f;

        }

        @Override

        public String cook() {

            return getDecoration() + food.cook();

        }

        private String getDecoration() {

            return "烤";

        }

    }

    public class SteamedFood extends FoodDecoration {

        private Food food;

        public SteamedFood(Food food) {

            this.food = food;

        }

        private String getDecoration() {

            return "蒸";

        }

        @Override

        public String cook() {

            return this.getDecoration() + food.cook();

        }

    }

    最后我们可以对实物进行装饰:

    public static void main(String args[]) {

        // 测试单纯的食物

        Chicken chicken = new Chicken();

        System.out.println(chicken.cook());

        // 测试单重修饰的食物

        RoastFood roastFood = new RoastFood(chicken);

        System.out.println(roastFood.cook());

        // 测试多重修饰的食物

        SteamedFood steamedFood = new SteamedFood(roastFood);

        System.out.println(steamedFood.cook());

        // 测试单纯的食物

        Duck duck = new Duck();

        System.out.println(duck.cook());

        // 测试单重修饰的食物

        RoastFood roastDuckFood = new RoastFood(duck);

        System.out.println(roastDuckFood.cook());

        // 测试多重修饰的食物

        SteamedFood steamedDuckFood = new SteamedFood(roastDuckFood);

        System.out.println(steamedDuckFood.cook());

    }


    结果:

    鸡肉

    烤鸡肉

    蒸烤鸡肉

    鸭肉

    烤鸭肉

    蒸烤鸭肉

    评价一下,装饰类主要是需要定义一个通用的抽象方法或者接口,供实体对象和装饰对象继承,并通过装饰类修饰该对象。

    因为通过修饰类对外暴露的cook()方法和原对象一致(肯定是一致的,因为他们重写同一个方法),所以这个装饰类对象就可以当做源对象使用,不同的是对源对象做了一层装饰。

    2.5 门面模式

    门面模式(也叫外观模式,Facade Pattern)在许多源码中有使用,比如 slf4j 就可以理解为是门面模式的应用。这是一个简单的设计模式,我们直接上代码再说吧。

    首先,我们定义一个接口:

    public interface Shape {

       void draw();

    }

    定义几个实现类:

    public class Circle implements Shape {

        @Override

        public void draw() {

           System.out.println("Circle::draw()");

        }

    }

    public class Rectangle implements Shape {

        @Override

        public void draw() {

           System.out.println("Rectangle::draw()");

        }

    }

    客户端调用:

    public static void main(String[] args) {

        // 画一个圆形

          Shape circle = new Circle();

          circle.draw();

          // 画一个长方形

          Shape rectangle = new Rectangle();

          rectangle.draw();

    }

    以上是我们常写的代码,我们需要画圆就要先实例化圆,画长方形就需要先实例化一个长方形,然后再调用相应的 draw() 方法。

    下面,我们看看怎么用门面模式来让客户端调用更加友好一些。

    我们先定义一个门面:

    public class ShapeMaker {

       private Shape circle;

       private Shape rectangle;

       private Shape square;

       public ShapeMaker() {

          circle = new Circle();

          rectangle = new Rectangle();

          square = new Square();

       }

      /**

       * 下面定义一堆方法,具体应该调用什么方法,由这个门面来决定

       */

       public void drawCircle(){

          circle.draw();

       }

       public void drawRectangle(){

          rectangle.draw();

       }

       public void drawSquare(){

          square.draw();

       }

    }

    看看现在客户端怎么调用:

    public static void main(String[] args) {

      ShapeMaker shapeMaker = new ShapeMaker();

      // 客户端调用现在更加清晰了

      shapeMaker.drawCircle();

      shapeMaker.drawRectangle();

      shapeMaker.drawSquare();

    }

    门面模式的优点显而易见,客户端不再需要关注实例化时应该使用哪个实现类,直接调用门面提供的方法就可以了,因为门面类提供的方法的方法名对于客户端来说已经很友好了。

    2.6 组合模式

    组合模式用于表示具有层次结构的数据,使得我们对单个对象和组合对象的访问具有一致性。

    代码实现

    抽象构件AbstractFile

    public abstract class AbstractFile {

        public void add(AbstractFile file) {

            throw new UnsupportedOperationException();

        }

        public void remove(AbstractFile file) {

            throw new UnsupportedOperationException();

        }

        public AbstractFile getChild(int i) {

            throw new UnsupportedOperationException();

        }

        public void print() {

            throw new UnsupportedOperationException();

        }

    }

    叶子构件  :图片文件

    public class ImageFile extends AbstractFile {

        private String name;

        public ImageFile(String name) {

            this.name = name;

        }

        public void print() {

            System.out.println(name);

        }

    }

    叶子构件  :音乐文件

    public class MusicFile extends AbstractFile {

        private String name;

        public MusicFile(String name) {

            this.name = name;

        }

        public void print() {

            System.out.println(name);

        }

    }

    叶子构件  :音频文件

    public class VideoFile extends AbstractFile {

        private String name;

        public VideoFile(String name) {

            this.name = name;

        }

        public void print() {

            System.out.println(name);

        }

    }


    容器构件

    public class Folder extends AbstractFile {

        private String name;

        private List files = new ArrayList<>();

        public Folder(String name) {

            this.name = name;

        }

        @Override

        public void add(AbstractFile file) {

            files.add(file);

        }

        @Override

        public void remove(AbstractFile file) {

            files.remove(file);

        }

        @Override

        public AbstractFile getChild(int i) {

            return files.get(i);

        }

        @Override

        public void print() {

            for (AbstractFile file : files) {

                file.print();

            }

        }

    }

    测试

    public static void main(String[] args) {

        AbstractFile m1 = new MusicFile("尽头.mp3");

        AbstractFile m2 = new MusicFile("飘洋过海来看你.mp3");

        AbstractFile m3 = new MusicFile("曾经的你.mp3");

        AbstractFile m4 = new MusicFile("take me to your heart.mp3");

        AbstractFile v1 = new VideoFile("战狼2.mp4");

        AbstractFile v2 = new VideoFile("理想.avi");

        AbstractFile v3 = new VideoFile("琅琊榜.avi");

        AbstractFile i1 = new ImageFile("敦煌.png");

        AbstractFile i2 = new ImageFile("baby.jpg");

        AbstractFile i3 = new ImageFile("girl.jpg");

        AbstractFile aa = new Folder("aa");

        aa.add(i3);

        AbstractFile bb = new Folder("bb");

        bb.add(m4);

        bb.add(v3);

        AbstractFile top = new Folder("top");

        top.add(aa);

        top.add(bb);

        top.add(m1);

        top.add(m2);

        top.add(m3);

        top.add(v1);

        top.add(v2);

        top.add(i1);

        top.add(i2);

        top.print();

    }

    结果:

    girl.jpg

    take me to your heart.mp3

    琅琊榜.avi

    尽头.mp3

    飘洋过海来看你.mp3

    曾经的你.mp3

    战狼2.mp4

    理想.avi

    敦煌.png

    baby.jpg

    2.7 享元模式

    如果在一个系统中存在多个相同的对象,那么只需要共享一份对象的拷贝,而不必为每一次使用都创建新的对象。目的是提高系统性能。

    享元模式包含三个角色:

    (1)抽象享元Flyweight类:享元对象抽象基类或接口。

    (2)具体享元ConcreteFlyweight类:实现抽象享元类。

    (3)享元工ctory类:厂FlyweightFa享元模式的核心模块,负责管理享元对象池、创建享元对象,保证享元对象可以被系统适当地共享。

    当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有一个符合要求的享元对象,如果已有,享元工厂角色就提供这个已有的享元对象;如果没有就创建一个。

    在这里享元工厂是享元模式的核心,它需要确保系统可以共享相同的对象。它会维护一个对象列表,当我们想要获取享元类时,如果请求的享元类已经被创建,则直接返回已有的享元类:若没有,则创建一个新的享元对象,并将它加入到维护队列中。

    代码实现

    第一步:定义抽象享元类(Book)

    /**

     * 抽象享元类

     */

    public interface Ticket {

        //显示票价,参数为列车类型

        public void showPrice(String type);

    }

    第二步:定义具体享元类(ConcreteBook)

    public class ConcreteTicket implements Ticket{

        String from;

        String to;

        public ConcreteTicket(String from,String to){

            this.from = from;

            this.to = to;

        }

        @Override

        public void showPrice(String type) {

            if(type.equals("adult")){

                System.out.println("从"+from+"到"+to+"的成人票价为200元");

            }else{

                System.out.println("从"+from+"到"+to+"的儿童票价为100元");

            }

        }

    }


    第三步:享元工厂(Llibrary)

    public class TicketFactory {

        static Map map= new ConcurrentHashMap< String,Ticket >();

        public static Ticket getTicket(String from,String to){

            String key = from+to;

            if(map.containsKey(key)){

                System.out.println("使用缓存"+key);

                return map.get(key);

            }else{

                System.out.println("创建对象"+key);

                Ticket ticket = new ConcreteTicket(from,to);

                map.put(key, ticket);

                return ticket;

            }

        }

    }

    第四步:客户端调用

    public class Client {

        public static void main(String[] args) {

            //使用时

            TicketFactory.getTicket("南京","杭州").showPrice("adult");

            TicketFactory.getTicket("南京","杭州").showPrice("children");

        }

    }


    输出结果:

    创建对象南京杭州

    从南京到杭州的成人票价为200

    使用缓存南京杭州

    从南京到杭州的儿童票价为100

    享元模式特点

    (1)节省内存空间,对于可重复的对象只会被创建一次,对象比较多时,就会极大的节省空间。

    (2)提高效率,由于创建对象的数量减少,所以对系统内存的需求也减小,使得速度更快,效率更高。

    2.8 结构型模式 - 总结:

    代理模式是做方法增强的。

    适配器模式是把鸡包装成鸭这种用来适配接口的。

    桥梁模式做到了很好的解耦。

    装饰模式从名字上就看得出来,适合于装饰类或者说是增强类的场景。

    门面模式的优点是客户端不需要关心实例化过程,只要调用需要的方法即可。

    组合模式用于描述具有层次结构的数据。

    享元模式是为了在特定的场景中缓存已经创建的对象,用于提高性能。

    第三大类:行为型模式

    行为型模式关注的是各个类之间的相互作用,将职责划分清楚,使得我们的代码更加地清晰。

    3.1 策略模式

    策略模式的主要角色如下。

    抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
    具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。
    环境(Context)类:持有一个策略类的引用,最终给客户端调用。
     

    下面设计的场景是,我们需要画一个图形,可选的策略就是用红色笔来画,还是绿色笔来画,或者蓝色笔来画。

    首先,先定义一个策略接口:

    public interface Strategy {

       public void draw(int radius, int x, int y);

    }

    然后我们定义具体的几个策略:

    public class RedPen implements Strategy {

       @Override

       public void draw(int radius, int x, int y) {

          System.out.println("用红色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);

       }

    }

    public class GreenPen implements Strategy {

       @Override

       public void draw(int radius, int x, int y) {

          System.out.println("用绿色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);

       }

    }

    public class BluePen implements Strategy {

       @Override

       public void draw(int radius, int x, int y) {

          System.out.println("用蓝色笔画图,radius:" + radius + ", x:" + x + ", y:" + y);

       }

    }

    使用策略的类:

    public class Context {

       private Strategy strategy;

       public Context(Strategy strategy){

          this.strategy = strategy;

       }

       public int executeDraw(int radius, int x, int y){

          return strategy.draw(radius, x, y);

       }

    }

    客户端演示:

    public static void main(String[] args) {

        Context context = new Context(new BluePen()); // 使用绿色笔来画

          context.executeDraw(1000);

    }


    策略模式的主要优点如下。

    多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句。
    策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
    策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
    策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
    策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。

    3.2 观察者模式

    观察者模式两个操作,观察者订阅自己关心的主题和主题有数据变化后通知观察者们。

    首先,需要定义主题,每个主题需要持有观察者列表的引用,用于在数据变更的时候通知各个观察者:

    public class Subject {

        private List observers = new ArrayList();

        private int state;

        public int getState() {

            return state;

        }

        public void setState(int state) {

            this.state = state;

            // 数据已变更,通知观察者们

            notifyAllObservers();

        }

        // 注册观察者

        public void attach(Observer observer) {

            observers.add(observer);

        }

        // 通知观察者们

        public void notifyAllObservers() {

            for (Observer observer : observers) {

                observer.update();

            }

        }

    }

    定义观察者接口:

    public abstract class Observer {

        protected Subject subject;

        public abstract void update();

    }

    其实如果只有一个观察者类的话,接口都不用定义了,不过,通常场景下,既然用到了观察者模式,我们就是希望一个事件出来了,会有多个不同的类需要处理相应的信息。

    比如,订单修改成功事件,我们希望发短信的类得到通知、发邮件的类得到通知、处理物流信息的类得到通知等。

    我们来定义具体的几个观察者类:

    public class BinaryObserver extends Observer {

        // 在构造方法中进行订阅主题

        public BinaryObserver(Subject subject) {

            this.subject = subject;

            // 通常在构造方法中将 this 发布出去的操作一定要小心

            this.subject.attach(this);

        }

        // 该方法由主题类在数据变更的时候进行调用

        @Override

        public void update() {

            String result = Integer.toBinaryString(subject.getState());

            System.out.println("订阅的数据发生变化,新的数据处理为二进制值为:" + result);

        }

    }

    public class HexaObserver extends Observer {

        public HexaObserver(Subject subject) {

            this.subject = subject;

            this.subject.attach(this);

        }

        @Override

        public void update() {

            String result = Integer.toHexString(subject.getState()).toUpperCase();

            System.out.println("订阅的数据发生变化,新的数据处理为十六进制值为:" + result);

        }

    }

    客户端使用也非常简单:

    public static void main(String[] args) {

        // 先定义一个主题

        Subject subject1 = new Subject();

        // 定义观察者

        new BinaryObserver(subject1);

        new HexaObserver(subject1);

        // 模拟数据变更,这个时候,观察者们的 update 方法将会被调用

        subject.setState(11);

    }

    结果:

    订阅的数据发生变化,新的数据处理为二进制值为:1011

    订阅的数据发生变化,新的数据处理为十六进制值为:B

    3.3 责任链模式

    责任链通常需要先建立一个单向链表,然后调用方只需要调用头部节点就可以了,后面会自动流转下去。

    比如流程审批就是一个很好的例子,只要终端用户提交申请,根据申请的内容信息,自动建立一条责任链,然后就可以开始流转了。

    首先,我们要定义流程上节点的基类:定义审批流程

    public abstract class Handler {

        private Handler nextHandler;

        public void setNextHandler(Handler handler) {

            this.nextHandler = handler;

        }

        // 处理申请单

        protected abstract boolean process(Requisition requisition);

        // 暴露方法

        public boolean handle(Requisition requisition) {

            boolean result = process(requisition);

            if (result) {

                if (nextHandler != null) {

                    return nextHandler.handle(requisition);

                else {

                    return true;

                }

            }

            return false;

        }

    }

    接下来,我们需要定义具体的每个节点了。

    组长审批:

    public class ManagerHandler extends Handler {

        @Override

        public boolean process(Requisition requisition) {

            System.out.println(String.format("Manager 审批通过来自[%s]的申请单[%s]...", requisition.getApplicant(), requisition.getName()));

            return true;

        }

    }

    经理审批:

    public class DirectorHandler extends Handler {

        @Override

        public boolean process(Requisition requisition) {

            System.out.println(String.format("Director 审批通过来自[%s]的申请单[%s]...", requisition.getApplicant(), requisition.getName()));

            return true;

        }

    }

    hr审批:

    public class HrHandler extends Handler {

        @Override

        public boolean process(Requisition requisition) {

            System.out.println(String.format("Hr 审批不通过来自[%s]的申请单[%s]...", requisition.getApplicant(), requisition.getName()));

            return false;

        }

    }


    客户端调用:

    public static void main(String[] args) {

        HrHandler hrHandler = new HrHandler();

        DirectorHandler directorHandler = new DirectorHandler();

        directorHandler.setNextHandler(hrHandler);

        ManagerHandler managerHandler = new ManagerHandler();

        managerHandler.setNextHandler(directorHandler);

        Requisition requisition = new Requisition();

        requisition.setApplicant("加薪申请");

        requisition.setName("情怀");

        managerHandler.handle(requisition);

    }

    结果:

    Manager 审批通过来自[加薪申请]的申请单[情怀]...

    Director 审批通过来自[加薪申请]的申请单[情怀]...

    Hr 审批不通过来自[加薪申请]的申请单[情怀]...

    3.4 模板方法模式

    在含有继承结构的代码中,模板方法模式是非常常用的。

    模板方法骨架,定义好整个流程 起床->刷牙->吃早饭->交通工具->睡觉 。其中起床、刷牙、睡觉 三个步骤是一样的,所以直接实现就好。对于不同的方法:吃早饭、交通工具 不同,交由子类去实现

    定义好模板骨架

    public abstract class AbstractMerchantService {

        public final void work() {

            //起床

            this.wake();

            //刷牙

            this.brush();

            //吃早饭

            this.breakfast();

            //交通工具

            this.transport();

            //睡觉

            this.sleep();

        }

        //步骤一样 直接实现

        public void wake() {

            System.out.println("起床...");

        }

        //步骤一样 直接实现

        public void brush() {

            System.out.println("刷牙...");

        }

        // 步骤不一样 (一个是吃面包 一个是喝牛奶)

        public abstract void breakfast();

        // 步骤不一样 (一个是开车 一个是坐地铁)

        public abstract void transport();

        // 步骤一样 直接实现

        public void sleep() {

            System.out.println("睡觉...");

        }

    }

    穷人实现:

    public class BService extends AbstractMerchantService{

        @Override

        public void breakfast() {

            System.out.println("我是B吃早饭:苦");

        }

        @Override

        public void transport() {

            System.out.println("我是B坐车:11路公交");

        }

    }

    富人实现:

    public class AService extends AbstractMerchantService{

        @Override

        public void breakfast() {

            System.out.println("我是A吃早饭:鹅肝");

        }

        @Override

        public void transport() {

            System.out.println("我是A坐车:兰博");

        }

    }

    调用:

    public class MuBanFangFaTest {

        @Test

        public void test(){

            AbstractMerchantService bService = new BService();

            bService.work();

            AbstractMerchantService aService = new AService();

            aService.work();

        }

    }

    结果:

    起床...

    刷牙...

    我是B吃早饭:苦

    我是B坐车:11路公交

    睡觉...

    起床...

    刷牙...

    我是A吃早饭:鹅肝

    我是A坐车:兰博

    睡觉...

    模板方法优点:

    • 提高代码复用性
      将相同部分的代码放在抽象的父类中
    • 提高了拓展性
      将不同的代码放入不同的子类中,通过对子类的扩展增加新的行为
    • 实现了反向控制
      通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为,实现了反向控制 & 符合“开闭原则”

    3.5 状态模式

    状态模式最核心的设计思路就是将对象的状态抽象出一个接口,然后根据它的不同状态封装其行为,这样就可以实现状态和行为的绑定,最终实现对象和状态的有效解耦。

    首先是状态接口,这个接口是给我们实际的状态对象继承的,这个接口有一个方法doAction,这个方法就是给不同的状态对象实现的,用于处理不同状态下的行为的。

    public interface State {

        /**

         * 改变状态的操作

         * @param context

         */

        void doAction(Context context);

    }

    然后是我们的状态所属者,这个类有一个核心的属性就是我们的State接口。

    public class Context {

        private State state;

        public Context(){}

        public void setState(State state){

            this.state = state;

        }

        public State getState(){

            return state;

        }

         @Override

        public String toString() {

            return "Context{" +

                    "state=" + state +

                    '}';

        }

    }

    状态实现者继承了State接口,并实现了doAction方法,在方法内部可以对我们的状态所有者进行对应的操作。

    定义减库存的状态:

    public class DeductState implements State {

        public void doAction(Context context) {

            System.out.println("商品卖出,准备减库存");

            context.setState(this);

            //... 执行减库存的具体操作

        }

        public String toString() {

            return "Deduct State";

        }

    }

    定义补库存状态:

    public class RevertState implements State {

        public void doAction(Context context) {

            System.out.println("给此商品补库存");

            context.setState(this);

            //... 执行加库存的具体操作

        }

        public String toString() {

            return "Revert State";

        }

    }


    客户端调用

    public static void main(String[] args) {

            // 我们需要操作的是 iPhone X

            Context context = new Context("iPhone X");

            // 看看怎么进行补库存操作

            State revertState = new RevertState();

            revertState.doAction(context);

            // 同样的,减库存操作也非常简单

            State deductState = new DeductState();

            deductState.doAction(context);

            // 如果需要我们可以获取当前的状态

            // context.getState().toString();

    }

    状态模式的优点:

    • 封装了转换规则。
    • 枚举可能的状态,在枚举状态之前需要确定状态种类。
    • 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
    • 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块
    • 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。

    3.6 行为型模式 - 总结:

    策略模式适合类中成员以方法为主,算法经常变动的程序。

    观察者模式观察者模式所做的工作就是解除耦合,让耦合双方都依赖于抽象,而不是依赖于具体,从而使得各自变化都不会影响另一方的变化,是依赖倒转原则的最佳体现。

    责任链模式可以随时的增加或修改处理一个请求的结构,增强给对象指派职责的灵活性

    模板模式将不变的代码转移到父类中去,将可变的代码留给子类。

    状态模式当一个对象的行为取决于它的状态并且它必须运行时刻根据状态改变它的行为时,可以考虑使用状态模式。

    第四:总结

    学习设计模式的目的是为了让我们的代码更加的优雅、易维护、易扩展。

  • 相关阅读:
    C#中Visual Studio如何为解决方案设置启动项目
    YOLOv5改进之二:添加CBAM注意力机制
    uniapp 阿里云点播 视频播放(加密版)(APP版)记录播放进度
    Mongodb出现Error: couldn‘t add user: Could not find role: root@database 解决方法
    基于bert-base-chinese的二分类任务
    IAR 下的雅特力AT32F415CBT7工程创建与设置
    前端点击地图上的位置获取当前经纬度
    怎么看一上副业项目适合不适合自己
    C++中的多态
    Metabase学习教程:入门-5
  • 原文地址:https://blog.csdn.net/pyd950812/article/details/128000002