《Effective Java》——关于Lambda和Stream

和《Java8实战相比》,很多东西都是重复的。写这篇笔记,一方面是作为回顾总结,另一方面,很多东西这本书换了一个角度去看,对于思考的完整性,依然不可或缺。

哲学上的白马非马其实就是所谓的副作用

其实上一篇的《Java8实战》学习笔记二已经把有些思想上的东西说的很多了,在这就是突然想到一些东西记录一些。
《信息简史》这本书里面说过一个意思“有逻辑的东西一定会有悖论”,程序逻辑一般三种(顺序、条件、循环)。大一开始学程序的时候,就感觉很别扭,想让a等于a+1,自己会先让b=a+1,再a=b,现在想想其实就是心里并没有意识到程序世界里的白马非马问题。a=a+1,一道错误的数学等式,却在程序里司空见惯。命令式编程可能对待它就是个sum指令,但是看成函数来说完全是错误的,a经过了加1映射以后就不可能是等于原来的a了,违反了引用的透明性,有副作用。

继续阅读“《Effective Java》——关于Lambda和Stream”

《Head First 设计模式》读书笔记(12)—— 复合模式 Compound

本文共5000词,阅读需要30分钟,本文相关代码地址被托管在 gitee

很多优秀的威力强大的设计模式会同时使用多个设计模式,这就是模式的模式——复合模式。
大多数流行的设计模式都是复合模式,同时用到几个模式,并且解决了一类通用的问题,比如MVC(很期待作者从设计模式怎么理解MVC,因为Spring MVC很普遍,但是这回要从根上看不知道会看到什么)。

继续阅读“《Head First 设计模式》读书笔记(12)—— 复合模式 Compound”

《Head First 设计模式》读书笔记(11)—— 代理模式 Proxy

本文共9000词,阅读需要30分钟,本文相关代码地址被托管在 gitee

代理就是要进行控制和管理访问,代理有很多种,这一章很精彩

代理模式——接着状态模式的糖果机说

背景:糖果机老板希望可以在看到这个糖果机的运行报告以及库存信息

当然最直接的想法就是新建一个GumballMonitor类,里面实现个report方法,然后一被使用就调用这个方法,差不多就这个意思,基本上就完美解决了

新背景:希望能够远程监控,不能只是在一个jvm里才能监控

这些就是远程代理。
梳理一下现在的情况,现在虽然可以监控,但是必须在同一个JVM里,所以我们要使用远程代理模式,具体往下看
远程代理就好比“远程对象的本地代表”。
什么是远程对象,就是一种活在不同的jvm堆里,在不同的地址空间运行的远程对象。
什么是本地代表,这是一种可以有本地方法调用的对象,其行为会在转发到远程对象中。

本例的代理模式具体流程大概就是:
糖果机就是远程对象,它真的在卖东西,在跟买的人交互
而代理就是假装自己是糖果机,和监视器去交互
监视器就是客户,以为自己在实时监控远程的糖果机,实际上只是在和代理沟通,使用的就是本地代表。

关于Java RMI(Java Remote Method Invocation)

这本书也讲了,先学下,如果没搞明白,估计要单独系统看一下这个东西
直接翻译就是 远程方法调用。
先照本宣科一下:
上面说的很多,但是怎么去调用远程的一个类呢?你不可能直接Duck d=new “别人电脑上的一个子鸭子类”
变量d只能引用当前代码语句的同一堆空间的对象,这时候就需要java RMI,帮助你实现远程JVM的对象,并允许我们调用他的方法

远程方法101

如果我们还不知道RMI,但是我们现在就是要实现远程方法的调用。我们可以这样做:
我们就是客户端,远程就是服务端,中间就是一些辅助对象(还没往下看,但我估计是利用Tcp的Socket通信),
客户使用辅助对象上的方法,仿佛这些辅助对象就是真正的服务,辅助对象帮助我们和服务端机交互。

果然就是Socket通信,但可能不是基础的消息发送过去:

服务器通过Socket连接从客户辅助对象接收请求,将调用的信息解包,然后服务器亲自调用真正的方法。所以服务器连接的服务辅助对象;
而服务辅助对象从服务器中得到返回值,将它打包然后通过Socket的输出流,客户辅助对象解包然后将返回值交给客户对象。

客户对象--客户辅助对象--服务辅助对象--服务对象

RMI提供客户辅助对象和服务辅助对象,为客户辅助对象创建和服务对象相同的方法。RMI直接实现了这些,让你不用亲自编写网络I/O的代码,客户调用远程方法就和在运行在客户自己的本地JVM上对对象进行正常方法调用一样。
RMI也提供了lookup service,用来寻找和访问远程对象。
看起来运行一样,实际上网络I/O是实际存在的,要考虑这些问题。

RMI称呼规定一下

客户辅助对象称为stub(桩),一般是RMI stub;
服务辅助对象称为skeleton(骨架),一般是RMI skeleton,新版java已经不这么叫,但差不多。

远程代理——以糖果机为例实现RMI

  1. 制作远程接口
    远程接口是要定义出让客户远程调用的方法,客户将用它作为服务的类类型,stub和实际的服务都要实现这个接口。

    import java.rmi.Remote;
    import java.rmi.RemoteException;
    //Remote定义一个记号接口,利用接口扩展接口
    public interface MyRemote extends Remote {
    /**
    * 所有方法必须抛出RemoteException,因为客户调用网络远程是需要网络I/O这些都是有风险的,
    * 所以直接在接口中声明异常让用户意识到这些危险
    *
    * 第二个就是远程方法的变量和返回值必须要实现Serializable接口
    */
    public String sayHello() throws RemoteException;
    }
  2. 制作远程的实现
    这是实际工作的类,比如糖果机就是为远程接口的远程方法进行了真正的实现。

    import java.net.MalformedURLException;
    import java.rmi.Naming;
    import java.rmi.RemoteException;
    import java.rmi.server.UnicastRemoteObject;
    /**
    * @author unclewang
    */
    /**
    * 因为这个服务实现就是实现MyRemote的,所以就是implements MyRemote
    * extends UnicastRemoteObject 扩展这个类可以让你直接变成远程服务对象,具备相应的远程功能
    *
    */
    public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
    /**
    * 扩展UnicastRemoteObject类的代价就是构造器就是也会抛出相应的异常。因为父类也是这样
    */
    public MyRemoteImpl() throws RemoteException {
    }
    @Override
    public String sayHello() throws RemoteException {
    return "server says, 'hey'";
    }
    public static void main(String[] args) {
    try {
    /**
    * 你现在已经有远程服务了,为了让它被远程客户调用,你要将这个服务实例化,
    * 然后放进RMI registry(要保证RMI registry正在运行)
    * 注册方法就是:java.rmi.Naming类中的静态rebind()方法
    */
    MyRemote service = new MyRemoteImpl();
    Naming.rebind("RemoteHello", service);
    } catch (RemoteException e) {
    e.printStackTrace();
    } catch (MalformedURLException e) {
    e.printStackTrace();
    }
    }
    }
  3. 利用rmic产生的stub和skeleton
    利用rmic产生的stub和skeleton,这是java虚拟机自带的命令,我用idea的使用办法就是先run一下这个类,然后进入 /Users/unclewang/Idea_Projects/headfirst/target/classes 文件夹,然后执行 rmic headfirst.proxy_pattern.two.MyRemoteImpl 不过只生成了stub后缀的class,skeleton没生成。

  4. 启动RMI registry
    rmiregistry就像是电话本,客户可以从中查到代理的位置
    还是要进入 /Users/unclewang/Idea_Projects/headfirst/target/classes ,然后用终端启动 rmiregistry

  5. 开始远程服务
    先让服务对象开始运行,然后你的服务实现类会去实例化一个服务的实例,并将这个服务注册到RMI registry,注册之后,这个服务就可以供客户调用了。
    还是要进入 /Users/unclewang/Idea_Projects/headfirst/target/classes ,然后用终端启动 java headfirst.proxy_pattern.two.MyRemoteImpl

实现RMI后进行调用

客户端代码:

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class MyRemoteClient {
    public static void main(String[] args) {
        new MyRemoteClient().go();
    }
    public void go(){
        try{
            //使用远程接口MyRemote作为服务类型
            //使用lookup方法,其实Refistry提供的。
            //不需要port只需要提供注册时用的服务名字。
            MyRemote service = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");
            String s = service.sayHello();
            System.out.println(s);

        } catch (RemoteException e) {
            e.printStackTrace();
        } catch (NotBoundException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }
}

成功运行:

p.s. 我对SpringCloud理解的虽然也不是特别深,但是里面的Eureka做http服务配置发现感觉有点异曲同工,以后看看资料估计可以融会贯通。

动态类加载,以后细看

RMI容易犯错的三个地方:
1.忘了启动远程服务前先启动rmiregistry,这样才能进行绑定操作
2.忘了让变量和返回值的类型称为可序列化的类型
3.忘了给用户提供stub类

两边最后的配置

客户本地上应该有Client.class MyRemoteImpl_Stub.class MyRemote.class
服务器上应该有MyRemoteImpl_Stub.class MyRemoteImpl_Skel.class MyRemote.class MyRemoteImpl.class
服务器的绑定操作:Naming.rebind("RemoteHello", service) 其实绑定的就是Stub
客户也需要这个Stub类,虽然表面上客户和MyRemote在交互,但是Myremote是依靠Stub类操作的

用RMi修改糖果机,代码在gitee上

首先两台电脑(笔记本和台式机在同一个局域网里)都需要rmic headfirst.proxy_pattern.three.GumballMachine生成stub的class文件,也就是两边都有
我的步骤就是在笔记本上先通过 java headfirst.proxy_pattern.three.GumballMachineTestDrive unclewang 100
然后台式机直接运行GumballMonitorDriver,里面的IP是笔记本的ip,然后成功实时打印出来糖果机的数量;

  • 此外,我发现确实其实这个Stub就跟纽带一样,只要你两边都有这个,服务器可以自行修改自己的糖果机类,本地虽然修改要打印的东西,两边是独立的,然后重点就是绑定的地址和lookup的地址能够ping 通而且对应上就好。
  • 而且生成stub的class里面应该主要实现的就是客户端这边要操作的方法。
  • 甚至你可以在台式机上删除那些具体的实现类,只留下两个接口和Monitor类和MonitorTest类,依旧没有任何问题

定义代理模式

代理模式为另一个对象提供一个替身或占位符以控制对这个对象的访问。
我的理解就是stub这个东西就是和socket很像,只是只绑定了ip,没有绑定端口的操作,两个通过ip和服务名称来进行通信,但通信的目的不是互传消息,而是为了本地这边调用自己的方法实际上可以访问对面的某些方法。

代理控制核心就是要创建代表,但根据具体情况还是分几种方式:
远程代理控制访问远程对象,上面的就是远程代理
虚拟代理控制访问创建开销大的资源
保护代理基于权限控制对资源的访问

代理模式类图以及介绍

代理模式类图

虚拟代理——从CD封面加载慢讲起

整个页面的某个地方加载有延迟,所以交给代理专门负责,这时候就可以用虚拟代理,虚拟代理帮助你不需要整个东西全部拿到才开始加载,而是先把下载的东西加载出来,等到图像或者什么难加载的东西加载好了,就替换掉虚拟代理原来呈现的东西。
所以虚拟代理作为创建开销大的对象的代表。虚拟代理经常直到我们真正需要一个对象的时候才创建它。当对象在创建前和创建中的时候,由虚拟代理来扮演对象的替身,对象创建后,代理就会将请求委托给对象。

public class ImageProxy implements Icon {
    ImageIcon imageIcon;
    URL imageUrl;
    Thread retrievalThread;
    boolean retrieving = false;

    public ImageProxy(URL imageUrl) {
        this.imageUrl = imageUrl;
    }

    @Override
    public void paintIcon(Component c, Graphics g, int x, int y) {
        if (imageIcon != null) {
            imageIcon.paintIcon(c, g, x, y);
        } else {
            g.drawString("loading cd cover,please wait...", x + 300, y + 190);
            if (!retrieving) {
                retrieving = true;
                retrievalThread = new Thread(() -> {
                    try {
                        imageIcon = new ImageIcon(imageUrl, "CD Cover");
                        c.repaint();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
                retrievalThread.start();
            }
        }
    }
}



import javax.swing.*;
import java.net.MalformedURLException;
import java.net.URL;

public class ImageProxyTestDrive {
    ImageComponent jComponent;

    public static void main(String[] args) throws MalformedURLException {
        new ImageProxyTestDrive();
    }

    public ImageProxyTestDrive() throws MalformedURLException {
        JFrame frame = new JFrame();
        Icon icon = new ImageProxy(new URL("https://y.gtimg.cn/music/photo_new/T002R300x300M000003DFRzD192KKD.jpg?max_age=2592000"));
        jComponent = new ImageComponent(icon);
        frame.getContentPane().add(jComponent);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(800, 600);
        frame.setVisible(true);
    }
}

因为图片是从URL出来的,所以有网络传输的时间,所以直接paintIcon的时候先判断有没有这个,第一次访问肯定没有,所以会检索这个icon,检索到了直接再次repaint()就好

这个要重点看ImageIcon,ImageProxy,Icon
Icon负责显示图片,共有的方法从这来,就相当于GumballMachineRemote定义要沟通的方法;
ImageIcon是一个图片具体实现显示的类,相当于GumballMachine具体实现GumballMachineRemote接口
ImageProxy就像是那些Stub类,也实现了GumballMachineRemote接口,但是目的是在中间放着帮助你实现远程连接的需求。

  • Stub类也是实现了GumballMachineRemote接口的

虚拟代理 vs 装饰者 vs 适配者

首先虚拟代理和装饰者真的有点像,都是用一个对象把另一个对象包起来,然后把调用委托给Icon。但是根据以前的套路,不用想肯定是目的不一样啊。
装饰者为对象增加行为,而代理是控制访问,虽然控制访问也可以看成一种行为,但是两者是解耦的。。。


写不下去了,我真的觉得这个是代理思想套在了装饰者模式了,装饰者模式的装饰方法改成这种异步加载的感觉你说是虚拟代理一点问题也没有,不过是什么模式不重要,解决问题最重要。
不过有一点很重要,装饰者的被包着的依然可能会被创建,但是虚拟代理一般只创建包过之后的对象

虚拟代理和适配器像的原因是因为两者都是当在一个对象的前面,并负责请求的转发,但是适配器修改接口,虚拟代理不修改。

心里话:其实开始学了几个的时候,区分的很开,后来的问题变得复杂一些,慢慢的都会有些重合,很难是非A即B的,这本书的思想是从目的出发或者是一些模式具有的独特性出发,重点是问题都能被正确方便的解决。

保护代理——从相亲怎么修改信息来讨论

java在java.lang.reflect有自己的代理支持,利用这个包你可以在运行时动态的创建一个代理类,实现一个或多个接口,并将方法的调用转发到你所指定的类。因为实际的代理类是在运行时创建的,我们称这个java技术为动态代理。

java已经创建了Proxy类,所以你需要有办法来告诉Proxy类你要做什么,而且你不能想以前一样把代码放在Proxy了(因为你用java自带的,又改不了)。因此解决方案就是:你控制的代码就要放在InvocationHandle里。
java内置了InvocationHandler接口,他的工作就是利用反射相应代理的任何调用,你可以把InvocationHandle理解成代理收到方法调用后,做具体工作的对象。

背景:相亲可以打分

首先创建一个人的基本信息,把它设置成接口PersonBean,里面可以得到基本信息也可以修改基本信息包括自己的相亲分数。
这一听就知道很不合理,因为你可以把自己的分改的很高就不客观了,甚至别人都能修改你的名字。
我们希望是get大家都可以,但是对于set,自己的分数自己不能改,但是自己的名字别人不能改。

现在我们要为PersonBean创建动态代理

  1. 步骤一:创建两个InvocationHandle,分别是这个PersonBean的Own和NotOwn两种代理。
  2. 步骤二:写代码创建动态代理
  3. 步骤三:利用适当的代理包装任何PersonBean对象,使用已经写好的代理来处理具体情况
步骤一

首先关于InvocationHandle,这个里面只有一个invoke的方法,不管代理调用的是什么方法,一定是在invoke方法运行。
首先说明一下这个场景下,代理返回的就是上面的PersonBean类型,不过一会还是要使用Proxy.newInstance()再强制类型转换的。


InvocationHandle工作流程分为三步:

  • 当proxy调用某个方法的时候,比如proxy.setHotOrNotRating(9)时,你要明白proxy并不是真的有这个setHotOrNotRating方法
  • 而是进入了public Object invoke(Object proxy, Method method, Object[] args)这个方法,这个时候会自动进行映射

    proxy ==> Object proxy
    setHotOrNotRating ==> Method method
    9 ==> Object[] args

  • invoke的方法具体会执行如何转发给RealSubject,比如return method.invoke(person, args),这里的person虽然也是PersonBean的实例,但并不是proxy,而是实现了PersonBean接口的PersonBeanImpl类,上面如果是用这个method.invoke(person, args)返回,那么就没什么差别,但是可以通过判断去throw new IllegalAccessException()告诉person你没权限调用这个


上面这部分的相关代码:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class NonOwnerInvocationHandler implements InvocationHandler {
    //把RealSubject要实例进来
    PersonBean person;

    public NonOwnerInvocationHandler(PersonBean person) {
        this.person = person;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            if (method.getName().startsWith("get")) {
                return method.invoke(person, args);
            } else if (method.getName().equals("setHotOrNotRating")) {
                return method.invoke(person, args);
            } else if (method.getName().startsWith("set")) {
                throw new IllegalAccessException();
            }
        }catch (InvocationTargetException e){
            e.printStackTrace();
        }
        //调用其他方法直接不理
        return null;
    }
}
步骤二&步骤三

这两步应该在一起,因为java已经有proxy类了,所以就是利用这个如何包装PersonBean对象让他在不同情况交给对应的InvocationHandle处理。
先放代码然后理解:

import java.lang.reflect.Proxy;

public class PersonProxyIntegration {

    public PersonBean getOwnerProxy(PersonBean person) {//设置Owner代理
        return (PersonBean) Proxy.newProxyInstance(
                person.getClass().getClassLoader(),
                person.getClass().getInterfaces(),
                new OwnerInvocationHandler(person));
    }

    public PersonBean getNonOwnerProxy(PersonBean person) {//设置非Owner代理
        return (PersonBean) Proxy.newProxyInstance(
                person.getClass().getClassLoader(),
                person.getClass().getInterfaces(),
                new NonOwnerInvocationHandler(person));
    }
}

首先说明PersonProxyIntegration这个类并不是代理,实际的代理类就是Proxy类(肯定是Object的子类),然后就是我们写的方法需要一个Person对象作为参数,然后返回它的代理,因为代理Proxy和RealSubject有同样的接口(Proxy类被强制转换),所以返回类型和参数类型相同。
其次,利用Proxy创建代理,用它的静态newProxyInstance()方法生成真实的代理实例,三个参数依次是PersonBean的类载入器 person.getClass().getClassLoader(),代理需要实现的接口person.getClass().getInterfaces(),代理要用到的处理器 new NonOwnerInvocationHandler(person),这就是Proxy和InvocationHandle协同工作的原理。

用户真实操作以为创建的是PersonBeanImpl,实际创建的是这个代理方法返回的PersonBean,别人操作的对方也是这个返回的,完美解决。

测试

对同一个人实例出来不同的代理返回值,没有太多内容。

代理的其他形式

防火墙代理:控制网络资源的访问,保护主体免于“坏客户“的侵害。
智能引用代理:当主题被引用时,进行额外的动作。
缓存代理:为开销大的运算结果提供暂时存储
同步代理:在多线程的情况下为主体提供安全的访问
复杂隐藏代理:用来隐藏一个类的复杂集合的复杂度,并进行访问控制。
写入时复制代理:用来控制对象的复制,方法是延迟对象的复制,直到客户真的需要为止。

《Head First 设计模式》读书笔记(10)—— 状态模式 State

本文共3000词,阅读需要10分钟,本文相关代码地址被托管在 gitee

卷首语

策略模式和状态模式是双胞胎,在出生时才分开
策略模式围绕可以互换的算法来创建业务的,而状态是通过改变对象内部的状态来帮助对象控制自己的行为。

栗子

背景

万能糖果公司,他们对于糖果售卖机有着一张状态图

每个圆圈代表一个状态图,要注意首先不能做不符合当前条件的事情,第二不能做没有意义的事情,所以对每一个状态的改变要判断清楚才行

开始动手做——状态机101

大致思路:首先找出所有状态,然后对每一个状态用一个变量来表示,接着对系统中的各种动作组合起来,注意状态机需要合适的条件判断

 public class GumballMachine {
    final static int SOLD_OUT = 0;
    final static int NO_QUARTER = 1;
    final static int HAS_QUARTER = 2;
    final static int SOLD = 3;
    int state = SOLD_OUT;
    int count = 0;

    //存储糖果数量
    public GumballMachine(int count) {
        this.count = count;
        if (count > 0) {
            state = NO_QUARTER;
        }
    }

    //当有25分钱投入,就会执行这个方法
    public void insertQuarter() {
        if (state == HAS_QUARTER) {
            System.out.println("如果已投入过25分钱,我们就告诉顾客不要再投");
        } else if (state == NO_QUARTER) {
            state = HAS_QUARTER;
            System.out.println("如果是在“没有25分钱”的状态下,你应该投25分钱");
        } else if (state == SOLD_OUT) {
            System.out.println("如果糖果已经售罄,我们就拒绝收钱");
        } else if (state == SOLD) {
            System.out.println("如果顾客刚才买了糖果,就需要稍等一些,好让状态转换完毕,恢复到“没有25分钱”的状态");
        }
    }

    //先在,如果顾客试着退回25分钱就执行这个方法
    public void ejectQuarter() {
        if (state == HAS_QUARTER) {
            System.out.println("如果有25分钱,我们就把钱退出来,回到“没有25分钱”的状态");
            state = NO_QUARTER;
        } else if (state == NO_QUARTER) {
            System.out.println("如果没有25分钱的话,当然不能退出25分钱");
        } else if (state == SOLD) {
            System.out.println("顾客已经转动曲柄就不能再退钱了,他已经拿到糖果了");
        } else if (state == SOLD_OUT) {
            System.out.println("如果糖果售罄,就不能接受25分钱,当然也不可能退钱");
        }
    }

    //顾客试着转动曲柄
    public void turnCrank() {
        if (state == SOLD) {
            System.out.println("别想骗过机器拿两次糖果");
        } else if (state == NO_QUARTER) {
            System.out.println("我们需要先投入25分钱");
        } else if (state == SOLD_OUT) {
            System.out.println("我们不能给糖果,已经没有任何糖果了");
        } else if (state == HAS_QUARTER) {
            System.out.println("成功,他们拿到糖果了," + "改变状态到“售出糖果”然后条用机器的dispense()方法");
            state = SOLD;
            dispense();
        }
    }

    //调用此方法,发放糖果
    public void dispense() {
        if (state == SOLD) {
            System.out.println("我们正在”出售糖果“状态,给他们糖果");
            count = count - 1;
            /* 我们在这里处理“糖果售罄的情况,如果这是最后一个糖果,将机器的状态设置到“糖果售罄”” 否则就回到“没有25分钱”的状态 */
            if (count == 0) {
                System.out.println();
                state = SOLD_OUT;
            } else {
                state = NO_QUARTER;
            }
        } else if (state == NO_QUARTER) {
            System.out.println("这些都不应该发生,但是如果做了,就得到错误提示");
        } else if (state == SOLD_OUT) {
            System.out.println("这些都不应该发生,但是如果做了,就得到错误提示");
        } else if (state == HAS_QUARTER) {
            System.out.println("这些都不应该发生,但是如果做了,就得到错误提示");
        }
    }

    @Override
    public String toString() {
        return String.valueOf(this.count);
    }
}

上面对每一个动作都对要发生的状态进行条件判断,然后基本没什么问题

新背景:变点花样——十个人中有一个可以得到免费糖果

当转动曲柄的时候,有10%的概率得到两个糖果
突然整个世界都乱了,因为你貌似需要修改每个方法的代码
上面的代码了违反了开放关闭原则,而且没有封装变化,而且是针对实现

状态模式实现

具体代码可以看gitee里面,我觉得重点就是
1.将原来的0,1,2,3改为一个具体的状态类来实现,每个状态类都包含一个售卖机参数,为的是同步售卖机里面的数量
2.不要if语句,这个基本不能修改
3.对各种状态用类实现,如果有新状态可以直接使用新类,对扩展开放
4.将每个状态下的行为局部化自己的状态类中
5.客户绝不会调用某个状态,这些状态都被放在售卖机中进行调用

定义状态模式

状态模式允许对象在内部状态改变时改变他的行为,而对象看起来好像修改了他的类。
这个说的确实很玄乎,其实就是将状态封装成独立的类,并将动作委托给当前状态,每个状态都是那些动作,但是不同的状态下同一个方法也是不同的行为
反过来对于使用者,你只是一直在使用某些类的某个方法,但是你根本没看到类的改变甚至类的存在

状态模式类图

状态模式 vs 策略模式

状态模式和策略模式的类图很相近,相同点:
1.都是将类中的某个行为用接口实现,比如鸭子会各种飞会各种叫,那就写个fly接口,写个quack接口,然后实现各种情况的飞和叫,而状态也是将一个context可能出现的各种情况写个接口,在context里面具体决定状态的各种改变等等
2.利用组合的方式都很有弹性,策略下具体怎么飞可以直接由使用者决定,状态的代码量感觉多了许多,也是因为使用了组合,使用者甚至都感觉不到有这个类

但是,我觉得还是很容易区别开,状态模式主要解决的事在一个context下很多if判断,让用的人永远只和这个context交互,而策略模式是让用户可以更加弹性的决定这个context里面具体长什么样

重新解决十个要中奖一个的问题

现在我们只需要重新定义一个中类的状态类然后稍加修改几个逻辑就好了
具体还是参见gitee吧

整体来看,当遇到的问题可以归结到一个context上,而且还能有状态图,而且需要很多if判断的话,状态模式简直神器

《Head First 设计模式》读书笔记(9)—— 迭代器与组合模式 Iterator and Composite

本文共7000词,阅读需要20分钟,本文相关代码地址被托管在 gitee,这一章的组合模式的外部迭代器有点难懂

有很多地方可以把对象堆起来成为一个集合,比如数组,堆栈,列表或者散列表(hashtable)
但是如果你想遍历这些对象,你肯定不希望别人看着你如何实现这个集合,因为这样极其不专业
最好应该是客户能够遍历你的集合到那时无法窥探你的存储方式。

栗子

背景

餐厅和煎饼屋合并,两个原来的菜单是用不同数据结构实现的,餐厅用的是ArrayList,而煎饼屋用的数组,他们两个都不愿意改变自己的实现,因为两者代码都依赖很多,都不容易改变。
1.数组实现的

public class DinerMenu {
    static final int MAX_ITEMS = 6;
    int numberOfItems = 0;
    MenuItem[] menuItems;

    public DinerMenu() {
        menuItems = new MenuItem[MAX_ITEMS];
        addItem("as","asd",true,0.1);
        addItem("as","asd",true,0.1);
        addItem("as","asd",true,0.1);
    }

    private void addItem(String a, String b, Boolean c, double d) {
        MenuItem menuItem = new MenuItem(a, b, c, d);
        if (numberOfItems >= MAX_ITEMS) {
            System.out.println("menu is full");
        } else {
            menuItems[numberOfItems]=menuItem;
            numberOfItems++;
        }
    }

    public MenuItem[] getMenuItems() {
        return menuItems;
    }
}

2.List实现的

public class PancakeHouseMenu {
    ArrayList<MenuItem> menuItems;

    public PancakeHouseMenu() {
        menuItems= new ArrayList<>();
        addItem("as","asd",true,0.1);
        addItem("as","asd",true,0.1);
        addItem("as","asd",true,0.1);

    }

    private void addItem(String a,String b,Boolean c,double d) {
        MenuItem menuItem = new MenuItem(a,b,c,d);
        menuItems.add(menuItem);
    }

    public PancakeHouseMenu(ArrayList<MenuItem> menuItems) {
        this.menuItems = menuItems;
    }

    public ArrayList<MenuItem> getMenuItems() {
        return menuItems;
    }
}

如果你现在希望实现一些功能,例如打印所有的价格,或者某些原料为肌肉的食品,总之你需要遍历一下
大概流程就不细说,总之你需要创建两个菜单对象,每个对象使用get方法拿到以后然后分别for循环,其中可以穿插一些if语句判断
其实你会发现每一个都是MenuItem对象。所以一定有办法可以简化。

找到不变的东西,然后封装——封装遍历

上面两个遍历其实java8用lambda表达式很容易弄,但是假如不用这些的话,那就需要for循环
List对应size(),get()方法;数组直接用length()和下标值

其实我们可以分别创建一个Iterator对象,就是迭代器,你会发现两者使用迭代器的代码竟然并没有什么差异,数组的迭代器

public class DinerMenuIterator implements Iterator {
    MenuItem[] menuItems;
    int position = 0;

    public DinerMenuIterator(MenuItem[] menuItems) {
        this.menuItems = menuItems;
    }

    @Override
    public boolean hasNext() {
        if (position >= menuItems.length || menuItems[position] == null) {
            return false;
        } else {
            return true;
        }
    }

    @Override
    public Object next() {
        MenuItem menuItem = menuItems[position];
        position = position + 1;
        return menuItem;
    }
}

List的迭代器:

public class PancakeHouseMenuIterator implements Iterator {
    ArrayList<MenuItem> menuItems;
    int position = 0;

    public PancakeHouseMenuIterator(ArrayList<MenuItem> menuItems) {
        this.menuItems = menuItems;
    }

    @Override
    public boolean hasNext() {
        if (position >= menuItems.size() || menuItems.get(position) == null) {
            return false;
        } else {
            return true;
        }
    }

    @Override
    public Object next() {
        MenuItem menuItem = menuItems.get(position);
        position = position + 1;
        return menuItem;
    }
}

于是主方法遍历的时候只是在使用两个迭代器

public class Menu {
    public static void main(String[] args) {
        DinerMenu dinerMenu = new DinerMenu();
        PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
        Iterator dinerMenuIterator = dinerMenu.createIterator();
        Iterator pancakeHouseMenuIterator = pancakeHouseMenu.createIterator();
        printMenu(dinerMenuIterator);
        printMenu(pancakeHouseMenuIterator);
    }

    private static void printMenu(Iterator iterator) {
        while (iterator.hasNext()){
            MenuItem menuItem = (MenuItem) iterator.next();
            System.out.println(menuItem.getDescription()+" "+menuItem.getPrice());
        }
    }
}

迭代器前后对比

原来需要两个循环,现在实现迭代器只需要迭代一个迭代器,而且不管多少都是循环一个
原来各种实现的方法中使用的都是具体的类,现在就是在面向一个接口
原来你可以看到具体实现的数据结构,现在你不知道里面到底有什么

使用java内置的Iterator接口


Collection接口里已经定义了了Iterator iterator()方法
而数组的实现则需要手动实现

import java.util.Iterator;

public class DinerMenuIterator implements Iterator {
    MenuItem[] menuItems;
    int position = 0;

    public DinerMenuIterator(MenuItem[] menuItems) {
        this.menuItems = menuItems;
    }

    @Override
    public boolean hasNext() {
        if (position >= menuItems.length || menuItems[position] == null) {
            return false;
        } else {
            return true;
        }
    }

    @Override
    public Object next() {
        MenuItem menuItem = menuItems[position];
        position = position + 1;
        return menuItem;
    }

    @Override
    public void remove() {
        if (position <= 0){
            throw new IllegalStateException("can't remove");
        }
        if (menuItems[position-1]!=null){
            for (int i = position-1; i < menuItems.length-1; i++) {
                menuItems[i]=menuItems[i+1];
            }
        }
        menuItems[menuItems.length-1]=null;
    }
}

定义迭代器模式

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
主要目的是就是访问的时候别人不知道你怎么实现的,使用统一的方法访问不同的聚合对象,意义就在于这样吧元素游走的责任交给了迭代器,让最后使用聚合和实现相关操作的接口变得很简单,让那些操作更专注到其功能上,而不是主要用在了遍历身上。

单一责任原则

一个类应该只有一个引起变化的原因

一个类应该由自己要完成的责任,而遍历这种责任如果维护起来的话,可能其本身的责任代码也要随着改动,所以一个类的责任代码就有了两个可能会引起变化的原因
所以我们应该将一个责任只指派给一个类,这其实很难,但是遍历这种同时出现,还是应该消除这种改变原因

新的背景:咖啡厅的菜单

增加一个用hashtable的咖啡店,然后按照相同的方法对hashtable的values进行遍历,其实还是和以前一样

这种做法让这个菜单确实变得有极大的扩展性

java还有很多的Collection接口,能够存取一群对象,他们都具有不同的接口,但是都能够实现迭代器,尤其是java8以后的lambda表达式

新背景:如果菜单还有子菜单

如果某个菜单里还希望添加一个甜点菜单,这是一个Menu对象,而不是一个MenuItem对象,所以原来那套有行不通了
现在的菜单应该是这个样子的:

我们需要重构

定义组合模式

组合模式允许你将对象组合成树形结构来表现“整体/部分”层次结构,组合能让客户以一致的方式来处理个别对象以及对象组合。
类图:

再看菜单

最后的菜单就是各种菜单对象的组合,现在利用组合模式设计菜单
上面所了就两种对象,一种Menu对象,一种MenuItem对象,现在写一个MenuComponent接口作为两种对象的超

public abstract class MenuComponent {
    public void add(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }

    public void remove(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }

    public MenuComponent getChild(int i){
        throw new UnsupportedOperationException();
    }

    public String getName(){
        throw new UnsupportedOperationException();
    }

    public String getDescription(){
        throw new UnsupportedOperationException();
    }

    public double getPrice(){
        throw new UnsupportedOperationException();
    }

    public boolean isVegetarian(){
        throw new UnsupportedOperationException();
    }

    public void print(){
        throw new UnsupportedOperationException();
    }
}


package headfirst.iterator_pattern.menu.five;

public class MenuItem extends MenuComponent {
    String name;
    String description;
    boolean vegetarian;
    double price;

    public MenuItem(String name, String description, boolean vegetarian, double price) {
        this.name = name;
        this.description = description;
        this.vegetarian = vegetarian;
        this.price = price;
    }

    @Override
    public String getName() {
        return name;
    }

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

    @Override
    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public boolean isVegetarian() {
        return vegetarian;
    }

    public void setVegetarian(boolean vegetarian) {
        this.vegetarian = vegetarian;
    }

    @Override
    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public void print() {
        System.out.println(this.getName());
        if (isVegetarian()){
            System.out.println("v");
        }
        System.out.println(this.getDescription());
        System.out.println(this.getPrice());
    }
}



package headfirst.iterator_pattern.menu.five;

import javafx.scene.input.InputMethodTextRun;

import java.util.ArrayList;
import java.util.Iterator;

public class Menu extends MenuComponent {
    ArrayList<MenuComponent> menuComponents = new ArrayList<>();
    String name;
    String description;

    public Menu(String name, String description) {
        this.name = name;
        this.description = description;
    }

    @Override
    public void add(MenuComponent menuComponent) {
        menuComponents.add(menuComponent);
    }

    @Override
    public void remove(MenuComponent menuComponent) {
        menuComponents.remove(menuComponent);
    }

    @Override
    public MenuComponent getChild(int i) {
        return menuComponents.get(i);
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public void print() {
        System.out.println(this.getName());
        System.out.println(this.getDescription());
        Iterator iterator = menuComponents.iterator();
        while (iterator.hasNext()){
            MenuComponent menuComponent = (MenuComponent) iterator.next();
            menuComponent.print();
        }
    }
}

关于组合模式的讨论

在迭代器模式中,你刚说一个类应该只有一种责任,单一责任原则
而现在一个类有两个责任,组合模式不但要设计其本身的层次结构,而且还要执行菜单的操作
这是不容争辩的,但是组合模式利用单一责任原则换取了“透明性”,通过让组件的接口同时包含一些管理子节点和叶结点的操作,客户就可以将组合和叶结点一视同仁,也就是用户对一个元素是组合还是叶结点是透明的。
这是一个很典型的折衷案例,尽管我们应该遵从设计原则,但是我们有时也会取舍一些原则,是为了一些更方便

内部迭代器和外部迭代器

以前女招待遍历打印整个菜单,其实她并没有使用迭代器模式去遍历,而是在每个菜单组件内部已经实现迭代遍历,这是内部遍历,但是如果你想一个迭代器直接遍历组件中所有的项,那就要使用外部的迭代器,而且需要使用栈结构来保存组合的层次结构,所以外部迭代器会看起来很复杂。

public class CompositeIterator implements Iterator {
    Stack stack = new Stack();

    public CompositeIterator(Iterator iterator) {
        stack.push(iterator);
    }

    @Override
    public boolean hasNext() {
        if (stack.empty()) {
            return false;
        } else {
            Iterator iterator = (Iterator) stack.peek();
            if (!iterator.hasNext()) {
                stack.pop();
                return this.hasNext();
            } else {
                return true;
            }
        }
    }

    @Override
    public Object next() {
        if (hasNext()) {
            Iterator iterator = (Iterator) stack.peek();
            MenuComponent menuComponent = (MenuComponent) iterator.next();
            if (menuComponent instanceof Menu) {
                stack.push(menuComponent.createIterator());
            }
            return menuComponent;
        } else {
            return null;
        }
    }
}

空迭代器——空对象设计模式的一种

比如上面的具体某道菜,它不像菜单内部也没什么可以遍历的,所以怎么实现组件的createIterator方法。
选择1:
直接返回null对象,但是在客户代码中你就要用条件语句来判断返回值
选择2:
返回迭代器,但是这个迭代器的hasNext()永远返回false
这样就不需要判断,只是它永远都返回的东西是不存在

public class NullIterator implements Iterator {
    @Override
    public boolean hasNext() {
        return false;
    }

    @Override
    public Object next() {
        return null;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

在命令模式中,曾经也用到过空对象模式,用空命令来代表初始化状态下的某个命令

个人总结

组合模式为了让客户免于检查自己的操作,把其可能设计的多种对象用一个相同的接口去实现,甚至采用抛出很多异常去实现。
组合模式通常内部的方法嵌入迭代器模式,如果是外部想进行迭代,通常因为要保存层次结构,会使用栈去存取。

《Head First 设计模式》读书笔记(8)—— 模板方法模式 template

本文共2500词,阅读需要10分钟,本文相关代码地址被托管在 gitee

背景一:其实咖啡和茶的冲泡方法很相似

两者都是把水煮沸,放进去茶叶或者咖啡然后泡好倒出来以后再加入牛奶等一些配料
你可以分别写出一个做咖啡的类和泡茶的类,你会发现两者非常类似,重复代码很多,所以我们应该将这些共同的部分剥离出来,放进一个基类里。

开始改造

在方法层面找出变与不变,类似策略模式

第一种想法写一个做饮料的超类,然后一些有不同的方法就不管了,子类具体实现,而那些共有的方法则直接在超类中实现,基本就是策略模式,找出变与不变,针对接口编程等等

public class Beverage {
     void prepareRecipe(){
         boidWater();
         pourInCup();
     }

    void boidWater() {
        System.out.println("water water");
    }

    void pourInCup() {
        System.out.println("pour");
    }
}
其实不变的东西还可以挖掘的更深

比如最后放配料,都是去放,不同的配料可以用参数传进去,继续从子类中逐步抽象出更多东西,其实比如浸泡茶叶和冲泡咖啡这两个也是很像的,可以合并,加牛奶还是柠檬都是加调料也是合并
于是就是可以直接一套流程在一个超类里全部实现,暂时无法确定的方法先用抽象方法占个位置,但是好处在于这个实现一套流程的方法你就不用管了

public abstract class Beverage {
    void prepareRecipe(){
        boidWater();
        brew();
        pourInCup();
        addConditions();
    }

    protected abstract void addConditions();

    protected abstract void brew();

    void boidWater() {
        System.out.println("water water");
    }

    void pourInCup() {
        System.out.println("pour");
    }
}

子类实现就把上面的两个抽象方法实现,子类直接使用prepareRecipe()是没有任何问题的
这就是模板方法模式,prepareRecipe()就是我们的模板方法:
首先这是一个方法,然后他又被用来作为一个算法模板完成一个共同目——制作出最后的饮料。
同时在这个方法中,他被认为是一种模板,因为他只是有一组方法组合,有些自己实现,有些子类具体实现。

此时如果实例化,比如Tea myTea = new Tea();
然后调用模板方法,myTea.prepareRecipe()
煮水是直接超类实现,专门泡则是具体茶那个子类实现

定义模板方法模式

模板方法模式是在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类不需要改变任何算法结构,只是在完成算法中某些步骤。

个人想法--车有些了,你看着买轮子吧

其实和策略模式在解耦方面其实差不多,都是使用超类来实现共用重复代码,类之间的继承结构也基本相同,但是模板方法模式最特殊的我认为是在模板两个字,模板以为你不用在想具体流程,只是实现就好,模板已经想好了结构是什么,模板已经规定好了1234,你只是根据具体情况实现某个1或者3就好;但是策略关键在于不用的就定个接口,然后组合进来

抽象方法中的挂钩把戏

钩子是一种被声明在抽象类中的方法,但是只有空或者默认的实现。
钩子的存在,可以让子类有能力对算法的不同点进行挂钩,要不要挂钩,由子类自己决定。

比如加调料这种事需要用户自己确定加不加,于是可以写个返回flag的方法结合条件判断来实现
这个返回flag的方法就是钩子,比如超类关于addCondiments()这个方法统一返回“加”,那具体实现的时候可以覆盖这个钩子方法,具体在判断加不加

真实api中这样的情况应该是很多的,以后我要留意一下

好莱坞原则——别调用我们,我们会调用你

个人感觉这也是区别策略模式的根本,也是能够定义算法结构的原因和保证
当使用模板方法模式时,我们告诉子类,不要来调用我们,我们都弄好了,你把你的干好就行了,用你的时候通知你

工厂方法和观察者模式也采用了好莱坞原则。
好莱坞原则成功避免了就是“依赖腐败”现象,例如超类调用子类的方法,子类也调用超类的方法,然后其他子类和这些又有调用关系,反正就是乱调,开发费劲,修改复杂。
于是定一条规定,我们不要毛遂自荐的,我们要用你的话会跟你说,就像大牌导演和十八线演员的关系。

饮料就是大牌导演,具体的咖啡和茶就是一个小演员,小演员只需要完成自己的工作,这些工作在导演那都已经规划好了。

好莱坞 与 依赖倒置

依赖倒置原则教我们尽量避免使用具体类,而多使用抽象
而好莱坞原则是用在创建框架或组件上的技巧,低层的被当作挂钩放进去,但是高层不会依赖低层
两者都是在于解耦,但是好莱坞原则强调低层之间可以互相操作,而防止其他类太过依赖

使用模板方法的接口或者类:Comparable接口 Comparator接口 Arrays类

钩子方法:compareTo()\compare()
只要实现Comparable接口的compareTo()方法,在使用相关方法时,如果传的参数是comparable接口的子类,就可以实现

Arrays类中的静态方法sort本身就是模板方法,需要参数,这个参数如果不用comparator接口,就是默认升序的规则

然后也可以使用两个参数,像上面,而compartor接口也具有模板方法。

Arrays.sort的目标就是希望能够满足任意数组并且满足任意排序规则,所以利用模板方法的思想,虽然并不想前面的使用方式,但是本质很像

边角料

  • 策略模式很像,两者也不要过度区分,但是其不同主要在于组合的子类是否完全实现整个算法。有的时候就是他自己一套算法,他又另外一套算法,这时候用模板方法就是作茧自缚,使用策略模式的委托机制就很有弹性很灵活
  • java.io的InputStream的read方法,也是使用了模板方法,具体怎么读可以自定义
  • 工厂方法就是模板方法的特殊版本。

《Head First 设计模式》读书笔记(7)——适配器模式与外观模式 Adapter and Facade

本文共2700词,阅读需要15分钟,本文相关代码地址被托管在 gitee,中间因为其他事情,20天才看完这一章,,,,

生活中的适配器

总所周知,在中国通用的电压时 220V,而美国电压则是 110V,如果有经常在美国和中国之间跑的 IT 人,而其笔记本都是随身携带的,那么它的笔记本的电压问题如何解决呢?

继续阅读“《Head First 设计模式》读书笔记(7)——适配器模式与外观模式 Adapter and Facade”

《Head First 设计模式》读书笔记(6)——命令模式 Command

本文共5500词,阅读需要15分钟,本项目相关代码地址被托管在 gitee

命令模式是将具体的一个或一组操作封装成一个独立命令的设计方法。
所有命令都实现同一个Command接口和其中的execute方法,调用者找到需要处理的命令并调用execute方法执行。

前言

在本章,我们将把封装带到一个全新的境界:把方法调用(method invocation)封装起来。通过封装方法调用,我们可以把运算块包装成形。所以调用此运算的对象不需要关心事情是如何进行的。只要知道如何使用包装成形的方法来完成它就可以。通过封装方法调用,也可以做一些很聪明的事情,例如记录日志,或者重复使用这些封装来实现撤销(undo)。

栗子

设计一个家电自动化遥控器的API。这个遥控器具有七个可编程的插槽,每个插槽都有对应的开关按钮,这个遥控器还具备一个整体的撤销按钮。希望你能够创建一组控制遥控器的API,让每个插槽都能够控制一个或一组装置,能够控制目前的装置和任何未来可能出现的装置,这一点很重要。(这里有一组Java类,这些类时由多个厂商开发出来的,用来控制家电自动化装置,例如点灯,风扇,热水器,音响设备和其他类似的可控制装置。)

上面是很多的厂商类,看不清不要紧。你只要知道它很多,控制各种电器。
有许多类都具备on()和off()方法,除外还有其他的一些方法。
遥控器应该知道如何解读按钮被按下的动作,然后发出正确的请求,但是遥控器不需要知道这些家电自动化的细节,或者如何打开热水器。
提示:命令模式可将“动作的请求者”从“动作的执行者”对象中解耦,在我们的题例中,请求者是遥控器,而执行者对象就是厂商类。利用命令对象,把请求(打开点灯)封装成一个特定对象(客厅点灯对象),如果对每个按钮都存一个命令对象,那么当按钮被按下的时候,就可以请命令对象做相关的工作,遥控器并不需要知道工作内容是什么,只要有个命令对象能和正确的对象沟通,把事情做好久可以了。

用餐厅来更好的理解命令模式

餐厅解析:研究顾客,女招待,订单,以及快餐厨师之间的交互。

1:你,也就是顾客,把订单交个女招待。
2:女招待拿了订单放在订单柜台,然后喊了一声“订单来了”。
3:快餐厨师根据订单准备餐点。

让我们更详细地研究这个交互

顾客知道自己要吃什么,并创建了一张订单createOrder(),订单包含一个订单表格,顾客订购的餐点项目写在上面。女招待拿走了订单tekeOrder(),放在订单柜台,然后调用orderUp()方法,通知厨师准备餐点。订单上有所有准备餐点是只是,知道厨师用类似makeBurger()这样的方法来烹饪。开餐厨师准备餐点。output()。

餐厅的角色和职责

一张订单封装了准备餐点的请求。
把订单想象成一个用来请求准备餐点的对象,和一般的对象一样,订单对象可以被传递:从女招待传递到订单柜台,或者从女招待传递到阶梯下一班的女找到,订单的接口只包含一个方法就是orderUp()。这个方法封装了准备从哪点所需的动作。订单内有一个到“需要进行准备工作的对象”(也就是厨师)的引用。这一切都被疯转起来,所以女招待不需要知道订单上有什么,也不需要知道是谁来准备餐点。

女招待的工作是接收订单,然后调用订单的orderUp()方法。

女招待其实不必担心订单的内容是什么,或者由谁来准备餐点,她只需要知道,订单有一个orderUp()方法可以调用,这就够了。

快餐厨师具备准备餐点的知识。

快餐厨师是一种对象,他真正知道如何准备餐点,一旦女招待调用orderUp()方法,快餐厨师就接手,实现需要创建餐点的所有方法,女找到和厨师之间是彻底的解耦,女招待的订单封装了餐点的细节,厨师只要调用每个订单的方法即可。
把餐厅想着一种设计模式的一种模型,而这个模型允许将“发出请求的对象”和“接收与执行这些请求的队形”分隔开来,对于遥控器API,我们需要分隔开“发送请求的按钮”和“执行请求的厂商特定对象”。

从餐厅到命令模式--第一个命令对象:单按钮遥控器

首先让所有的命令都实现相同的包含一个方法的接口,在餐厅的栗子,是orderup(),实际上一般是execute()

public interface Command {
    public void execute();
}

电灯命令实现

public class LightOnCommand implements Command {

    Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.on();
    }
}

现在只有一个遥控器,假如就有一个按钮

public class SimpleRemoteControl {
    Command slot;

    public SimpleRemoteControl() {
    }

    public void setSlot(Command slot) {
        this.slot = slot;
    }

    public void buttonWasPressed(){
        slot.execute();
    }
}

遥控器测试:

public class RemoteControlTest {
    public static void main(String[] args) {
        SimpleRemoteControl remote=new SimpleRemoteControl();
        Light light=new Light();
        //GarageDoor garageDoor= new GarageDoor();
        LightOnCommand lighton= new LightOnCommand(light);
        //GarageDoorOpenCommand garageopen = new GarageDoorOpenCommand(garageDoor);
        //remote.setSlot(garageopen);
        //remote.buttonWasPressed();
        remote.setSlot(lighton);
        remote.buttonWasPressed();
    }
}

定义命令模式

命令模式将“请求”封装成对象,以便使用不同的请求、队列、或者日志来参数化其他对象,命令模式也支持可撤销的操作。
我们知道一个命令对象通过在特定接受者上绑定一组动作来封装一个请求。要达到这个目的,命令对象将动作和接受者包进对象,这个对象就只会漏出一个execute()方法。
对比餐厅:command就是订单,一个人具体点的订单就像是一个具体的开灯命令,各种各样的按钮相当是各种各样的顾客,但是顾客本身定订单就像是告诉遥控器一些命令,遥控器就是女招待,订单告诉对应的厨师做相应的事情相当于把按钮按下的命令传过去各种对应的电器设备。
遥控器--invoker--女招待
各种电器设备--receiver--厨师
按钮--client--顾客
电器设备的指令--command--订单
按压对应指令的按钮--setCommand()--订单给女招待
按钮对应指令被电器设备响应--execute()--女招待打订单给厨师

命令模式类图

遥控器要升级

首先学习一种新的小模式,很常见——空对象模式

public class NoCommand implements Command {

    @Override
    public void execute() {

    }

    @Override
    public void undo() {

    }
}

没错NoCommand对象是一个空对象,当你不想返回一个又意义的对象时,空对象就很有用,客户也可以将处理null的责任转移给空对象,遥控器不可能一出厂就设置了有意义的命令对象,所以提供了NoCommand对象作为代用品,当调用它的execute()方法时,这种对象什么事情都不做
遥控器具体实现:

public class RemoteControl {
    Command[] onCommands;
    Command[] offCommands;
    Command undoCommand;

    public RemoteControl() {
        onCommands = new Command[7];
        offCommands = new Command[7];
        Command noCommand = new NoCommand();
        for (int i = 0; i < 7; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
        undoCommand = noCommand;
    }

    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    public void onButtowWasPushed(int slot) {
        onCommands[slot].execute();
        undoCommand = onCommands[slot];
    }

    public void offButtowWasPushed(int slot) {
        offCommands[slot].execute();
        undoCommand = offCommands[slot];
    }
    public void undoButtonWasPushed(){
        System.out.println("命令要被撤回");
        undoCommand.undo();
    }
    @Override
    public String toString() {
        StringBuffer stringBuff = new StringBuffer();
        stringBuff.append("\n------Remote Control------\n");
        for (int i = 0; i < onCommands.length; i++) {
            stringBuff.append("[slot" + i + "]" + onCommands[i].getClass().getName() + " " + offCommands[i].getClass().getName() + "\n");
        }
        return stringBuff.toString();
    }
}

还是实现一些设备对象以及启动关闭的命令,具体可以查看github
然后遥控器具体被使用:

public class RemoteLoader {
    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl();
        Light l = new Light("l1");
        LightOnCommand l1on = new LightOnCommand(l);
        LightOffCommand l1off = new LightOffCommand(l);
        remoteControl.setCommand(0, l1on, l1off);
        remoteControl.onButtowWasPushed(0);
        remoteControl.offButtowWasPushed(0);
        remoteControl.offButtowWasPushed(0);
        remoteControl.undoButtonWasPushed();
        CeilingFan ceilingFan = new CeilingFan("living room");
        CeilingFanHighCommand ceilingFanHighCommand = new CeilingFanHighCommand(ceilingFan);
        CeilingFanOFFCommand ceilingFanOFFCommand = new CeilingFanOFFCommand(ceilingFan);
        remoteControl.setCommand(1, ceilingFanHighCommand, ceilingFanOFFCommand);
        remoteControl.onButtowWasPushed(1);
        remoteControl.offButtowWasPushed(1);
        remoteControl.undoButtonWasPushed();
    }
}

希望遥控器带有party模式——一键启动

宏命令--同时调用多个命令

public class MacroCommand implements Command {
    Command[] commands;

    public MacroCommand(Command[] commands) {
        this.commands = commands;
    }

    @Override
    public void execute() {
        for (Command command : commands) {
            command.execute();
        }
    }

    @Override
    public void undo() {
        for (Command command : commands) {
            command.undo();
        }
    }
}

public class MacroCommandRemote {
    public static void main(String[] args) {
        Light light = new Light("living room");
        GarageDoor garageDoor = new GarageDoor();
        CeilingFan ceilingFan = new CeilingFan("sf");

        LightOnCommand lightOnCommand = new LightOnCommand(light);
        GarageDoorOpenCommand garageDoorOpenCommand = new GarageDoorOpenCommand(garageDoor);
        CeilingFanHighCommand ceilingFanHighCommand = new CeilingFanHighCommand(ceilingFan);

        Command[] on = {lightOnCommand, ceilingFanHighCommand};
        MacroCommand macroCommand = new MacroCommand(on);
        RemoteControl remoteControl = new RemoteControl();
        remoteControl.setCommand(0, macroCommand, macroCommand);
        remoteControl.onButtowWasPushed(0);
    }
}

命令模式的更多用途

队列请求

命令可以将运算块打包(一个接受者和一组动作),然后将它传来传去,就像是一般的对象一样,现在,即使在命令对象被创建许久之后,运算依然可以被调用,事实上,它甚至可以在不同的线程中被调用,我们可以利用这样的特性衍生一些应用,例如:日程安排,线程池,工作队列等。
想象有一个工作队列:你在某一端添加命令,然后另一端则是线程,线程进行下面的动作:从队列中取出一个命令,调用它的execute()方法,等待这个调用完成,然后将此命令对象丢弃,再取出下一个命令……

日志请求

更多应用需要我们将所有的动作都记录在日志中,并能在系统死机后,重新调用这些动作恢复到之前的状态。当我们执行命令的时候,将历时记录存储在磁盘中。一旦系统死机,我们就可以将命令对象重新加载,并成批地依次调用这些对象的execute()方法。
比如基本所有系统支持撤销命令,word可以恢复刚刚临时保存的文件。