lazytimes

lazytimes 查看完整档案

深圳编辑湖南化工职业技术学院  |  通用软件 编辑保密  |  java后端 编辑 lazytime.site/ 编辑
编辑

赐他一块白石,石上写着新名
日常摸鱼,有空才写博客
掘金地址:https://juejin.im/user/299912...
博客地址:https://whitestore.top/(site域名害人)
微信公众号:懒时小窝(刚起步)

个人动态

lazytimes 发布了文章 · 2月24日

浅谈设计模式 - 命令模式(七)

浅谈设计模式 - 命令模式(七)

前言:

命令模式也是一种比较常见的行为型模式,可以想象我们的手机智能遥控器,通过按动按钮的形式开启各种家具,说白了,就是将一系列的请求命令封装起来,不直接调用真正执行者的方法,这样比较好扩展。需要注意的是命令模式和策略模式相似,所以有时候可能容易弄混,这篇文章将会详细介绍命令模式

文章目的:

  1. 了解命令的模式的特点
  2. 简单对比命令模式和策略模式
  3. 命令模式的优缺点总结

什么是命令模式?

解释:把“请求”封装为对应的对象,使用不同的请求参数化对象,命令模式支持撤销撤销的操作

命令模式是一种行为型模式,实现了接口调用对象和返回对象,用命令对象作为桥梁实现调用者和具体实现者之间的解耦和交互。

命令模式的特点:

  • 将发出请求的对象和执行请求的对象解耦
  • 调用者可以自由定义命令参数进行自由的组合
  • 命令可以用来实现日志或者事务系统(undo操作)

命令模式结构图:

下面根据命令模式的定义,以及上面对于命令模式的理解,构建具体的结构图

+ Client 客户端:客户端需要创建具体的命令类,并且通过发送请求给执行者调用具体的对象,发送方和接收方不存在关联,统一由命令对象进行连接。

+ Invoker 执行者:请求的发送者,负责将请求分发给具体的命令实现类,由实现类调用实际的执行者进行执行操作

+ Command 接口:命令接口,定义命令的规范

+ ConcreteCommand 命令接口实现类:实现命令的同时组合具体对象。

+ ConcreteObject 具体实现类:定义截图的实现生产对象。

+ Receive 执行者:请求的真正执行者,可以是任意对象,通常以 组合形式出现在执行者的内部

命令模式的理解

这里参考《Head firtst设计模式》的案例,模拟具体的交互流程

对象村餐厅交互过程

我们到餐厅点餐,一般会经历如下的流程

  1. 客人负责下订单,由服务员接受订单
  2. 服务器接收订单,调用订单柜台的下订单的方法,不需要关注细节
  3. 订单柜台通知厨师进行生产
  4. 厨师生产订单物品之后,交给服务员上菜

根据上面的步骤利用伪代码的表现如下:

  • createCommandObject() 构建命令对象
  • setCommand() 传递命令
  • execute() 命令执行
  • action1()action2() 执行者实际执行

交互流程图

我们根据上面的交互过程介绍,构建具体的交互流程图,我们可以看到里面有角色:客人服务员订单柜台厨师,他们本身并没有关联,而是通过餐厅的形式彼此产生了具体的关联,同时我们对比上面的结构图,看下对象村餐厅对应的结构图:

下面根据结构图说一下各种角色的职责:

客人:相当于client客户端,负责指挥服务员进行下单的操作。

服务员:充当请求的发送者,接受客户端的请求,调用下订单的接口到具体的订单柜台,但是不需要关心具体的细节,只具备下订单这一个操作

订单柜台:通过服务员传递的订单,安排厨师执行具体的任务

厨师:根据订单柜台的订单做菜,将结果返回给服务员(或客人)

我们从上面的角色图再来看具体的命令模式定义,可以看到基本都是一一对应的情况。

命令模式和策略模式的对比

命令模式和策略模式的结构图有些许的类似,下面我们来对比看一下这两张图的异同:

策略模式结构图:

策略模式

命令模式结构图:

命令模式

相同点:

  1. 命令模式通过定义命令规范接口,由子类实现命令的执行细节,策略同样定义策略行为同时用子类实现不同的策略功能
  2. 命令模式和策略都解耦了请求的发送者和执行者

不同点:

  1. 命令模式利用了命令组合执行对象的形式执行实现具体实现,而策略模式依靠上下文对象进行切换
  2. 策略模式针对某个对象实现不同的策略效果,而命令模式关注请求发送者和实现者之间的业务解耦组合

实战

模拟场景:

​ 这次的案例还是模拟《Head First》设计模式的当中对于遥控器遥控电器的一个案例,我们定义如下的内容:

遥控器:命令的发送方,负责根据不同的操作按钮调用不同的设备工作,生成具体的命令对象调用接口执行具体的命令

命令接口:负责定义命令的实现规范,充当遥控器里面的每一个按钮,对应都有具体的实现

命令实现类:负责实现命令的接口,同时调用具体的实现对象执行命令

实现对象:命令的真正执行者,一般夬在命令实现类的内部,比如电视,灯泡等

不适用设计模式

在不使用设计模式的情况下,我们通常通过对象组合的形式组合不同的实体对象执行命令,下面通过一些简单的代码说明一下设计的弊端:

// 灯泡
public class Light {

    public void on(){
        System.out.println("打开灯光");
    }

    public void off(){
        System.out.println("关闭灯光");
    }

}
// 电视机
public class Television {

    public void on(){
        System.out.println("打开电视");
    }

    public void off(){
        System.out.println("关闭电视");
    }

}
// 遥控器
public class RemoteControl {

    private Light light;

    private Television television;

    public RemoteControl(Light light, Television television) {
        this.light = light;
        this.television = television;
    }

    public void button1(){
        light.on();
    }

    public void button2(){
        television.on();
    }
}
// 单元测试
public class Main {

    public static void main(String[] args) {
        Television television = new Television();
        Light light = new Light();
        RemoteControl remoteControl = new RemoteControl(light, television);
        remoteControl.button1();
        remoteControl.button2();

    }
}/*运行结果:
打开灯光
打开电视
*/

从上面的简单代码可以看到,如果我们继续增加电器,同时增加方法,不仅会导致遥控器要随着电器的改动不断改动,同时每次新增一个电器,遥控器要进行类似“注册”的行为,需要将电器接入到遥控器,这样显然是不符合逻辑的,因为我们都知道,遥控器是单纯的指挥者,他不参与任何命令的操作细节,同时虽然真正工作的方法是具体对象的方法,但是这种形式类似将电器“塞”到了遥控器的内部执行,这样也是存在问题,我们下面需要修改一下这种严重耦合的设计。

使用命令模式改写:

我们按照命令模式的结构图,改写案例,我们需要定义下面的类和对应的接口:

+ RemoteControl 遥控器
+ Command(接口) 命令规范接口,用于接入到遥控器内部
+ LightCommandConcrete 控制电器的亮灭命令实现
+ SwitchCommandConcrete 控制电器的开关命令实现
+ Light 灯泡
+ Television 电视机

首先,我们定义命令的接口,定义接口的规范方法。然后定义实现子类实现不同命令的操作效果,在命令实现类的内部,我们组合实际执行对象,在接口方法调用实际的对象方法,这样就做到了执行者和发送者之间的解耦。

接着,我们改写控制器,他不在持有任何实际的对象方法,通过组合命令的接口,让客户端传入实现的功能,通过这种方式,遥控器不在需要依赖具体的电器实现调用具体方法,而是关注命令的接口方法,一切的细节都在命令的子类内部。

下面代码是依照命令模式进行的最简单的一个实现。

// 命令接口
public interface Command {

    /**
     * 接口备份
     */
    void execute();

}

public class LightCommandConcrete implements Command {

    private Light light = new Light();

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

public class SwitchCommandConcrete implements Command{
    private Television television = new Television();

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

// 遥控器
public class RemoteControl {

    private Command command;

    public RemoteControl(Command command) {
        this.command = command;
    }

    public void execute(){
        command.execute();
    }

    public Command getCommand() {
        return command;
    }

    public void setCommand(Command command) {
        this.command = command;
    }
}

public class Main {
    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl(new LightCommandConcrete());
        remoteControl.execute();
        remoteControl.setCommand(new SwitchCommandConcrete());
        remoteControl.execute();
    }
}

经过上面的代码改造,我们成功上面的代码改造为命令模式的代码,使用设计模式之后,我们将调用者和实际执行者进行了解耦,控制器不需要知道执行的细节,只需要组合自己的命令接口,由客户端指定希望实现的内容,执行相对应的具体命令。

案例的额外扩展:

下面是对应案例如何进行后续的扩展,对于这部分内容文章篇幅有限,同时本着不重复造轮子的理念,请阅读《Head First设计模式》关于命令模式这一个章节,同时安利一下这本书,非常通俗易懂的讲解设计模式,对于个人的提升帮助很大。

对于上面的设计,如何加入Undo的操作?

Undo是一个很常见的功能,如果想要让Undo的操作集成到案例内部,需要按照如下的步骤进行操作:

  1. Command 接口增加Undo的操作,让所有命令支持undo
  2. 在控制器记录最后一个命令的执行对象,记录最后的操作命令,实现控制器支持undo操作
  3. 具体Command实现增加对于undo()方法调用,并且根据实际的组合对象调用方法
  4. 具体实现类实现undo()操作的具体行为效果。

如果undo里面,存在一些变量如何处理?

在命令的实现类内部,需要增加一个最后变量值的记录,用于记录当前最后一步操作的属性和变量

如何做到宏命令?

实现一个命令类,通过组合数组或者堆栈组合多个其他命令对象,通过for循环的形式依次调用。

undo也可以使用这种方式进行调用的,但是要注意**调用的顺序相反

命令模式的优缺点:

优点:

  • 命令模式实现了请求发送方和实现方解耦,不论是发送方还是接收方都不需要
  • 命令模式可以实现不同实现对象的自由组合,通过命令组合可以实现一连串简单功能

缺点:

  • 和策略模式类似,命令模式很容易造成子类的膨胀

总结:

​ 命令模式是一种非常常见的设计模式,这种模式更多的关注点是解耦请求的发送方和实现方,命令模式在系统设计中使用还是十分常见的,是一种值得关注的设计模式。

查看原文

赞 0 收藏 0 评论 0

lazytimes 发布了文章 · 2月19日

《代码简洁之道》读书笔记

《代码简洁之道》读书笔记

前言:

​ 这本书算是一本编程必读书,对于如何优化自己的代码很有帮助。这本书看了有将近一周的时间,更多时间花在了实践代码上面,按照书中给出的建议去思考和优化自己的代码,发现其中的过程其实是非常快乐的,这次的读书笔记介绍一下从这本书中学到了那些内容。

文章目的:

  1. 努力写出简洁的代码是程序员的基本素质,时刻关注各个细节
  2. 把简单的事情做到极致,才有能力做更有挑战的事情
  3. 收集《代码简洁之道》的书籍基本知识和内容点,回顾书籍个人认为比较重要的内容。
  4. 利用思维导图总结整个书籍的大致内容(导图内容较为庞大)

简述:

​ 作者通过最简单的为什么要编写代码到编写简洁代码的一些建议,从最简单的命名规范逐渐扩展到方法,类,系统,到最终利用一个实际的案例讲述作者亲身经历的一次代码重构体验,在难易度和节奏的把控比较到位,不需要担心一上来就是复杂的理论,非常具备条理。

​ 这本书的内容比较重要的是在于实战,提供大量代码和说明进行迭进式改造(虽然看的想睡觉),如果当做理论书去看把这本书简单扫一遍其实毫无意义。同时这本书全书围绕着“简洁”两个字,用了大量的案例和实际经验举证各种程序员写代码容易犯的错误,稍微看看就可以看到很多建议“很像”自己平时写代码的做法。

推荐程度:

强烈推荐,程序员必读书之一,作者提供了很多的建议如何写出整洁的代码,同时给出代码实战怎么处理代码。

如果想要看实战代码改进部分,可以查看附录B对于SerialDate这个类是如何进行迭进和改造的,对于学习改良代码十分有帮助。

最大的问题可能还是本书没有配套的源代码。书中许多对于代码的优化思路在阅读的时候不易理解作者思路

思维导图:

由于这本书的笔记单靠一篇文章很难梳理完,为了节省阅读文章的时间个人将书本的内容提炼笔记和重点精简为思维导图,读者可以按需参考思维导图阅读:

https://share.mubu.com/doc/2o...

幕布思维导图

书籍内容分析:

重点阅读:

下面列出几个比较重要的点,这里去掉了后面几章对于代码的迭进相关章节,注意这些内容比较重要,但是苦于找不到现成代码所以没有列入考虑范围(用纸看代码虽然可以但是十分痛苦并且难以理解,个人认为效率太低),所以只列出前面建议的部分以及最终的总结部分。

PS:下面的内容摘录自思维导图
  • 第三章:函数

    • 重点

      • 时刻保持参数的数量控制
      • 函数要返回期望的内容,运行期望的行为
      • 避免副作用的方法
      • 取个好名是好方法的关键步骤
    • 简介

      • 介绍如何写出更加简单并且令人夸赞的好方法
      • 函数式封装的第一个步骤,时刻练习如何写出好的函数
  • 第四章:注释

    • 重点

      • 写出好代码才是关键,先写出好简洁代码,再考虑写好注释
      • 最好的状态是代码本身就能诠释注释
      • 努力写出简单易懂的好注释
      • 好注释不仅可以让阅读者理解,还可以让阅读者学到新知识
    • 简介

      • 写一个注释容易,但是写好一个注释不容用。
      • 程序员因为各种“借口”懒得写注释
      • 如果你的代码足够具备表达力,就不需要注释,否则请加上你的注释
  • 第七章:异常处理

    • 重点

      • 异常处理的一些技巧

        • 用单独的方法进行try/catch
        • 从大的异常到细化异常
      • 不要返回Null值
      • 异常需要作为单一职责看待,而不是和方法捆绑
    • 简介
  • 第九章:单元测试

    • 重点

      • 测试单元:一个测试一个断言,测试单元尽可能简短
      • FIRST原则

        • F:快速

          • 测试可以快速进行
        • I:独立

          • 测试之间相互独立
        • R:可重复

          • 测试在任何环境都可以通过
        • S:自足验证

          • 存在布尔值输出,可以验证自己的结果
        • T:及时

          • 测试要及时进行编写
    • 简介

      • 学习使用TDD测试驱动开发的形式
      • 单元测试是非常重要并且必要的

        • 测试的好处
        • 整洁的测试
      • TDD的三大定律

        • 编写不能通过的测试代码之前,不编写生产代码
        • 只编写刚好无法通过的单元测试
        • 只编写刚好足以通过测试的生产代码
  • 第十章:类

    • 重点

      • 少量的大类并不一定比大量小类好管理
      • 用尽可能少的类和方法完成目的
      • 类不应该有太多的权责和干扰
    • 简介

      • 关键内容在于类的权责拆分

        • 当失去内聚的时候就想办法拆分
        • 生活抽象:你是要一个装任何东西的百宝箱还是一个布满各种放个的工具箱
  • 第十七章:味道和启发 #重点

    • 重点

      • 总结全书的一些要点
      • 回顾全书的重点内容
    • 简介

      • 非常重要的一个章节,用一个章节概括了全书的一些重要内容
      • 可以从最后一章确定全书要阅读的部分

作者的核心思想:

从头到尾认真仔细阅读下来,做了很多的笔记,下面就个人理解说一下作者的几个核心思想

  • 单一职责:不管编写简单还是复杂的代码,都需要关注权责的拆分。
  • 简洁:要想尽一切办法努力优化自己的代码,养成一个良好的编程习惯。
  • 迭进:作者通过大量的案例和代码表达对于代码是如何一步步进行迭代改进的,要不断的思考和改良
  • 注释:好注释真的非常非常重要,虽然注释这一章其实更像是作者的吐槽和埋怨
  • 批判精神:不在乎水平的高低,敢于去重构代码,并且反省和思考
  • 重构:看似每一个无关紧要的细节的改动对于系统构建产生的魔力变化

序章

序章可以明白作者写这本书的用意,作者鼓励读者对于代码提出挑战,并提出了如下的观点:

  • 神在细节之中
  • 5S哲学

    • 整理:命名规范
    • 整顿:每段代码在期待的地方
    • 清楚:注释和处理应用diamante
    • 清洁:组织结构清晰
    • 身美:乐于改进

首章要有代码

作者在第一章不是上来就介绍如何编写好代码或者一些技巧,而是探讨提倡的AI编程(其实很早就有这个概念了)以及编写代码的重要性,下面记录书中提出的对于编写代码的一些态度,个人理解是作者对代码存在极致的追求,所以开头便鼓励读者要写出 整洁的代码:

  • 代码永存,必须要要有代码
  • 勒布朗法则:稍后等于永远不做
  • 不要把烂代码归咎为外因
  • 好代码的前提

    • 果断敢绝,就事论事,没有犹豫和无用细节
    • 外表或者举止令人愉悦,优美与雅致

味道和启发

个人建议时间比较紧迫或者想要简单了解全书大致内容的,直接去阅读最后一个章节“味道与启发”,本部分内容直接介绍了全书的一些核心理念,相当如作者给你做了一次总结,非常良心。建议重点阅读本章节的内容,里面用了非常多的小点介绍了一些平时编写简洁代码的建议。

附录部分内容:附录的内容比较容易被忽略,虽然是对于并发模块和测试检测异常做了一个入门的介绍。这部分作者讲了个人亲身经历调优并发代码的故事比较有意思。

附录部分摘录

这里记录附录部分关于如何计算线程的路径数量笔记

如何计算路径数量:

假设有N个指令和T个线程,将会产生T*N个步骤

假设有步骤AB和线程12,将会有如下的情况:

1122、1212、1221、2112、2211、2121

他们的特征是:每个T会出现N次,因为不管顺序如何执行,执行肯定会被执行完成

这样就引出一个公式:

(N*T)! / N!^T (!为阶乘操作)

举例:对于有2个步骤和2个线程的代码, 带入公式可得:(2*2)! / (2!)^2 = 24 / 4 = 6

将会出现6种情况

那么这时候就会有疑问了,我们synchronized到底改变了什么?

我们按照上面的假设,有2个线程得出来的结果是 N! 就是 指令的阶乘变为常数2

个人感悟:

​ 对于个人来说这本书物超所值,干货满满的一本书,不是单纯的讲空话,而是针对代理的代码案例进行吐槽,特别是注释这一个章节,以前喜欢写很多的注释生怕同事看不懂过来询问(当然牵扯业务问题很乐意与同事讨论),但是看完书感觉以前做的事情有些多余。个人在工作之后,逐渐养成了编写文档的习惯,不管需求大小,总是喜欢写一写文字记录一下需求以及自己的思路轮廓,实践下来发现还是挺有用的,虽然事后会觉得文档写的很啰嗦或者有时候会忘记写过文档=-=,而且有些可能还不算十分准确,但是对于自己回顾业务和需求确实有一定的帮助。总之,非常庆幸自己坚持下来读完整本书。

下面是个人的一些感悟,同时如果不想看书,可以参考下面列出个人比较建议的点:

  • 边看边思考,边看边实践。实践最重要
  • 代码不是一步到位的,代码永远都没有最优解,根据自己的能力要不断摸索更优解。刻意练习
  • 不想看书的一些建议点

    • 单一职责

      • 代码是否真的只做了一件事,是否在自己骗自己
      • 验证是否只做一件事的万金油法则:加需求
      • 方法是否是“全包干”
    • 不要重复自己

      • "CV"编程会让水平停留在"CV"
      • 现代的编辑器可以减少很多思考
    • 什么是简洁

      • 没有注释,读代码就像在读业务逻辑
      • 没有过时的注释,也没用错误的注释,更没有没用的注释
      • 用最少的代码做最大的事情,代码复用度高
      • 没有重复,单一职责
    • 迭进

      • 学会拆分职责和剥离职责
  • 思考何为面向对象

    • 不断的改进,写出更简洁的代码
  • 批判:不断接受批评,才能不断进步

下面是个人想到的一些日常生活中碰到的参考案例以及一些小思考,读者可以写下自己碰到或者遇到的编程习惯进行批判:

  • 在做需求之前,写一份需求文档的草稿。不要求十分的精确,但是要简略的说明改动的理由

    • 介绍需求
    • 思路
    • 解决步骤1、2、3
  • 喜欢一行代码一行注释,在保证完成任务的前提下补齐个人的注释代码

    • 写代码不是写文章,不需要面面俱到
    • 大量的注释容易埋没代码本身的价值
  • 容易编写许多参数的方法,并且通常容易忽视

    • 该需求喜欢喜+1,这周情况不妙
  • 不自觉的写出很多无用注释,和书中的喃喃自语相同
  • 改正喜欢写很多注释的习惯
  • 改正喜欢CV代码的习惯
  • 多想想自己写的代码是否符合设计规则,是否是在重复劳动
  • 让方法和参数的命名更加的符合规范
  • 复查代码,注意代码的格式和行数
  • 思考设计模式思想能否在代码层面应用。
  • 减少参数名,多注意方法的取名
  • 偷懒是烂代码的本质原因

书本总结:

​ 这本书更多的还是建议将书中的观点带入到实际的代码当中,会有比较好的效果,因为作者提出的很多问题确实从自己的代码当中显而易见(冗余的注释,累赘的方法等等)

​ 先说说这本书的优点,虽然年代可能比较久远,而且某些思想可能不是非常符合现代的开发思想,但是依然可以看到很多值得借鉴的地方,比如要时刻注意细节,并且要敢于打破自己的代码思考新的方向,个人从书中查漏补缺,反省了一下自己需要改正的一些编程思路,对于提升个人的编程水平确实是有实际作用的。

​ 再说说缺点,首先是代码部分,个人认为部分案例代码来的比较突兀,同时由于很多代码不存在上下文的关联,虽然作者进行了解释但是依然让读者摸不着头脑(当然也可能是个人理解能力问题),总之不是部分代码案例确实云里雾里。其次是书中包含的代码量较多,需要较长的时间消化和吸收,比较影响沉浸式的阅读体验。当然这本书更多的也是支持读者去自己实践。

​ 这本书个人比较遗憾的是代码部分,因为书中作者从12章之后介绍如何迭进代码,加入使用实际的案例如何改写“烂”代码的,粗略看了下虽然可以理解作者想要尽力的告知读者一些优化思想,但是个人在网络上寻找很久依然没有找到相关的源码,比较遗憾。同时篇幅多大几页的代码以及没有上下文的代码在书本当中有些让人望而却步。

作者在书中说建议查看《字面编程》这本书135页对于权责拆分的良好案例,后续会花时间去研究一下

精句阅读:

只有通过批评,我们才能学到东西。医生就是这样做的,飞行员就是这样做的,律师就是这样做的,我们程序员也需要学习如何这样做。(书本的252页)

这里偷懒直接截图了=-=

总结:

​ 更多的内容欢迎查看思维导图,为了避免文章的内容过长,将更多的细节放置到思维导图帮助自己的思考和回顾。后续将会阅读《重构》这本书,虽然也是一本老书,但是看到很多人进行了推荐所以也买了一本翻翻看。这本书也花了很多的心思编写读书笔记,希望读者给予建议。

查看原文

赞 0 收藏 0 评论 0

lazytimes 发布了文章 · 2月17日

浅谈设计模式 - 工厂模式(六)

浅谈设计模式 - 工厂模式(六)

前言:

在第一篇里面已经介绍过简单工厂了,但是工厂模式里面不仅仅是简单工厂,还存在工厂方法和抽象工厂,并且从严格意义来讲简单工厂不能算是一种设计模式,本次的文章针对工厂的进化来展开讲一讲工厂模式的三种常见形式:简单工厂、工厂方法、抽象工厂。

文章目的

  1. 了解简单工厂这种代码编写形式的优点,回顾工厂模式
  2. 了解如何从简单工厂扩展到工厂方法以及抽象工厂
  3. 对比工厂方法和抽象工厂的异同。
  4. 总结简单工厂,工厂方法和抽象工厂,对比优缺点和特点

如何辨别工厂模式

工厂模式一般从类的命名就可以直接看到含义,所以一般情况下很容易看出工厂模式的应用。

  • 工厂模式主要是负责对象的创建
  • 无论是创建者还是使用者,都是针对一个抽象对象的实现。
  • 工厂模式最关注的是对象是如何创建的而不是对象的使用。它针对的是创建这一个过程。

工厂模式的具体介绍

简单工厂模式

简单工厂模式的介绍:https://juejin.cn/post/692206...

之前文章已经介绍过简单工厂模式,我们直接看一下简单工厂是如何设计的,从严格的意义上来说,简单工厂是一种良好的“编程习惯”,他很好的解耦了创建对象和使用对象这两个不同的过程。做到“单一职责”的原则

简单工厂

从上面的图当中我们构建基本的工厂类和对应的实现子类以及对应的产品抽象类。

下面回顾一下简单工厂的优缺点

优点:

  1. 使用创建工厂的方法,我们实现了获取具体对象和生产对象的解耦,由生产对象的工厂通过我们传入的参数生产对应的对象,调用方只需要传递需要生产的对象来实现具体的效果。
  2. 解耦了创建被创建的过程。
  3. 根据不同的逻辑判断生成不同的具体对象。

缺点:

  1. 每增加一个工厂对象具体的实现类,就需要增加if/else不利于维护
  2. 大量的子类会造成工厂类的代码迅速膨胀和臃肿
  3. 简单工厂的方法一般处理简单的业务逻辑,如果创建逻辑复杂不建议使用。

从上面的优缺点分析可以知道,简单工厂并不能完全解决对象的创建解耦,对于对象的创建细节容易造成耦合,同时如果创建的对象过多容易出现臃肿的工厂代码。

工厂方法模式

工厂方法模式:定义了创建对象的接口方法,但是具体的创建过程由子类来决定。工厂方法将创建的过程延迟到子类,工厂方法是对简单工厂的扩展和升级,为了解决简单工厂破坏了“开放-关闭原则”的问题而做的改进。我们将具体的产品进行了抽象的同时,将创建对象的过程延迟到子类进行实现。

工厂方法的结构图

下面为工厂方法的结构图,我们由简单工厂转变为工厂方法之后,工厂类定义增加了抽象的对象创建方法,由子类通过继承的方式实现工厂的抽象方法并且实现自己的构建过程。

工厂方法

+ Product 产品类,定义产品的公用方法和抽象类
+ ConcreteProduct 产品的具体实现子类,包含具体产品的实现
+ Factory 工厂类,定义工厂的创建方法以及需要子类继承实现的方法
+ ConcreteFactory 工厂的实现类,由子工厂来决定生成的具体产品和定义生产的具体过程。

工厂方法的特点

下面是工厂方法的具体特点

  • 创建的过程解耦到子类,由子类决定创建的过程和结果
  • 具体的产品和工厂之间存在必要关联,同时可以使用任意子类产品进行替换
  • 需要依靠继承的形式由子工厂来决定生产的过程,子类决定产品创建的结果
提醒:子类决定创建的结果并不是字面上的创建,而是由调用者决定的。子类决定的是具体针对哪一个实例进行生产,但是生成的具体结果还是控制在创建者的身上

简单工厂和工厂方法有什么区别

  1. 简单工厂是对产品的创建过程进行“封装”,同时创建新的产品必须改动工厂代码。
  2. 工厂方法是对简单工厂的升级,工厂方法可以控制具体对象的创建以及由子类来决定具体需要创建哪一个对象。
  3. 简单工厂只是单纯的解耦创建者和使用者,但是简单工厂无法改变创建的结果。

抽象工厂模式

抽象工厂模式:提供接口,通过定义抽象方法的形式,通过实现具体工厂方法实现创建具体对象家族,同时不需要指定特殊的类。

抽象工厂的内部往往使用工厂方法进行实现,两者经常被弄混,从结构上来看,他们最大的区别在于工厂方法往往使用继承实现,而抽象工厂往往使用内部继承工厂方法的接口实现。区分工厂方法和抽象工厂也是工厂模式的学习关键。

抽象工厂的结构图

由于抽象工厂更像是对工厂方法的改进,我们定义抽象工厂的结构图,抽象工厂的结构相比工厂方法要复杂一些:

可以参考抽象工厂和工厂方法的结构图,看看两者的异同

抽象工厂

+ FactoryInterface     抽象工厂接口,定义一批抽象对象的生产接口
 + ConcreteFactoryA    抽象工厂实现类A,实现抽象工厂接口。
 + ConcreteFactoryB    抽象工厂实现类B,实现抽象工厂接口。
+ ProductA            抽象产品A,定义公共的抽象方法或者公用属性
 + ConcreteProductA    具体实现产品A
 + ConcreteProductA    具体实现产品A
+ ProductB            抽象产品B,定义公共的抽象方法或者公用属性
 + ConcreteProductB    具体实现产品B
 + ConcreteProductB    具体实现产品B

抽象工厂的特点:

  1. 所有的具体工厂都实现同一个抽象工厂接口。
  2. 生产的结果实现类可以自由实现具体类或者其扩展类的实例。
  3. 抽象工厂的痛点在于扩展一个新的产品生产会造成所有的具体工厂的改动,也包含了产品类的变动。
  4. 抽象工厂往往包含了一系列的工厂方法

抽象工厂和工厂方法的区别

  1. 抽象工厂定义抽象接口依靠子类实现创建的过程,而工厂方法针对子类实现具体的对象创建细节
  2. 工厂方法需要使用继承的手段实现工厂方法“埋藏”工厂创建具体对象的细节
  3. 工厂方法对于处理“独立”产品的创建非常有效,而抽象工厂往往用于处理生产多个存在关联的产品对象。

实际案例

依旧参考坦克大战的案例,介绍如何改造坦克大战的具体代码。

模拟场景

依然以经典的任天堂游戏坦克大战为例,在进入游戏的关卡的时候,会出现我方的坦克和敌人的坦克,我方坦克和地方坦克不仅形状不同,而且很脆,但是敌人的坦克根据颜色需要打好几枪才会毁灭,那么如果用代码来模拟是什么样的呢?

简单工厂实现:

使用简单工厂实现的代码如下:

使用简单工厂类来管理坦克的创建过程,简单工厂顾名思义,就是简单的将创建对象的过程进行管理。

增加工厂类 TankFactory.java

用工厂来管理具体的坦克创建过程:

/**
 * 坦克工厂,专门负责生产坦克
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/25 22:27
 */
public class TankFactory {

    /**
     * 创建坦克
     * @return
     */
    public Tank createTank(String check){
        Tank tank = null;
        if(Objects.equals(check, "my")){
            tank = new MyTank();
        }else if(Objects.equals(check, "mouse")){
            tank = new MouseTank();
        }else if (Objects.equals(check, "big")){
            tank = new BigTank();
        }else {
            throw new UnsupportedOperationException("当前坦克不支持生产");
        }
        return tank;
    }
}

下面是对应的坦克以及坦克的子类实现

/**
 * 坦克的父类,定义坦克的行为
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/25 0:14
 */
public abstract class Tank {


    /**
     * 坦克hp
     */
    protected int hp;

    /**
     * 坦克子弹
     */
    protected List<Object> bullet;

    /**
     * 移动的方法s
     */
    abstract void move();

    /**
     * 攻击
     */
    abstract void attack();

    /**
     * 停止
     */
    abstract void stop();
}

我方的坦克继承坦克的父类:

/**
 * 我方坦克
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/25 21:58
 */
public class MyTank extends Tank {

    public MyTank() {
        // 我方坦克假设只有一条命
        hp = 1;
        bullet = new ArrayList<>();
        // 初始化添加三发子弹
        bullet.add(new Object());
        bullet.add(new Object());
        bullet.add(new Object());
    }

    @Override
    void move() {
        System.err.println("移动");
    }

    @Override
    void attack() {
        System.err.println("攻击地方坦克");
        // ..弹出子弹
        if(bullet.size() == 0){
            System.err.println("没有子弹了");
            return;
        }
        bullet.remove(bullet.get(bullet.size() -1));
    }

    @Override
    void stop() {
        System.err.println("停止");
    }
}

敌人的坦克如下:

/**
 * 老鼠坦克
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/25 22:02
 */
public class MouseTank extends Tank implements Runnable {

    public void display() {
        System.err.println("长得尖尖的,很像老鼠");
    }

    public MouseTank() {
        // 坦克假设只有一条命
        hp = 1;
        new Thread(this).start();
        bullet = new ArrayList<>();
        // 初始化添加六发子弹
        bullet.add(new Object());
        bullet.add(new Object());
        bullet.add(new Object());
        bullet.add(new Object());
        bullet.add(new Object());
    }

    @Override
    void move() {
        System.err.println("老鼠坦克移动");
    }

    @Override
    void attack() {
        System.err.println("老鼠坦克开枪");
        // ..弹出子弹
        if (bullet.size() <= 0) {
            System.err.println("老鼠坦克没有子弹了");
            return;
        }
        // 老鼠坦克一次性开两枪
        bullet.remove(bullet.get(bullet.size() - 1));
    }

    @Override
    void stop() {
        System.err.println("停止");
    }

    @Override
    public void run() {
        while (true) {
            // 一旦创建就开始移动
            move();
            // 漫无目的开枪
            attack();
            attack();
            // 做完一轮操作歇一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 随机停止
            if (new Random(100).nextInt() % 2 == 0) {
                stop();
            }
        }
    }
}

最后编写单元测试如下,我们使用简单工厂生产出不同的坦克,但是客户端不需要纠结生产的细节:

/**
 * 单元测试
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/25 22:15
 */
public class Main {

    /**
     * 我们将生产坦克的过程全部交给了工厂来处理
     * 可能还是奇怪,这和刚才没有什么区别呀?
     * 我们来看下区别:
     * 1. 创建的过程没有了,虽然是一个简单的new,但是new的过程交给了工厂
     * 2. 我们后续如果要在坦克加入别的东西,只需要去改工厂类和具体的实现类,不需要该此处代码
     * 3. 如果不支持的操作,工厂还可以通知我们这样做不对
     * @param args
     */
    public static void main(String[] args) {

        TankFactory tankFactory = new TankFactory();

        Tank my = tankFactory.createTank("my");
        Tank mouse = tankFactory.createTank("mouse");
        Tank big = tankFactory.createTank("big");
        // 我要一个没有的设计过的坦克
        Tank mybig = tankFactory.createTank("mybig");


    }/*//
    运行结果:
    Exception in thread "main" 老鼠坦克移动
    巨型坦克移动
    老鼠坦克开枪
    巨型坦克开枪
    老鼠坦克开枪
    java.lang.UnsupportedOperationException: 当前坦克不支持生产
    at com.headfirst.factory.use.TankFactory.createTank(TankFactory.java:27)
    at com.headfirst.factory.use.Main.main(Main.java:33)
    */



}

从上面的代码可以看到,对于坦克的创建和使用过程虽然进行解耦了,但是可以看到创建的过程耦合在了简单工厂的内部,工厂创建的方法耦合了过多的细节,同时如果需要创建新的产品需要改动工厂代码,这违背了开放-关闭原则。

针对上面的问题,我们显然需要使用工厂方法进行改良,我们让工厂的创建细节延迟到子类去实现,子类只需要关注创建的细节,不需要了解客户端的调用,下面我们针对上面的代码使用工厂方法进行改进。

这种改动也符合开放-关闭原则

工厂方法实现:

从简单工厂可以看出,如果每次修改产品都需要牵动工厂的代码改动,同时针对创建的过程都被“耦合”在单独的工厂创建方法内部,我们根据工厂方法的结构图看一下如何改进坦克大战的代码:

工厂方法

首先,我们需要依照工厂方法的定义,将原本的简单工厂类改造为具备工厂方法的工厂,在下面的代码当中,工厂类具备两个方法,一个用于创建具体的对象,由客户端调用,并且提供一个抽象的方法,由工厂子类实现并且定义具体的工厂生产过程。

/**
 * 坦克工厂
 * 工厂增加抽象方法由子类进行构建
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/16 17:33
 */
public abstract class TankFactory {

    /**
     * 创建坦克
     * @return
     */
    public Tank createTank(String check){
        return createConcreteTankMethod(check);
    }

    /**
     * 构建具体产品过程的方法
     * @return
     */
    protected abstract Tank createConcreteTankMethod(String check);


}

子类不需要关心createTank()方法是如何运行的,只需要实现自己的工厂方法同时定义生产的细节提供支持即可。

下面的代码为我方坦克的生产工厂

/**
 * 我方坦克的创建工厂
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/16 14:28
 */
public class OurTankFactory extends TankFactory {

    @Override
    public Tank createConcreteTankMethod(String check) {
        Tank tank = null;
        if(Objects.equals(check, "my")){
            tank = new MyTank();
        }
        return tank;
    }
}

下面的代码为敌人的坦克的生产工厂实现子类。

/**
 * 敌人坦克的构建工厂
 * 老鼠坦克
 * @author zxd
 * @version 1.0
 * @date 2021/2/16 14:28
 */
public class MouseTankFactory extends TankFactory {

    @Override
    public Tank createConcreteTankMethod(String check) {
        Tank tank = null;
        if(Objects.equals(check, "mouse")){
            tank = new MouseTank();
        }
        return tank;
    }
}

通过这样的调整之后,我们每次增加新的产品,只需要继承具备工厂方法的工厂并且实现对应的方法完成自己的坦克创建细节,就将原本耦合的创建规则从父类从剥离,延迟到子类完成,下面来看下单元测试的代码,可以看到工厂的生产具体具体化到子类工厂的内部,而对外依旧是坦克的生成工厂,这样既符合依赖倒转的原则,也方便后续的扩展和更多实现工厂的添加:

/**
 * 工厂方法的单元测试
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/16 17:50
 */
public class Main {

    public static void main(String[] args) {
        TankFactory tankFactory = new MouseTankFactory();
        TankFactory ourTankFactory = new OurTankFactory();
        Tank my = tankFactory.createTank("mouse");
        Tank mouse = ourTankFactory.createTank("my");
        System.err.println(my);
        System.err.println(mouse);
    }/*运行结果:
    com.headfirst.factory.use.MouseTank@677327b6
    老鼠坦克移动
    com.headfirst.factory.use.MyTank@14ae5a5
    老鼠坦克开枪
    老鼠坦克开枪
    */
}

工厂方法的问题:虽然工厂方法很好的为我们解决了创建过程由子类进行构建的问题,但是如果我们需要往坦克的产品里面提供配对的零件,此时会发现一些问题,我们的工厂方法只能提供一种产品的生产,如果我们需要生产很多的产品,工厂方法此时就遇到的瓶颈,因为需要调整继承结构,同时扩展非常不便。

注意点:工厂方法的另一个问题在于他需要依赖继承来实现对象创建过程定义,此时如果改动整个顶层的抽象方法会导致依赖磁铁导致所有的子类都需要改变。假如需要加入多个产品的生产,此时对于所有的子类改动来看都是十分麻烦的事情.

总结:工厂方法在构建一类产品的时候非常有效,但是需要构建很多种产品的时候会产生大量的继承具体化问题

抽象工厂的实现:

我们之前讲过抽象工厂实际上是对工厂方法的进一步提取,抽象工厂需要的是一系列产品的接口,由子工厂负责一系列产品的接口生产,同时更多的需要依赖组合的形式为具体的产品进行扩展。

在具体的案例代码介绍之前,我们需要对于案例进行改动,由于之前只存在坦克父类和具体的实现子类,为了详细介绍抽象工厂,我们针对坦克类增加一个大炮类,大炮类提供展示外观的方法,和坦克类的产品完全不同,我们需要定义坦克的大炮产品父类和具体的不同实现子类,在抽象工厂提供大炮的生产接口抽象同时,我们需要在大炮的类内部组合大炮的对象,为坦克增加不同的大炮外观,下面我们根据抽象工厂的结构图,构建如下的结构图:

抽象工厂

我们参考结构图,定义类似的坦克结构,下面是加入新需求之后的结构图:

根据抽象工厂绘制

根据上面的结构图,我们先将工厂有具体类改造为工厂接口,不再持有具体的创建过程,将一系列创建的细节分布到子类进行,同时定义接口的方式可以创建多个产品。(这里简化为2个不同的产品)

/**
 * 坦克工厂,专门负责生产坦克
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/25 22:27
 */
public interface TankFactory {

    /**
     * 坦克创建方法抽象
     * @return
     */
    Tank createTank();

    /**
     * 大炮的创建方法
     * @return
     */
    Cannon createCannon();
}

接下来我们根据抽象工厂接口创建具体的生产工厂,我们在子类可以返回具体的产品子类也可以返回抽象的父类,下面定义我方坦克的工厂类,同时定义一个特定敌人坦克的工厂类。

/**
 * 我方坦克的创建工厂
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/16 14:28
 */
public class OurTankFactory implements TankFactory {

    /**
     * 创建自带大炮的坦克
     * @return
     */
    public Tank createTanAndCannon() {
        Tank myTank = createTank();
        myTank.setCannon(createCannon());
        return myTank;
    }

    @Override
    public Tank createTank() {
        return new MyTank();
    }

    @Override
    public Cannon createCannon() {
        return new Artillery();
    }
}

敌人坦克的工厂实现子类:敌人的坦克工厂实现子类,可以生产不同抽象产品的不同具体实现子类

/**
 * 敌人坦克的构建工厂
 * 老鼠坦克
 * @author zxd
 * @version 1.0
 * @date 2021/2/16 14:28
 */
public class MouseTankFactory implements TankFactory {
    @Override
    public MouseTank createTank() {
        return new MouseTank();
    }

    @Override
    public Cannon createCannon() {
        return new RocketLauncher();
    }
}

接着我们定义另一个独立的产品,定义顶层的抽象类

/**
 * 大炮抽象类
 * 子类具备不同的大炮形式
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/16 18:15
 */
public abstract class Cannon {

    /**
     * 外观
     */
    public abstract void display();


}

根据对应上面的抽象父类,定义对应点具体实现子类,这里为了简单将两个具体实现子类放到一块:

/**
 * 火炮
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/16 19:06
 */
public class Artillery extends Cannon{


    @Override
    public void display() {
        System.out.println("火箭炮");
    }
}
/**
 * 火箭炮
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/16 19:06
 */
public class RocketLauncher extends Cannon{
    @Override
    public void display() {
        System.out.println("火箭炮");
    }
}

这里扩展了一下坦克类,为坦克类组合了大炮的对象:

/**
 * 坦克的父类,定义坦克的行为
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/25 0:14
 */
public abstract class Tank {
    /**
     * 坦克hp
     */
    protected int hp;

    /**
     * 坦克子弹
     */
    protected List<Object> bullet;

    private Cannon cannon;

    /**
     * 移动的方法s
     */
    public abstract void move();

    /**
     * 攻击
     */
    public abstract void attack();

    /**
     * 停止
     */
    public abstract void stop();

    public Cannon getCannon() {
        return cannon;
    }

    public void setCannon(Cannon cannon) {
        this.cannon = cannon;
    }

    @Override
    public String toString() {
        return "Tank{" +
                "hp=" + hp +
                ", bullet=" + bullet +
                ", cannon=" + cannon +
                '}';
    }
}

下面是单元测试代码,我们在坦克的对象里面设置或者组合其他的对象,并且由工厂提供生产:

/**
 * 单元测试
 * 抽象工厂
 * @author zxd
 * @version 1.0
 * @date 2021/2/16 16:32
 */
public class Main {

    public static void main(String[] args) {
        TankFactory ourTankFactory = new OurTankFactory();
        TankFactory mouseTankFactory = new MouseTankFactory();
        Tank ourTankFactoryTank = ourTankFactory.createTank();
        Cannon cannon = ourTankFactory.createCannon();
        Tank mouseTankFactoryTank = mouseTankFactory.createTank();
        Cannon cannon1 = mouseTankFactory.createCannon();
        ourTankFactoryTank.setCannon(cannon);
        mouseTankFactoryTank.setCannon(cannon1);
        System.err.println("our = " + ourTankFactoryTank);
        System.err.println("mouse = " + mouseTankFactoryTank);
    }/*
        our = Tank{hp=1, bullet=[java.lang.Object@677327b6, java.lang.Object@14ae5a5, java.lang.Object@7f31245a],
        cannon=com.headfirst.factory.abstractfac.Artillery@6d6f6e28}
        老鼠坦克移动
        mouse = Tank{hp=1, bullet=[java.lang.Object@135fbaa4, java.lang.Object@45ee12a7, java.lang.Object@330bedb4, java.lang.Object@2503dbd3, java.lang.Object@4b67cf4d],
        cannon=com.headfirst.factory.abstractfac.RocketLauncher@7ea987ac}
        老鼠坦克开枪

     */
}

工厂模式的变化:

从上面的案例和具体实现我们分析了工厂模式的三种变化:简单工厂、工厂方法、抽象工厂。他们的递进次序也是简单工厂 -> 工厂方法 -> 抽象工厂这种顺序。

我们可以发现简单工厂是一种非常简单的设计思路,他仅仅定义了的创建和使用过程的接口,同时产品具备最基本的抽象和继承设计,这类设计往往用于简单的对象构建。而一旦出现大量的具体对象,简单工厂的代码将会不断的膨胀,同时产生很多的if/else代码。

此时就需要使用工厂方法对于简单工厂的结构进行升级,工厂方法通过继承的方式(定义抽象的方法),推迟具体对象的创建到子类,工厂父类既可以控制子类的创建结果,同时又不需要关心具体对象的创建过程,这种设计非常巧妙,很好的解决了工厂的对象创建方法代码臃肿的问题。

但是我们也发现了问题,工厂方法扩展会导致所有的子类进行强制实现,不利于后期的维护,同时如果需要一系列相关产品的生成,使用工厂方法进行继承实现会造成高度的继承耦合,不利于工厂的产品生产扩展,此时就可以运用抽象工厂进行改进,我们用抽象工厂扩展工厂方法,使用接口的形式定义一批接口,由子类工厂进行实现和后续的所有生产细节,同时还可以自定义生产的具体产品。

上面是根据案例对于本次的设计模式进行一个模式的总结,可以看到工厂模式的应用还是非常多的,在WEB领域最常用的Spring框架就是的Bean工厂就是一个非常良好的工厂模式的实践案例。

工厂模式的总结:

下面用一张表格总结工厂模式的三种形态,优缺点以及相关总结:

模式名称简单工厂工厂方法抽象方法
特点根据产品按照客户端的需求生产不同的具体对象,将生产和使用的过程进行解耦将工厂的创建细节延迟到工厂的子类实现。定义一系列工厂方法,由子工厂负责具体的多类产品的生产
派生方式需要修改简单工厂的代码顶层工厂增加方法需要所有的子类强制实现。生产多个产品需要改动继承结构扩展产品和生产具体产品非常方便。但是扩展新对象需要改动抽象工厂接口
优点1. 简单工厂将创建对象的过程和使用对象的过程进行解耦
2. 工厂可以创建生产对象的不同实现子类,扩展子类实现非常方便
1. 工厂方法将工厂生产对象的创建细节延迟到子类
2. 克服了简单工厂部分缺点,比如符合开放-关闭原则
3. 同样可以对客户端和创建对象工厂进行解耦
1. 有利于多个产品的对象创建扩展
2. 将抽象类转变为接口,可以定义更高级的抽象。方便向上扩展
3. 类似制定工厂的生产规则,而具体的细节交由实现接口的子类完成
缺点1. 工厂扩展新的对象需要改动代码,不符合开放-关闭原则
2. 简单工厂对应简单的创建过程,所以创建过程复杂会造成工厂的臃肿
1. 不利于维护,加入工厂方法需要扩展所有的子类都需要实现工厂方法
2. 当需要多个产品类的时候,更改会相当的麻烦
1. 面对新的产品,需要所有的工厂实现类进行实现。
2. 最大的缺点是难以扩展新的产品,或者说扩展新产品的代价很大
总结是一种良好的编码和思考方式,但是严格意义上不能算是设计模式将具体对象的创建过程延迟到子类,符合开放-关闭原则抽象工厂是对工厂方法的升级,分离了多个产品的生产同时,子工厂可以对多个产品的生产细节进行自由控制。

总结:

本次设计模式的文章内容比较长,由于本次设计模式虽然是一个设计模式,但是他存在三种“变体”,所以在什么使用哪一种设计还是需要依靠具体的需求环境来决定。可以看到该设计模式最容易混淆的是工厂方法和抽象工厂。希望通过本文的总结和案例可以让读者更好的了解工厂模式下这两者的使用场景和区别。

查看原文

赞 0 收藏 0 评论 0

lazytimes 发布了文章 · 2月14日

钉钉自定义机器人简单使用

钉钉自定义机器人简单使用

前言:

年前公司的需求里面有用到钉钉机器人,使用之后发现真的非常简单,不得不感叹阿里的牛逼,这篇文章总结了一下个人使用钉钉机器人的经验,同时介绍个人据此构建一个工具类来方便后续直接“开箱即用”,希望对于读者有所启发。

文章目的:

  1. 简单的说明一下钉钉自定义机器人使用,注意是自定义机器人
  2. 说明一下个人针对钉钉机器人设计了一个工具类,说明一下设计的思路。(重点)
  3. 汇总一些个人使用钉钉机器人的小坑,同时提供解决办法希望读者参考可以解决问题

钉钉文档:

机器人的使用还是非常简单的,直接参考文档就可以进行构建,如果了解过这一部分可以直接跳到编写工具类的部分进行文章的后续阅读。

https://developers.dingtalk.c...

由于钉钉的官方文档更新较为频繁,这里的连接可能在以后会失效

如何创建一个机器人

文档里面介绍的比较详细了,我们根据文档的内容进行实战一下即可。这里使用了 新手体验群 创建的机器人进行实验。下面的内容包括创建自定义机器人以及测试机器人如何使用。

创建一个自定义机器人

随意点击一个机器人,右击菜单,出现“更多机器人”,进入到界面

点击“更多机器人”

选择钉钉的自定义机器人进行使用:

这里还有很多其他的机器人,如果感兴趣可以查看钉钉的文档进行更多的了解

在下面的界面选择添加:

到达下一个界面,根据指示需要填写如下的内容:

  • 机器人的名称:自己取一个合适的名字,自己喜欢就行
  • 添加到群组:关键的一步,意味着你的机器人要添加到哪一个具体的群组里面进行使用。也意味着只有在这个群组里面的人才可以收到对应的通知。

下面说明一下安全设置的内容:

  • 自定义关键词:关键配置,这里自定义关键词可以按照自己的喜好进行设置。但是一旦设置在发送请求的时候必须要携带关键词,请求才会生效,否则会返回对应的错误码31000和对应的错误信息。
  • 加签:建议勾上,这里加签可以在请求中更好的保护接口,同时注意一下加上签名之后要复制一下内容
  • IP地址(段):这里个人没有进行过测试,所以没有进行勾选,正式的生产环境建议使用IP限制,保证万无一失

这里建议保存一下前面和关键字,当然忘记了也可以在构建完成之后从设置里面查看:

签名:SECf075e3890b7d79ca645e51b42644fc57c2402577d5a955bce51cb980cec0a3b6

关键词:新人

至此,我们成功创建了一个钉钉的自定义机器人,整个过程十分简单,这里记得保存一下对应的信息:

https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050

上面为个人的配置。发文的时候此机器人已经删除,所以读者自己实验即可。

测试机器人是否可以正常使用

通过上面的步骤,我们已经构建了一个基本的机器人为我们使用,再进行下一步之前,我们需要验证一下钉钉机器人是否可以正常使用。这里针对不同的平台说下比较简单快捷的验证方法。

windows 验证方式:

windows 推荐使用git的一个shell命令框进行测试,因为windows 本身是没有curl这个命令的,当然也有其他的办法,但是为了图省事直接使用git给我们开发的一个小工具即可。

如下图所示,我们选择Git Bash Here,打开命令行的界面

我们根据上一步的机器人配置,构建一个CURL请求进行测试:

curl 'https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050&timestamp=1613211530113&secret=SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc' \
   -H 'Content-Type: application/json' \
   -d '{"msgtype": "text","text": {"content": "新人内容测试"}}'

不出所料,这里按照官方文档给的方式验证失败了,这是为什么呢?原因有几个:

  • 加签密文:我们设置了加签,所以在请求参数里面要加入对应的签名密文,也就是在添加这一步勾选了签名这一步。
  • 时间戳:请求需要传递时间戳,但是我们没有在请求参数里面附带时间戳,同时时间戳必须在系统时间的一小时之内,超过这个时间即使请求参数正确也无法通过
timestamp = 1613212103494
sign = MO79EJ58O9lmuQJo1dB1KGMhkZI%2BM5KkyD0NYuNe8%2B8%3D

我们根据上面的说明修复一下,注意在URL增加了两个参数:

curl 'https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050&timestamp=1613212722591&sign=SsKKlkvwM%2F4tsCPE6YoGls8vgkQqWJGHYpvWbW7hTGM%3D' \
   -H 'Content-Type: application/json' \
   -d '{"msgtype": "text","text": {"content": "新人为什么你这么牛逼"}}'
关于这一部分内容,已经汇总到“问题汇总”这一部分,如果还是感到迷惑可以参考。

我们再次验证一下,发现依然失败,比较奇怪,个人设置的关键字在请求content里面却失败了:

zhaoxudong@LAPTOP-MEUFMP1M MINGW64 /d/Users/zhaoxudong/Desktop
$ curl 'https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050&timestamp=1613212722591&sign=SsKKlkvwM%2F4tsCPE6YoGls8vgkQqWJGHYpvWbW7hTGM%3D' \
>    -H 'Content-Type: application/json' \
>    -d '{"msgtype": "text","text": {"content": "新人为什么你这么牛逼"}}'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   178  100   115  100    63    991    543 --:--:-- --:--:-- --:--:--  1534
{"errcode":310000,"errmsg":"keywords not in content, more: [https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq]"}

排查问题之后发现由于windows系统默认使用了gb2312的编码,所以我们此时需要切换一下系统的编码,为了证明是系统编码的问题,我们先验证一下编码:

打开window的cmd窗口,我们输入chcp命令进入到具体的页面,可以看到下面936,百度一下发现就是GB2312,在请求发送的过程中被转码导致乱码。

C:\Users\zhaoxudong>chcp
活动代码页: 936

解决办法也比较简单,改一下整改系统的编码即可,关于设置的方法:https://blog.csdn.net/robinhu...

插曲:个人在设置过后,因为编码的问题导致编辑器无法编译,经过核实发现是由于文件夹的编码乱码找不到类的问题,所以这里建议放置Java项目的时候放置到全英文的目录。所以更推荐linux的方式,可以省去很多麻烦

linux 验证方式:

linux 验证比较简单,而且出问题的概率比较小,根据window内容得知最后需要三个参数才能请求成功,这里直接给出一个相似的CURL请求作为案例说明:

curl 'https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050&timestamp=1613212722591&sign=SsKKlkvwM%2F4tsCPE6YoGls8vgkQqWJGHYpvWbW7hTGM%3D' \
   -H 'Content-Type: application/json' \
   -d '{"msgtype": "text","text": {"content": "新人为什么你这么牛逼"}}'

我们把这个请求放到linux命令行里面进行运行,如果errorcode返回0,说明请求成功:

{"errcode":0,"errmsg":"ok"}

请求成功之后,我们可以看到对应的结果:

注意一下钉钉机器人不能请求过于频繁。建议限制一下每分钟的请求QPS

编写工具类

从上一节可以看到,整个钉钉机器人的构建还是十分简单的。但是使用起来不是特别的方便,个人之前有使用钉钉做过一个预警的小需求,为了后续可以直接开箱即用,自己构建了工具类,下面的部分主要说个人的工具类的设计以及个人的构建思路

个人水平有限,工具类还有很大的改进空间,但是对于我来说暂时没有遇到使用的瓶颈。

工具类的代码地址

这里个人的小工具类整合到了个人小项目里面,想要参考的可以直接进行下载,下面的文章代码也是来源于这个项目里面。

具体请查看:com.zxd.interview.dingrobot这个包

具体的代码地址:https://gitee.com/lazyTimes/i...

构建工具类的思路

把整个请求的流程需要的组件分为了以下的几个部分:

构建基本的请求环境:也就是需要的请求地址,请求签名或者关键字等参数,这些参数都是必须的,否则请求无法正常运行,所以我们提出来作为环境使用。

构建请求参数:由于钉钉支持非常多的msgtype也就是文本类型,个人参考了一下SDK,对应构建了一个请求的参数类,为了方便扩展,设计了一个接口进行后续的扩展和兼容。

使用JAVA代码发送请求:本着最小依赖的原则,使用最常见的HttpClient进行模拟JAVA的请求发送。但是在这个基础上做了一点点的封装,方便后续扩展

  1. HttpClient的封装,将请求所需要的一些请求参数封装到一个配置对象进行管理
  2. 请求方法的封装,这里用了一个对象进行封装,也可以直接使用Spring封装的org.springframework.web.bind.annotation.RequestMethod或者直接使用枚举构建常量即可。
  3. 构建钉钉请求工具类:最后我们整合上面所有步骤构建一个核心请求工具类,通过环境参数构建请求URL和一些Header设置,以及构建不同的请求方法发送请求,调用HttpClient工具类进行请求发送,以及发送之后转化为结果对象等一系列操作均由该工具类完成,是本次工具类最核心的类。
  4. 构建钉钉的请求Msg:该对象包含了请求所支持的所有JSON参数格式对应的实体对象,根据参数格式构建对应的对象,个人利用内部类全部封装到一个对象里面,方便客户端理解调用。

返回请求结果:包含了错误码,错误信息,以及其他的参数等,也可以修改为直接返回字符串,由客户端决定如何处理

请求之后返回结果:将上面的错误码或者错误信息等封装为一个简单对象进行返回,同样如果不喜欢也可以改为返回字符串的结果。

单元测试

在介绍正式的结果之前,我们看下结果,下面是效果截图,包含了钉钉文档里面的所有类型,包含了目前钉钉文档支持的几种主要的类型:

测试结果1

测试截图2

下面为单元测试的代码,整个单元测试测试各种不同请求类型,调用工具包发送请求:

注意下面的请求text里面包含了之前请求示例里面设置的关键字,没有关键字是无法请求成功的
import com.alibaba.fastjson.JSON;
import org.apache.commons.codec.binary.Base64;
import org.junit.jupiter.api.Test;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author zxd
 * @version v1.0.0
 * @Package : com.zxd.interview.dingrobot
 * @Description : 钉钉机器人测试类
 * @Create on : 2021/2/7 11:06
 **/
public class DingRobotUtilsTest {

    /**
       运行下面五个单元测试的结果
    */ 
    @Test
    public void testAll() {
        testText();
        testLink();
        testMarkdown();
        testActionCard();
        testFeedCard();
    }

    /**
     * 构建当前的系统时间戳
     */
    @Test
    public void generateSystemCurrentTime() throws Exception {
        long currentTimeMillis = System.currentTimeMillis();
        String secret = "SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc";
        String sign = generateSign(currentTimeMillis, secret);
        System.out.println("timestamp = " + currentTimeMillis);
        System.out.println("sign = " + sign);
    }

    /**
     * 测试link类型的请求
     */
    @Test
    public void testLink() {
        DingRobotRequest.Builder builder = new DingRobotRequest.Builder();
        DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc")
                .url("https://oapi.dingtalk.com/robot/send")
                .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050")
                .msg(generateLink()).build();
        try {
            DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build);
            System.err.println(JSON.toJSONString(dingRobotResponseMsg));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 测试text类型
     */
    @Test
    public void testText() {
        DingRobotRequest.Builder builder = new DingRobotRequest.Builder();
        DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc")
                .url("https://oapi.dingtalk.com/robot/send")
                .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050")
                .msg(generateText()).build();
        try {
            DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build);
            System.err.println(JSON.toJSONString(dingRobotResponseMsg));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**测试markdown 类型 */
    @Test
    public void testMarkdown() {
        DingRobotRequest.Builder builder = new DingRobotRequest.Builder();
        DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc")
                .url("https://oapi.dingtalk.com/robot/send")
                .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050")
                .msg(generateMarkdown()).build();
        try {
            DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build);
            System.err.println(JSON.toJSONString(dingRobotResponseMsg));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

     /**测试ActionCard 类型 */
    @Test
    public void testActionCard() {
        DingRobotRequest.Builder builder = new DingRobotRequest.Builder();
        DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc")
                .url("https://oapi.dingtalk.com/robot/send")
                .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050")
                .msg(generateActionCard()).build();
        try {
            DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build);
            System.err.println(JSON.toJSONString(dingRobotResponseMsg));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**测试FeedCard 类型 */
    @Test
    public void testFeedCard() {
        DingRobotRequest.Builder builder = new DingRobotRequest.Builder();
        DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc")
                .url("https://oapi.dingtalk.com/robot/send")
                .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050")
                .msg(generateFeed()).build();
        try {
            DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build);
            System.err.println(JSON.toJSONString(dingRobotResponseMsg));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private DingRobotRequestBody generateFeed() {
        List<DingRobotRequestBody.FeedCard.FeedItem> list = new ArrayList<>();
        DingRobotRequestBody dingRobotRequestBody = new DingRobotRequestBody();
        DingRobotRequestBody.FeedCard feedCard = new DingRobotRequestBody.FeedCard();
        DingRobotRequestBody.FeedCard.FeedItem feedItem = new DingRobotRequestBody.FeedCard.FeedItem();
        feedItem.setMessageURL("https://www.dingtalk.com/");
        feedItem.setTitle("新人时代的火车向前开");
        feedItem.setPicURL("https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png");
        list.add(feedItem);
        feedCard.setLinks(list);
        dingRobotRequestBody.setFeedCard(feedCard);
        dingRobotRequestBody.setMsgType("feedCard");
        return dingRobotRequestBody;
    }

    private DingRobotRequestBody generateActionCard() {
        DingRobotRequestBody dingRobotRequestBody = new DingRobotRequestBody();
        DingRobotRequestBody.ActionCard actionCard = new DingRobotRequestBody.ActionCard();
        actionCard.setBtnOrientation("0");
        actionCard.setSingleTitle("阅读全文");
        actionCard.setSingleURL("https://www.dingtalk.com/");
        actionCard.setText("新人![screenshot](https://gw.alicdn.com/tfs/TB1ut3xxbsrBKNjSZFpXXcXhFXa-846-786.png) \n" +
                " ### 乔布斯 20 年前想打造的苹果咖啡厅 \n" +
                " Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划");
        actionCard.setTitle("乔布斯 20 年前想打造一间苹果咖啡厅,而它正是 Apple Store 的前身");
        dingRobotRequestBody.setMsgType("actionCard");
        dingRobotRequestBody.setActionCard(actionCard);
        return dingRobotRequestBody;
    }

    private DingRobotRequestBody generateMarkdown() {
        DingRobotRequestBody dingRobotRequestBody = new DingRobotRequestBody();
        DingRobotRequestBody.MarkDown markDown = new DingRobotRequestBody.MarkDown();
        dingRobotRequestBody.setMsgType("markdown");
        markDown.setTitle("杭州天气");
        markDown.setText("新人测试 标题\n" +
                "# 一级标题\n" +
                "## 二级标题\n" +
                "### 三级标题\n" +
                "#### 四级标题\n" +
                "##### 五级标题\n" +
                "###### 六级标题\n" +
                "\n" +
                "引用\n" +
                "> A man who stands for nothing will fall for anything.\n" +
                "\n" +
                "文字加粗、斜体\n" +
                "**bold**\n" +
                "*italic*\n" +
                "\n" +
                "链接\n" +
                "[this is a link](http://name.com)\n" +
                "\n" +
                "图片\n" +
                "![](http://name.com/pic.jpg)\n" +
                "\n" +
                "无序列表\n" +
                "- item1\n" +
                "- item2\n" +
                "\n" +
                "有序列表\n" +
                "1. item1\n" +
                "2. item2");
        dingRobotRequestBody.setMarkDown(markDown);
        return dingRobotRequestBody;
    }

    private DingRobotRequestBody generateText() {
        DingRobotRequestBody dingRobotRequestBody = new DingRobotRequestBody();
        DingRobotRequestBody.Text text = new DingRobotRequestBody.Text();
        text.setContent("新人为什么这么牛逼");
        DingRobotRequestBody.At at = getnerateAt();
        dingRobotRequestBody.setMsgType("text");
        dingRobotRequestBody.setAt(at);
        dingRobotRequestBody.setText(text);
        return dingRobotRequestBody;
    }

    private DingRobotRequestBody generateLink() {
        DingRobotRequestBody dingRobotRequestBody = new DingRobotRequestBody();
        DingRobotRequestBody.Link link = new DingRobotRequestBody.Link();
        link.setMessageUrl("https://www.dingtalk.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI");
        link.setPicUrl("");
        link.setTitle("时代的火车向前开");
        link.setText("新人:这个即将发布的新版本,创始人xx称它为红树林。而在此之前,每当面临重大升级,产品经理们都会取一个应景的代号,这一次,为什么是红树林");
        DingRobotRequestBody.At at = getnerateAt();
        dingRobotRequestBody.setMsgType("link");
        dingRobotRequestBody.setAt(at);
        dingRobotRequestBody.setLink(link);
        return dingRobotRequestBody;
    }

    /**
     * 构建at请求
     *
     * @return
     */
    private DingRobotRequestBody.At getnerateAt() {
        DingRobotRequestBody.At at = new DingRobotRequestBody.At();
        at.setAtAll(true);
        at.setAtMobiles(Arrays.asList("xxxxx", "123456789"));
        return at;
    }

    /**
     * 构建签名方法
     *
     * @param timestamp 时间戳
     * @param secret    秘钥
     * @return
     * @throws Exception
     */
    private String generateSign(Long timestamp, String secret) throws Exception {
        String stringToSign = timestamp + "\n" + secret;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
        return URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
    }

}

构建工具类:

下面就上面的单元测试,说明一下个人的基本设计。我们根据思路构建一个支持拿来即用的钉钉工具类。

类结构介绍:

Maven依赖:

在进行具体的代码编写之前,需要引入对应的依赖,个人秉持最小依赖的原则,使用的三方jar包仅仅为一些测试工具包和Httpclient请求工具包还有最熟悉的fastjson的工具包。

<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.6</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.75</version>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

类结构:

类结构包含了之前设计思路里面说明的情况,包含请求类,工具类,参数封装和请求对象结构封装等。

+ DingRobotRequest.java            钉钉请求对象
+ DingRobotRequestAble.java        请求接口,允许发送钉钉请求的接口
+ DingRobotRequestBody.java     允许发送钉钉请求的接口具体的实现类,比较重要,对接文档的钉钉对象
+ DingRobotRequestMsg.java        废弃对象,但是依然保留s
+ DingRobotResponseMsg.java        请求返回对象
+ DingRobotUtils.java            钉钉请求工具类,非常重要的一个类
+ HttpClientUtil.java            httpclient请求工具类
+ HttpConfig.java                请求参数构建类
+ HttpMethods.java                请求方法类

构建基本的请求环境

构建基本的请求环境,我们使用对象来封装所有的环境参数,并且使用建造模式构建一个建造器,使用建造来构建我们需要的环境参数,它的使用方式如下:

  • 构建请求URL
  • 构建请求accessToken
  • 构建请求msg,重点,可以通过构建对应的请求来实现发送不同的信息
DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc")
                .url("https://oapi.dingtalk.com/robot/send")
                .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050")
                .msg(generateActionCard()).build();

具体的源代码如下,包含了几个简单的必要参数,以及一个建造器,注意对于构造器的私有化,对外只允许使用构建器进行初始化:

/**
 * @author zxd
 * @version v1.0.0
 * @Package : com.dcc.common.field
 * @Description : 钉钉机器人请求实体类
 * @Create on : 2021/2/5 15:40
 **/
public class DingRobotRequest {

    /**
     * 请求URL
     */
    private String url;

    /**
     * token
     */
    private String accessToken;

    /**
     * 秘钥
     */
    private String secret;

    /**
     * 请求msg
     */
    private DingRobotRequestBody msg;

    private DingRobotRequest(){

    }

    private DingRobotRequest(Builder builder) {
        this.url = builder.url;
        this.accessToken = builder.accessToken;
        this.secret = builder.secret;
        this.msg = builder.msg;
    }

    public static class Builder {

        private String url;
        private String accessToken;
        private String secret;
        private DingRobotRequestBody msg;

        public DingRobotRequest.Builder url(String url){
            this.url = url;
            return this;
        }
        public DingRobotRequest.Builder accessToken(String accessToken){
            this.accessToken = accessToken;
            return this;
        }
        public DingRobotRequest.Builder secret(String secret){
            this.secret = secret;
            return this;
        }
        public DingRobotRequest.Builder msg(DingRobotRequestBody msg){
            this.msg = msg;
            return this;
        }

        public DingRobotRequest build(){
            return new DingRobotRequest(this);
        }
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getAccessToken() {
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    public String getSecret() {
        return secret;
    }

    public void setSecret(String secret) {
        this.secret = secret;
    }

    public DingRobotRequestBody getMsg() {
        return msg;
    }

    public void setMsg(DingRobotRequestBody msg) {
        this.msg = msg;
    }

    @Override
    public String toString() {
        return "DingRobotRequest{" +
                "url='" + url + '\'' +
                ", accessToken='" + accessToken + '\'' +
                ", secret='" + secret + '\'' +
                ", msg='" + msg + '\'' +
                '}';
    }
}

构建请求参数

下面是请求参数的构建案例,我们可以使用链式调用的方式构建不同的request请求:

 /**
     * 钉钉机器人的默认配置
     *
     * @param dingRobotRequest    钉钉机器人请求对象
     * @param dingRobotRequestMsg 钉钉机器人请求实体
     * @return
     */
    private static HttpConfig buildDefaultHttpConfig(DingRobotRequest dingRobotRequest, DingRobotRequestAble dingRobotRequestMsg) {
        return HttpConfig.custom().headers(defaultBasicHeader())
                .url(dingRobotRequest.getUrl())
                .encoding("UTF-8")
                .method(HttpMethods.POST)
                .json(JSON.toJSONString(dingRobotRequestMsg));
    }

从上面的案例可以看到下面对于请求配置类,构建HttpConfig请求,同样类似构建器进行对象的参数构建,我们定义了基本的请求encoding、请求header,请求方法参数,请求的context等对应的参数配置。

/**
 * 请求配置类
 *
 */
public class HttpConfig {

    private HttpConfig() {
    }

    // 传入参数特定类型
    public static final String ENTITY_STRING = "$ENTITY_STRING$";
    public static final String ENTITY_MULTIPART = "$ENTITY_MULTIPART$";

    /**
     * 获取实例
     *
     * @return
     */
    public static HttpConfig custom() {
        return new HttpConfig();
    }

    /**
     * HttpClient对象
     */
    private HttpClient client;

    /**
     * Header头信息
     */
    private Header[] headers;

    /**
     * 是否返回response的headers
     */
    private boolean isReturnRespHeaders;

    /**
     * 请求方法
     */
    private HttpMethods method = HttpMethods.GET;

    /**
     * 请求方法名称
     */
    private String methodName;

    /**
     * 用于cookie操作
     */
    private HttpContext context;

    /**
     * 传递参数
     */
    private Map<String, Object> map;

    /**
     * 以json格式作为输入参数
     */
    private String json;

    /**
     * 输入输出编码
     */
    private String encoding = Charset.defaultCharset().displayName();

    /**
     * 输入编码
     */
    private String inenc;

    /**
     * 输出编码
     */
    private String outenc;

    /**
     * 解决多线程下载时,strean被close的问题
     */
    private static final ThreadLocal<OutputStream> outs = new ThreadLocal<OutputStream>();

    /**
     * 解决多线程处理时,url被覆盖问题
     */
    private static final ThreadLocal<String> urls = new ThreadLocal<String>();

    /**
     * HttpClient对象
     */
    public HttpConfig client(HttpClient client) {
        this.client = client;
        return this;
    }

    /**
     * 资源url
     */
    public HttpConfig url(String url) {
        urls.set(url);
        return this;
    }

    /**
     * Header头信息
     */
    public HttpConfig headers(Header[] headers) {
        this.headers = headers;
        return this;
    }

    /**
     * Header头信息(是否返回response中的headers)
     */
    public HttpConfig headers(Header[] headers, boolean isReturnRespHeaders) {
        this.headers = headers;
        this.isReturnRespHeaders = isReturnRespHeaders;
        return this;
    }

    /**
     * 请求方法
     */
    public HttpConfig method(HttpMethods method) {
        this.method = method;
        return this;
    }

    /**
     * 请求方法
     */
    public HttpConfig methodName(String methodName) {
        this.methodName = methodName;
        return this;
    }

    /**
     * cookie操作相关
     */
    public HttpConfig context(HttpContext context) {
        this.context = context;
        return this;
    }

    /**
     * 传递参数
     */
    public HttpConfig map(Map<String, Object> map) {
        synchronized (getClass()) {
            if (this.map == null || map == null) {
                this.map = map;
            } else {
                this.map.putAll(map);
                ;
            }
        }
        return this;
    }

    /**
     * 以json格式字符串作为参数
     */
    public HttpConfig json(String json) {
        this.json = json;
        map = new HashMap<String, Object>();
        map.put(ENTITY_STRING, json);
        return this;
    }

    /**
     * 上传文件时用到
     */
    public HttpConfig files(String[] filePaths) {
        return files(filePaths, "file");
    }

    /**
     * 上传文件时用到
     *
     * @param filePaths 待上传文件所在路径
     */
    public HttpConfig files(String[] filePaths, String inputName) {
        return files(filePaths, inputName, false);
    }

    /**
     * 上传文件时用到
     *
     * @param filePaths                     待上传文件所在路径
     * @param inputName                     即file input 标签的name值,默认为file
     * @param forceRemoveContentTypeChraset
     * @return
     */
    public HttpConfig files(String[] filePaths, String inputName, boolean forceRemoveContentTypeChraset) {
        synchronized (getClass()) {
            if (this.map == null) {
                this.map = new HashMap<String, Object>();
            }
        }
        map.put(ENTITY_MULTIPART, filePaths);
        map.put(ENTITY_MULTIPART + ".name", inputName);
        map.put(ENTITY_MULTIPART + ".rmCharset", forceRemoveContentTypeChraset);
        return this;
    }

    /**
     * 输入输出编码
     */
    public HttpConfig encoding(String encoding) {
        //设置输入输出
        inenc(encoding);
        outenc(encoding);
        this.encoding = encoding;
        return this;
    }

    /**
     * 输入编码
     */
    public HttpConfig inenc(String inenc) {
        this.inenc = inenc;
        return this;
    }

    /**
     * 输出编码
     */
    public HttpConfig outenc(String outenc) {
        this.outenc = outenc;
        return this;
    }

    /**
     * 输出流对象
     */
    public HttpConfig out(OutputStream out) {
        outs.set(out);
        return this;
    }

    public HttpClient client() {
        return client;
    }

    public Header[] headers() {
        return headers;
    }

    public boolean isReturnRespHeaders() {
        return isReturnRespHeaders;
    }

    public String url() {
        return urls.get();
    }

    public HttpMethods method() {
        return method;
    }

    public String methodName() {
        return methodName;
    }

    public HttpContext context() {
        return context;
    }

    public Map<String, Object> map() {
        return map;
    }

    public String json() {
        return json;
    }

    public String encoding() {
        return encoding;
    }

    public String inenc() {
        return inenc == null ? encoding : inenc;
    }

    public String outenc() {
        return outenc == null ? encoding : outenc;
    }

    public OutputStream out() {
        return outs.get();
    }

}

使用JAVA代码发送请求

之前说明,我们使用最常用的Httpclient进行设计请求,根据Httpclient请求工具包构建一个基本的工具类:

这个类是一个很难复用和扩展的高耦合类,并且设计不是非常良好。
/**
 * httpclient 请求工具封装类
 */
public class HttpClientUtil {

    public static String doGet(String url, Map<String, String> param) {

        // 创建Httpclient对象
        CloseableHttpClient httpclient = HttpClients.createDefault();

        String resultString = "";
        CloseableHttpResponse response = null;
        try {
            // 创建uri
            URIBuilder builder = new URIBuilder(url);
            if (param != null) {
                for (String key : param.keySet()) {
                    builder.addParameter(key, param.get(key));
                }
            }
            URI uri = builder.build();

            // 创建http GET请求
            HttpGet httpGet = new HttpGet(uri);

            // 执行请求
            response = httpclient.execute(httpGet);
            // 判断返回状态是否为200
            if (response.getStatusLine().getStatusCode() == 200) {
                resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
                httpclient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return resultString;
    }

    public static String doGet(String url) {
        return doGet(url, null);
    }

    public static String doPost(String url, Map<String, String> param) {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";
        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);
            // 创建参数列表
            if (param != null) {
                List<NameValuePair> paramList = new ArrayList<>();
                for (String key : param.keySet()) {
                    paramList.add(new BasicNameValuePair(key, param.get(key)));
                }
                // 模拟表单
                UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
                httpPost.setEntity(entity);
            }
            // 执行http请求
            response = httpClient.execute(httpPost);
            resultString = EntityUtils.toString(response.getEntity(), "utf-8");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                response.close();
            } catch (IOException e) {

                e.printStackTrace();
            }
        }

        return resultString;
    }

    public static String doPost(String url) {
        return doPost(url, null);
    }
    
    public static String doPostJson(String url, String json) {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";
        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);
            // 创建请求内容
            StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
            httpPost.setEntity(entity);
            // 执行http请求
            response = httpClient.execute(httpPost);
            resultString = EntityUtils.toString(response.getEntity(), "utf-8");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if(response != null){
                    response.close();
                }
            } catch (IOException e) {

                e.printStackTrace();
            }
        }

        return resultString;
    }

    /**
     * 根据请求Config 进行请求发送
     * @param httpConfig
     * @return
     */
    public static String send(HttpConfig httpConfig) {
        return doPostJson(httpConfig.url(), httpConfig.json());
    }
}

接着根据请求的结果设计一个钉钉机器人的返回对象,返回对象的设计也比较的简单。

/**
 * @author zxd
 * @version v1.0.0
 * @Package : com.dcc.common.field
 * @Description : 钉钉机器人返回对象
 * @Create on : 2021/2/5 18:26
 **/
public class DingRobotResponseMsg {

    /**
     * 错误码
     */
    private String errcode;

    /**
     * 错误信息
     */
    private String errmsg;
    /**
     * 更多链接
     */
    private String more;

    public DingRobotResponseMsg(String errcode, String errmsg, String more) {
        this.errcode = errcode;
        this.errmsg = errmsg;
        this.more = more;
    }

    public DingRobotResponseMsg() {

    }

    public String getErrcode() {
        return errcode;
    }

    public String getErrmsg() {
        return errmsg;
    }

    public String getMore() {
        return more;
    }

    public void setErrcode(String errcode) {
        this.errcode = errcode;
    }

    public void setErrmsg(String errmsg) {
        this.errmsg = errmsg;
    }

    public void setMore(String more) {
        this.more = more;
    }
}

最后,也是最重要的,我们要根据钉钉的文档,构建一个所有类型的请求对象类,这个类包含了钉钉文档目前支持的所有类型。内部使用了大量的内部类,客户端需要了解一定的细节才可以具体的调用。下面简要说明一下内容类的基本使用结构。

  • At 艾特对象内部类
  • Text 文本类型
  • Link 请求链接类型
  • MarkDown markdown类型
  • ActionCard 整体跳转类型
  • FeedCard 分享卡片类型
/**
 * @author zxd
 * @version v1.0.0
 * @Package : com.dcc.common.field
 * @Description : 钉钉机器人请求实体对象
 * 请求案例:{"msgtype": "text","text": {"content": "自定义具体内容"}}
 * @link {https://developers.dingtalk.com/document/app/custom-robot-access}
 *
 * @Create on : 2021/2/5 11:55
 **/
public class DingRobotRequestBody implements DingRobotRequestAble {

    /**
     * 艾特对象内容
     */
    private At at;

    /**
     * 类型
     */
    private String msgtype;

    /**
     * 文本类型
     */
    private Text text;

    /**
     * 连接类型
     */
    private Link link;

    /**
     * markdown 类型
     */
    private MarkDown markdown;

    /**
     * 整体跳转ActionCard类型
     */
    private ActionCard actionCard;

    /**
     * FeedCard类型
     */
    private FeedCard feedCard;


    /**
     * FeedCard类型
     *
     * msgtype        String    是    此消息类型为固定feedCard。
     * title        String    是    单条信息文本。
     * messageURL    String    是    点击单条信息到跳转链接。
     * picURL        String    是    单条信息后面图片的URL。
     */
    public static class FeedCard{

        private List<FeedItem> links;

        /**
         * 代表 FeedCard类型 子类型
         */
        public static class FeedItem{

            private String title;

            private String messageURL;

            private String picURL;

            public String getTitle() {
                return title;
            }

            public void setTitle(String title) {
                this.title = title;
            }

            public String getMessageURL() {
                return messageURL;
            }

            public void setMessageURL(String messageURL) {
                this.messageURL = messageURL;
            }

            public String getPicURL() {
                return picURL;
            }

            public void setPicURL(String picURL) {
                this.picURL = picURL;
            }
        }

        public List<FeedItem> getLinks() {
            return links;
        }

        public void setLinks(List<FeedItem> links) {
            this.links = links;
        }
    }


    /**
     * 整体跳转ActionCard类型
     * msgtype            String    是    消息类型,此时固定为:actionCard。
     * title            String    是    首屏会话透出的展示内容。
     * text                String    是    markdown格式的消息。
     * singleTitle        String    是    单个按钮的标题。
     *
     * 注意 设置此项和singleURL后,btns无效。
     *
     * singleURL        String    是    点击singleTitle按钮触发的URL。
     * btnOrientation    String    否    0:按钮竖直排列1:按钮横向排列
     */
    public static class ActionCard{

        private String title;

        private String text;

        private String btnOrientation;

        private String singleTitle;

        private String singleURL;

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }

        public String getBtnOrientation() {
            return btnOrientation;
        }

        public void setBtnOrientation(String btnOrientation) {
            this.btnOrientation = btnOrientation;
        }

        public String getSingleTitle() {
            return singleTitle;
        }

        public void setSingleTitle(String singleTitle) {
            this.singleTitle = singleTitle;
        }

        public String getSingleURL() {
            return singleURL;
        }

        public void setSingleURL(String singleURL) {
            this.singleURL = singleURL;
        }
    }

    /**
     * 艾特类
     */
    public static class At{

        /**
         * 是否通知全部人
         */
        private boolean atAll;

        /**
         * 需要@的手机号数组
         */
        private List<String> atMobiles;

        public boolean isAtAll() {
            return atAll;
        }

        public void setAtAll(boolean atAll) {
            this.atAll = atAll;
        }

        public List<String> getAtMobiles() {
            return atMobiles;
        }

        public void setAtMobiles(List<String> atMobiles) {
            this.atMobiles = atMobiles;
        }
    }

    /**
     *
     * markdown 类型, 可以发送markdown 的语法格式
     * msgtype        String    是    消息类型,此时固定为:markdown。
     * title        String    是    首屏会话透出的展示内容。
     * text            String    是    markdown格式的消息。
     * atMobiles    Array    否    被@人的手机号。 注意 在text内容里要有@人的手机号。
     * isAtAll    Boolean    否    是否@所有人。
     */
    public static class MarkDown{

        private String title;

        private String text;

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }
    }

    /**
     * 钉钉请求:链接类型
     *
         msgtype        String    是    消息类型,此时固定为:link。
         title            String    是    消息标题。
         text            String    是    消息内容。如果太长只会部分展示。
         messageUrl        String    是    点击消息跳转的URL。
         picUrl            String    否    图片URL。
     */
    public static class Link{

        private String text;

        private String messageUrl;

        private String picUrl;

        private String title;

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }

        public String getMessageUrl() {
            return messageUrl;
        }

        public void setMessageUrl(String messageUrl) {
            this.messageUrl = messageUrl;
        }

        public String getPicUrl() {
            return picUrl;
        }

        public void setPicUrl(String picUrl) {
            this.picUrl = picUrl;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }
    }

    /**
     * 钉钉请求:纯文本类型
     */
    public static class Text{

        /**
         * text请求内容
         */
        private String content;

        public String getContent() {
            return content;
        }

        public void setContent(String content) {
            this.content = content;
        }
    }

    @Override
    public void setMsgType(String msgtype) {
        this.msgtype = msgtype;
    }

    @Override
    public void setText(Text text) {
        this.text = text;
    }

    @Override
    public void setLink(Link link) {
        this.link = link;
    }

    @Override
    public void setMarkDown(MarkDown markDown) {
        this.markdown = markDown;
    }

    @Override
    public void setActionCard(ActionCard actionCard) {
        this.actionCard = actionCard;
    }

    @Override
    public void setFeedCard(FeedCard feedCard) {
        this.feedCard = feedCard;
    }

    public At getAt() {
        return at;
    }

    public void setAt(At at) {
        this.at = at;
    }

    public String getMsgtype() {
        return msgtype;
    }

    public Text getText() {
        return text;
    }

    public Link getLink() {
        return link;
    }

    public MarkDown getMarkdown() {
        return markdown;
    }

    public ActionCard getActionCard() {
        return actionCard;
    }

    public FeedCard getFeedCard() {
        return feedCard;
    }
}

插曲:在生成具体的钉钉对应请求对象时候,我们构建了一个对应的接口

/**
 * @author zxd
 * @version v1.0.0
 * @Package : com.zxd.interview.dingrobot
 * @Description : 允许发送钉钉请求的接口
 * @Create on : 2021/2/7 11:45
 **/
public interface DingRobotRequestAble {

    /**
     * 所有的子类需要集成该接口
     * @return
     */
    void setMsgType(String msgType);

    /**
     * 普通文本类型
     * @param text
     */
    void setText(DingRobotRequestBody.Text text);

    /**
     * link类型
     * @param link
     */
    void setLink(DingRobotRequestBody.Link link);

    /**
     * markdown 类型
     * @param markDown
     */
    void setMarkDown(DingRobotRequestBody.MarkDown markDown);

    /**
     * 整体跳转ActionCard类型
     * @param actionCard
     */
    void setActionCard(DingRobotRequestBody.ActionCard actionCard);

    /**
     * feedcard 类型
     * @param feedCard
     */
    void setFeedCard(DingRobotRequestBody.FeedCard feedCard);

}

构建钉钉请求工具类

介绍完上面所有的辅助对象之后,我们着手构建核心的钉钉请求工具类,钉钉的请求工具类包含了基本的请求步骤,提供对外的请求方法,调用者根据请求对象构建对应的请求参数即可,从下面的代码可以看到最核心的方法是notifyRobot这个方法,这个方法非常简单,内部的逻辑分为如下的几步:

  • 构建请求环境参数
  • 构建请求的URL和对应的携带参数
  • 构建具体的请求参数
  • 将请求返回的JSON字符串进行转化
/**
 * @author zxd
 * @version v1.0.0
 * @Package : com.dcc.common.utils
 * @Description : 钉钉机器人工具类
 * @Create on : 2021/2/4 00:11
 **/
public class DingRobotUtils {

    private static final Logger LOGGER = LoggerFactory.getLogger(DingRobotUtils.class);

    public static DingRobotResponseMsg notifyRobot(DingRobotRequest dingRobotRequest, long currentTimeMillis) throws Exception {
        Map<String, Object> param = buildParam(dingRobotRequest, currentTimeMillis);
        String s = buildParamUrl(param);
        // 钉钉的请求参数需要拼接到URL链接
        dingRobotRequest.setUrl(String.format("%s?%s", dingRobotRequest.getUrl(), s));
        HttpConfig httpConfig = buildDefaultHttpConfig(dingRobotRequest, dingRobotRequest.getMsg());
        return parseResponse(notifyRobot(httpConfig));
    }

    /**
     * 转化为对应对象
     *
     * @param notifyRobot 转化JSON
     * @return
     */
    private static DingRobotResponseMsg parseResponse(String notifyRobot) {
        try {
            return JSON.parseObject(notifyRobot, DingRobotResponseMsg.class);
        } catch (Exception e) {
            LOGGER.error("类型转化失败,失败原因为:{}", e.getMessage());
            throw e;
        }
    }

    /**
     * 按照自定时间戳进行通知
     *
     * @param dingRobotRequest 钉钉机器人请求
     * @throws Exception
     */
    public static DingRobotResponseMsg notifyRobot(DingRobotRequest dingRobotRequest) throws Exception {
        long currentTimeMillis = System.currentTimeMillis();
        return notifyRobot(dingRobotRequest, currentTimeMillis);
    }


    /**
     * 构建请求环境参数
     *
     * @param dingRobotRequest  请求request
     * @param currentTimeMillis 当前时间戳
     * @return
     * @throws Exception
     */
    private static Map<String, Object> buildParam(DingRobotRequest dingRobotRequest, long currentTimeMillis) throws Exception {
        Map<String, Object> param = new HashMap<>(3);
        param.put("access_token", dingRobotRequest.getAccessToken());
        param.put("timestamp", currentTimeMillis);
        param.put("sign", generateSign(currentTimeMillis, dingRobotRequest.getSecret()));
        return param;
    }

    /**
     * 钉钉机器人的默认配置
     *
     * @param dingRobotRequest    钉钉机器人请求对象
     * @param dingRobotRequestMsg 钉钉机器人请求实体
     * @return
     */
    private static HttpConfig buildDefaultHttpConfig(DingRobotRequest dingRobotRequest, DingRobotRequestAble dingRobotRequestMsg) {
        return HttpConfig.custom().headers(defaultBasicHeader())
                .url(dingRobotRequest.getUrl())
                .encoding("UTF-8")
                .method(HttpMethods.POST)
                .json(JSON.toJSONString(dingRobotRequestMsg));
    }

    /**
     * 默认headers配置
     *
     * @return
     */
    private static Header[] defaultBasicHeader() {
        Header[] headers = new Header[1];
        headers[0] = new BasicHeader("Content-Type", "application/json");
        return headers;
    }

    private static String notifyRobot(HttpConfig httpConfig) throws Exception {
        String send = "";
        try {
            send = HttpClientUtil.send(httpConfig);
        } catch (Exception e) {
            LOGGER.error("HTTPClient请求发送失败, 失败原因为:{}", e.getMessage());
            throw e;
        }
        return send;
    }

    /**
     * 根据时间戳和秘钥生成一份签名
     *
     * @param timestamp 时间戳
     * @param secret    秘钥
     * @return
     * @throws Exception
     */
    private static String generateSign(Long timestamp, String secret) throws Exception {
        String stringToSign = timestamp + "\n" + secret;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
        return URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
    }

    /**
     * 构建URL参数
     *
     * @param param 请求MAP参数
     * @return
     */
    private static String buildParamUrl(Map<String, Object> param) {
        if (null == param || param.size() == 0) {
            return "";
        }
        StringBuilder stringBuilder = new StringBuilder();
        param.forEach((key, value) -> {
            stringBuilder.append(key).append("=").append(value);
            stringBuilder.append("&");
        });
        stringBuilder.deleteCharAt(stringBuilder.length() - 1);
        return stringBuilder.toString();
    }

}

下面是本工具类的使用方式,只需要传入环境参数并且传入必须的请求msg,就可以直接发送请求并且返回对应的结果。

 DingRobotRequest.Builder builder = new DingRobotRequest.Builder();
DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc")
    .url("https://oapi.dingtalk.com/robot/send")
    .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050")
    .msg(generateActionCard()).build();
try {
    DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build);
    System.err.println(JSON.toJSONString(dingRobotResponseMsg));
} catch (Exception e) {
    e.printStackTrace();
}

至此,一个工具类构建就完成了,整个构建的过程还是十分简单的。这次的工具代码也是不断进行小改动的成果。个人的代码水平功底有限,如果有什么意见欢迎点评。

问题汇总:

下面汇总了一些个人使用钉钉花的时间比较多的点。

吐槽:其实个人感觉钉钉的机器人在错误码这一块并不是特别的直观,下面说下个人踩到的一些小坑。

关于加签测试机器人出现31000的问题

如果在添加机器人的时候进行加签是需要加入对应的signtimestamp参数才可以测试成功,这里个人卡了一会儿才明白设计者的意图,虽然很好理解,但是对于第一次使用的人不是十分友好,同时在文档里面明显对于这一块的描述比较少,这里提供一下个人的小坑说明:

首先,我们需要根据请求的时间戳和秘钥生成签名

.....
/**
 * 构建当前的系统时间戳
 */
@Test
public void generateSystemCurrentTime() throws Exception {
    long l = System.currentTimeMillis();
    String secret = "SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc";
    String sign = generateSign(l, secret);
    System.out.println("timestamp = "+ l);
    System.out.println("sign = " + sign);
}

private String generateSign(Long timestamp, String secret) throws Exception {
    String stringToSign = timestamp + "\n" + secret;
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
    return URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
}

....

生成签名之后,我们需要把时间戳签名放入到请求的URL参数里面,测试方可通过:

https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050&timestamp=1613212722591&sign=SsKKlkvwM%2F4tsCPE6YoGls8vgkQqWJGHYpvWbW7hTGM%3D
提示:还是注意一下,在设置里面增加了加签

结尾

本文主要为记录个人使用钉钉的一些心得体会,以及以此编写了一个工具包方便以后有需要的时候可以直接拿来使用。

钉钉机器人的使用就告一段落了,目前工具类已经应用到公司项目正常的发送请求通知。后续看心情对于HttpClient请求工具类重构,但是目前个人还在参考和学习设计记录,发现可以拆分的对象还是不少的。包含请求方法,请求Header,请求编码等各种形式的转化。

最后,个人最近从《代码简洁之道》里面学习了很多有用的编程技巧和编写代码的细节问题,推荐读者看一看这本书,对于写出一个好代码和好注释或者想要学习改良自己的代码都是很有好处的,后续个人也会写一篇学习笔记,感兴趣的可以关注一波。

查看原文

赞 0 收藏 0 评论 0

lazytimes 发布了文章 · 2月8日

浅谈设计模式 - 装饰器模式(五)

浅谈设计模式 - 装饰器模式(五)

前言:

​ 装饰器模式是是对类进行增强的一种典型设计模式,它允许对于一个现有类进行增强的操作,对于喜欢使用继承的伙伴,这个模式非常贴切的展示的了对于继承的灵活用法。但是装饰器模式同样不是一个推崇使用的模式,因为他对于继承存在依赖性,从本文后续就可以了解到装饰类膨胀的问题,所以在设计代码结构的时候,装饰器模式并不是第一考虑

什么是装饰器模式?

​ 装饰器模式:对现有类不改动结构的情况下为类添加新职责和功能的模式。

​ 动态的扩展类的职责,装饰器模式是一种是比继承更加灵活的代码扩展模式。同时装饰类之间可以进行互相的嵌套

装饰器模式的结构图:

  • Component 装饰接口:装饰接口定义了装饰的顶层抽象行为,一般定义被装饰者和装饰者的公用行为

    • ConrecteComponent 被装饰类:主要为被装饰类实现,和装饰类相互独立,拥有单独的功能方法
    • Decorder 装饰器:定义了装饰的通用接口,包含装饰器的通用方法

      • ConrecteDecorderA 装饰器A:定义了装饰器的具体设计,可以包含自己的装饰方法
      • ConrecteDecorderB 装饰器B:定义了装饰器的具体设计,可以包含自己的装饰方法

装饰器模式的特点

  1. 装饰者和被装饰者都需要实现相同的接口(必要条件)
  2. 装饰者一般需要继承一个抽象类,或者需要定义抽象的方法和实现
  3. 装饰者可以在所委托被装饰者的行为之前或之后,加上自己的行为,以达到特定的目的。
  4. 任何父类出现的地方都可以用子类进行替换,在活用继承的同时可以灵活的扩展。

什么时候使用装饰器模式

  • 需要大量的子类为某一个对象进行职责增强的时候,可以使用装饰器模式
  • 希望使用继承对于类进行动态扩展的时候,可以考虑使用装饰器模式

实际案例:

模拟场景:

我们用一个奶茶的结构来模拟一个装饰器的设计场景,我们通常在奶茶店点奶茶的时候,对于一杯奶茶,可以添加各种配料,这时候配料就是奶茶的装饰者,而奶茶就是典型的被装饰者,我们使用配料去“装饰”奶茶,就可以得到各种口味的奶茶。同时可以计算出奶茶的价格

下面我们来看一下针对模拟场景的案例和使用:

不使用设计模式:

​ 不使用设计模式,我们的第一考虑就是简单的使用继承去设计装饰类,我们通过各种子类组合来实现一杯杯不同口味的奶茶,从下面的结构图可以看到,将被装饰类定义为独立的类,同时不进行任何的继承而是作为独立的类使用。而调料也就是奶茶饮料的配料需要继承同一个抽象类,同时在内部实现自己的方法。

​ 紧接着,我们在装饰者的方法中引入被装饰者,可以通过内部组合被装饰者进行 模仿行为的同时进行增强,就像IO当中的Buffer

​ 我们根据上面的说明画出这一种设计的大致结构图:

看了上面的设计图稿之后,我们来说明一下具体的代码实现:

首先是奶茶实体类:在奶茶的实体类里面定义两个属性, 使用一个display()打印信息,奶茶的实体类表示被装饰类

/**
 * 奶茶实体类
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/7 22:21
 */
public class MilkTea {

    private String name;

    private double price;


    public MilkTea(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public double getPrice() {
        return price;
    }

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


    public void display() {
        System.out.println("name = "+ name + " price = " +price);
    }
}

下面是柠檬汁的被装饰类,这个被装饰类也是独立的:

/**
 * 柠檬汁
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/7 22:53
 */
public class LeamonJuice {

    private String name;

    private double price;


    public LeamonJuice(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public double getPrice() {
        return price;
    }

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


    public void display() {
        System.out.println("name = "+ name + " price = " +price);
    }
}

调料的父类:注意这是一个抽象类,定义了调料的基本方法。

/**
 * 调料父类
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/7 22:23
 */
public abstract class Codiment {

    /**
     * 为装饰类添加附加值
     * @return
     */
    abstract void plusAdditionVal(MilkTea milkTea);

    /**
     * 详细信息
     */
    protected String description(){
        return "无任何配料";
    }

}

调料的子类珍珠类,这里为父类进行装饰,添加父类的信息

/**
 * 配料:珍珠
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/7 22:27
 */
public class Pearl extends Codiment{


    @Override
    void plusAdditionVal(MilkTea milkTea) {
        if(milkTea == null){
            throw new RuntimeException("对不起,请先添加奶茶");
        }
        milkTea.setPrice(milkTea.getPrice() + 2);
        milkTea.setName(milkTea.getName() + "," +description());
    }

    /**
     * 详细信息
     */
    protected String description(){
        return "珍珠";
    }
}

调料的子类椰果类,这里同样是为了父类进行装饰的方法:

/**
 * 配料:椰果
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/7 22:30
 */
public class Coconut extends Codiment{
    @Override
    void plusAdditionVal(MilkTea milkTea) {
        if(milkTea == null){
            throw new RuntimeException("对不起,请先添加奶茶");
        }
        milkTea.setPrice(milkTea.getPrice() + 1);
        milkTea.setName(milkTea.getName() + "," +description());
    }

    @Override
    protected String description() {
        return "椰果";
    }
}

最后我们使用一个单元测试:

/**
 * 单元测试
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/7 22:34
 */
public class Main {

    public static void main(String[] args) {
        MilkTea milkTea = new MilkTea("原味奶茶", 5);
        Pearl pearl = new Pearl();
        Coconut coconut = new Coconut();
        pearl.plusAdditionVal(milkTea);
        coconut.plusAdditionVal(milkTea);
        milkTea.display();
    }
}/*
打印结果:name = 原味奶茶,珍珠,椰果 price = 8.0
*/

不使用设计模式的优缺点:

优点:

  • 添加一个装饰者十分简单,只需要继承抽象父类接口,同时子类只需要通过方法传入被装饰者进行装饰。

缺点:

  • 我们的调料父类如果增加抽象方法所有的子类都需要改动,这是整个子类群体来说是毁灭性的,对于编写代码的程序员来说也是毁灭性的。
  • 可以看到装饰者已经是一种面向实现编程的状态,如果我们换一种被装饰者,需要添加更多的装饰类进行装饰。并且这些装饰者是相互独立并且不能复用的
从结构图的设计就可以看出这种设计不符合面向接口编程的设计原则

总结不使用模式:

​ 不使用设计模式看起来没有什么大问题,但是可以从结构可以看到抽象父类以及子类的耦合过于严重,父类完全不敢动abstract void plusAdditionVal(MilkTea milkTea)这个抽象签名方法,并且如果需求增加一个其他的被装饰者,这些装饰奶茶的装饰者就完全“傻眼”了,因为他们完全不认识新的被装饰者,这导致程序要更多的子类来接纳新的的被装饰者,这种设计结构将导致类子类无限膨胀,没有尽头。

使用设计模式:

​ 从不使用设计模式可以看出,不使用设计模式最大的问题是在于调料的父类抽象方法耦合过于严重,以及被装饰类和装饰者之间存在依赖磁铁。从结构图可以看出来被装饰类和装饰类并没有明显的关联,我们之前已经说明了装饰模式更多的是对于一个被装饰类的增强,既然是增强,那么被装饰类和装饰类通常需要具备相同的抽象行为,这样才比较符合装饰模式的设计结构。

​ 下面就上面的结构图进行改进,在 被装饰类装饰类之上,再增加一层接口,调料的父类不在管理公用接口,而是可以增加自己的方法。我们改进一下结构图,只要稍微改进一下,整个结构就可以变得十分好用:

为了方便展示代码和理解,这里只列出了奶茶类调料父类配料:珍珠,以及我们最重要的公用接口进行介绍:

我们从最顶层开始,最顶层在结构上定义了一个抽象公用接口,提供装饰者以及被装饰者进行实现或者定义抽象和扩展:

/**
 * 饮料的抽象类,定义饮料的通用接口
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/7 23:46
 */
public interface DrinkAbstract {

    /**
     * 装饰接口
     */
    void plusAdditionVal();

    /**
     * 计算售价
     * @return
     */
    double coat();
}

然后是奶茶类,我们的奶茶类在上一个版本基础上,实现了一个新的接口,所以需要定义实现接口后的方法:

奶茶类:

/**
 * 奶茶实体类
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/7 22:21
 */
public class MilkTea implements DrinkAbstract{

    private String name;

    private double price;


    public MilkTea(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public double getPrice() {
        return price;
    }

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


    public void display() {
        System.out.println("name = "+ name + " price = " +price);
    }

    // 增加
    @Override
    public void plusAdditionVal() {
        System.out.println("name = "+ name  + " price = " + price);
    }

    // 增加
    @Override
    public double coat() {
        return price;
    }
}

下面是调料的父类,调料的父类需要改动的内容不是很多,本质上就是把自己的抽象方法提取到父接口。这个类可以是抽象类,也可以是配料接口的通用抽象:

/**
 * 调料父类
 * 这里需要实现饮料接口
 * @author zxd
 * @version 1.0
 * @date 2021/2/7 22:23
 */
public class Codiment implements DrinkAbstract{


    /**
     * 为装饰类添加附加值
     * @return
     */
    public void plusAdditionVal(){
        description();
    }

    @Override
    public double coat() {
        return 5.0f;
    }

    /**
     * 详细信息
     */
    private String description(){
        return "无任何配料";
    }

}

最后是配料的具体实现类配料-珍珠进行改动:

/**
 * 配料:珍珠
 *
 * @author zxd
 * @version 1.0
 * @date 2021/2/7 22:27
 */
public class Pearl extends Codiment implements DrinkAbstract{

    private DrinkAbstract drinkAbstract;

    public Pearl(DrinkAbstract drinkAbstract) {
        this.drinkAbstract = drinkAbstract;
    }

    @Override
    public void plusAdditionVal() {
        // 如果是奶茶
        if(drinkAbstract instanceof MilkTea){
            MilkTea drinkAbstract = (MilkTea) this.drinkAbstract;
            drinkAbstract.setName(drinkAbstract.getName() + " -- " + "珍珠");
            drinkAbstract.setPrice(drinkAbstract.getPrice() + 55);
            description();
        }
    }

    @Override
    public double coat() {
        return 5;
    }


    /**
     * 详细信息
     */
    private void description(){
        drinkAbstract.plusAdditionVal();
    }
}

最后,我们来看下单元测试的变化:

public class Main {
    private static void run2(){
        DrinkAbstract drinkAbstract = new MilkTea("原味奶茶", 5);
        Pearl codiment = new Pearl(drinkAbstract);
        codiment.plusAdditionVal();

    }
    public static void main(String[] args) {
       run2();
    }
}/*控制台结果:name = 原味奶茶 -- 珍珠 price = 60.0*/

可以看到我们使用装饰类对于被装饰类的属性进行了改变的同时并没有改变被装饰者的本身的行为,而是对于行为做了扩展。

使用装饰器设计模式的优缺点:

优点:

  1. 装饰类的公用类不再需要设置抽象的方法,使得装饰实现子类也不在依赖抽象父类的抽象方法
  2. 既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象(被包装的)的场合,就可以用装饰过的对象代替它。
  3. 装饰类和被装饰类的扩展和实现都是解耦的,不需要互相关注实现细节,装饰子类可以独自实现方法
  4. 我们解决了增加新的被装饰类之后导致装饰类大量膨胀的问题,现在可以进行简单的应用。

缺点:

  1. 本质上还是继承结构,而且装饰类和被装饰类必须有相同的顶级父类接口
  2. 装饰类在系统越来越复杂之后会出现明显的膨胀。

JAVA IO - 典型的装饰模式:

​ 首先说明JAVA IO类其实本质上并不是一个十分优秀的设计(因为复杂的装饰子类和API结构),这个问题可以查看《JAVA编程思想》作者对于JAVA IO复杂难用的API以及继承结构进行过的一系列吐槽,而且JAVA IO经过后面版本的迭代改进。使得原本的方法更加复杂多变,但是不管JAVA IO设计的API如何不“便民”,这一块的设计依然是非常值得学习和思考的,也是装饰模式最典型的使用。

​ 下面为一张《Head First设计模式的一张图》说明一下JAVA IO装饰设计的装饰器膨胀问题:

  • 可以看到InputStream是一个抽象类。
  • JDK1.5当中,他扩展自接口java.io.Closeable,规定需要接入装饰的类需要实现自己的流关闭方法。
  • JDK1.7 中,在Closeable基础上增加了java.io.AutoClosable来实现流的自动关闭功能。

从上面的图标也可以看到装饰器的一些缺点:

  1. 装饰类之间的具有复杂的继承结构
  2. 装饰者之间虽然可以互相嵌套,但是不一定互相兼容
JAVA IO对于JAVA初学者来说十分不友好,从其他语言可以看到吸取了这一点的教训,通常都把IO流这一块设计的越简单好用越好(尽量的让调用者不需要去思考IO流的细节问题)。而JAVA IO 显然设计的不是很亲民。

总结装饰器模式:

优点:

+ 装饰者和被装饰对象有相同的接口。
+ 可以用一个或多个装饰者包装一个被装饰对象或者被装饰对象。
+ 既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象(被包装的)的场合,可以用装饰过的对象代替它。
+ 装饰者可以在所委托被装饰者的行为之前或之后,加上自己的行为,以达到特定的目的。
+ 装饰者可以无限的嵌套,因为他们本质上归属于同一个接口

缺点:

+ 装饰者很容易出现大量的小类,这让了解代码的人不容易清楚不同装饰的设计
+ 一个依赖其他具体类型的接口导入装饰者可能会带来灾难。所以导入装饰者要非常小心谨慎,并且仔细考虑是否真的需要装饰者模式
+ 装饰者互相嵌套可能会增加代码的复杂度,也增加扩展装饰者子类的复杂度,最终这个难题会变成调用者的难题

总结:

​ 许多的设计模式书籍都警告过装饰器模式是一个需要谨慎考虑的设计模式,因为装饰模式很容易会造成装饰类的膨胀,同时对于特定类型接入装饰类可能会有意想不到的灾难,同时在接入装饰类的时候,需要仔细的了解公用接口和抽象类的实现,需要了解这一类装饰针对的行为,否则只是简单的继承装饰父类或者继承接口可能会有一些莫名其妙的问题。

查看原文

赞 1 收藏 1 评论 0

lazytimes 发布了文章 · 1月31日

我的读书方法论(一)

我的读书方法论(一)

前言:

这篇文章主要是之前的读书笔记发文有评论说让我写一篇文章介绍下如何读书和做读书笔记的,本人对于答应的事情都会尽力去完成,所以这篇文章是个人读书方面的总结。具体要不要照做就由读者自己决定了。个人用碎片时间把:《小狗钱钱》、《穷爸爸与富爸爸》、《Redis实战》、《漫步华尔街》.....还是看了不少书的。

特别提示一下:读书经验和5W1H的法则几乎在哪里都是通用的。本篇文章为个人的经验分享。

文章目的:

  1. 说到做到,承诺第一。这是个人的信条。
  2. 个人买书、看书的心得和理解。
  3. 读书既是兴趣,也是个人爱好,说一下个人的读书方式和实践
  4. 适合的才是最好的,这篇文章更多的是经验参考而不是照搬
  5. 如果不会规划和计划,那就先开始行动。行动派往往会有所收获

思维导图:

我把这篇文章做了一份思维导图,如果觉得文章内容太多建议可以看看思维导图:

https://share.mubu.com/doc/5i...

幕布思维导图

碎片时间

什么是碎片时间?

在具体介绍之前,说明一下个人看书更多的是利用碎片时间看书。我相信工作党利用好碎片时间读书才是最重要的,因为工作一周之后周六周末还要读书真的是一件比较难的事情。

如果是在校学生,那么基本不存在碎片的时间,除非存在很长的通勤时间或者存在兼职等。至于上班党的碎片时间基本就是地铁公交车或者路上了。

  1. 上班的通勤时间或者远程旅行路途的时间
  2. 发呆时间:无论工作还是学习,人脑都不可能保存100%的全神贯注,随着时间的推移,人脑会不自觉的“发呆”
  3. 任何细碎的事情开销时间,吃饭,刷牙等等
  4. 路上琐碎的时间。

如何利用好碎片时间?

  • 个人买了一个Kindle Oassis2 在地铁上看电子书,同时推荐买一个kindle看书,很轻很方便。当然要有一定的经济能力。
  • 手机上可以用微信读书看书,或者用樊登读书进行听书,喜马拉雅听说也不错,不过我没有试过,不做评价
  • 地铁或者公交车上看书(最常见)
  • 阅读以前个人做的笔记(帮助反馈)
个人的KINDLE是购入二手,电纸书个人觉得买个二手完全OK,面交我也不怕有什么猫腻。如果自己看走眼也怨不得任何人。还是强烈推荐弄个电纸书看书,能抱着手机不分心看书的人一定都是很强的人,反正我是做不到(看小说除外)

碎片时间的计算:

下面的内容比较理想化,内容主要和自己的生活比较贴合。粗略看下即可。

通勤时间:一个半小时

碎片时间以个人的生活为例

前提:手机或者电纸书。

个人上下班约等于1个半小时

除开必要的20分钟路上消耗的时间,这个时间不建议看书,而是好好看路,也不要看手机。这个时间就好好走路,注意走路的姿势。

去年个人发生过一件事情让自己在路上再也不敢玩手机,当时在过马路遇到过路拖着3个煤气罐的摩托车,差点撞到,从那天开始很少在马路上拿出过手机。

地铁上的1个左右小时,这里合并了上下班,换线等时间进去,这里的时间可以看大概10到20页书左右,看的快可以把一本很厚的书扫完一小半,厉害的可以扫完一大半。这里提一下地铁上不建议看博客或者过于专业的内容,除非你在到了公司之后可以马上进行记录和回顾,地铁上也不建议过于深度思考,容易坐过站,而且换线次数多很容易打断思路。

通勤时间推荐:听书代替文字阅读,视频教程代替文字阅读,思维导图代替文字阅读

午休时间:两小时

个人午休时间一般为10到15分钟,同时公司午休两个小时,这个时间的碎片时间可以干不少事情。

  1. 吃饭半小时:由于个人一般自己做饭带饭,很少出去外面吃,所以吃饭的时间开销不是特别大
  2. 午休15分钟左右,浅睡眠即可,深睡眠被叫醒影响下午的工作
  3. 当别人在休息,你在充实自己的时候,会很开心。

午休时间推荐:看专业书籍、看看视频教程、听听音乐,谢谢文章和笔记。

下班后时间:三到四小时

这里的时间是除开了在路上的时间。下班后的时间需要好好利用,下面说下个人在做的一些内容:

  1. 看些专业之外的书籍(睡前读半小时)
  2. 看专业的书籍,做好笔记和内容分类
  3. 刷题或者做练习,思考和查资料
  4. 看视频教程,边听边做笔记
  5. 系统学习,输入和输出自己所学所想
  6. 回顾和总结以前的内容
  7. 定目标,做计划,完成并且打卡

周六周日:

这一部分变动比较大,有时候会出去锻炼一下身体,有时候会在家看书或者写写文章啥的,个人没有做过特别具体的安排,这里就不做论述了。

  • 自由安排
  • 锻炼身体
  • 写文章
  • 做笔记
  • 看视频教程

规划自己的碎片时间

上面的计算只是粗略的规划,还是建议读者有自己的规划,个人用APP作时间规划,这里就不过多展示了,否则容易被限制思维而且估计也不太贴合所有人。规划碎片时间的目的是让自己清楚什么时间要做什么事情,久而久之形成习惯之后,自然就可以总结出自己的那一套经验。

个人比较自由,不喜欢做表格或者卡时间点,因为发现自己曾经做的具体计划最终因为琐事都放弃,所以后续个人只要在自己设置的时间完成了自己该做的事情,就算是达标了。这一点希望读者不要学,我是一个懒人,懒人有懒人的办法,哈哈。

读书之前书的准备

读书之前你先得有一本书,同时要有一个良好的环境,下面说下读书之前的一些准备以及一些买书的方案。

事前准备:

  1. 把手机塞到看不到的地方
  2. 一把舒适的椅子,最好可以躺下
  3. 一个支撑书的阅读架或者任何可以把书撑起来的物品
  4. 一本想看的书,只要一本就足够
  5. Ipad或者KINDLE(视个人)
  6. 一份购书清单和读书的清单
个人买的NICE202d阅读架:

https://gitee.com/lazyTimes/i...

个人做的打卡本:

https://gitee.com/lazyTimes/i...

购书清单:

  1. 列出自己想看的所有书籍。
  2. 合适价格,多家对比。
  3. 寻找合适的购书渠道。
  4. 自己是否真的想看,是否真的能看完

如何买书?

在去年以前我很抵触买二手书这件事情,但是今年有了改观,其实二手书对于有书本洁癖的人来说是碰不得的东西,但是我个人比较偏向书本内容,也没有收藏书本的爱好,所以今年换了一种买书方式,选择在专业的二手书机构买书,买完总结好然后卖回去,循环利用。

买一手书的方式:

一手书现在买的已经比较少了,买的一手书一般都是自己会保留较长时间兴趣书,可能是自己喜欢的书或者一些漫画。

  1. 狗东:狗东买书个人也是冲着满减去的,没有满减我也不会买,买之前找一些APP看下价格对比,价格合适我就直接下单了
  2. 当当:不建议去当当买,而且要买就乘着满100减50的时候买书,有时候存在优惠券啥的可以买
  3. 某宝:某宝买书买的比较多的是台版的小说,技术书籍买的比较少。

买二手书的方式:

买二手书也就那么几家,所以无所谓打不打广告了。个人的策略是买入二手书之后做好记录然后抽时间卖出去这种循环利用的形式。

转转:

转转的书 大部分时候比较靠谱,小部分时候存在缺页的问题,比如个人之前在读书笔记里面的 《恶意》这本书。同时转转上的书也比较靠谱,目前个人买书没有发现和商家描述出入很大的地方。

闲鱼:

闲鱼买书基本就是两种,一种是二手贩子,不过二手贩子大多书都是盗版扫描版,纸质非常差,买过一次之后我就没有在上面买过二手贩子的书。

第二种是搬家带不走出售,这种情况比较还是比较多的,这种看一下卖家个人评价基本可以放心买了。有时候运气好可以买到几乎全新的书,比如个人去年低价购买了一本《计算机程序的构造和解释》:(个人也有一个十多斤的书架和书=-=)

个人在闲鱼买的一本准全新的书:

https://gitee.com/lazyTimes/i...

多抓鱼:

多抓鱼的书第一次买是非常实惠的,对于新用户优惠的力度很大,但是二次之后购买需要80多才包邮,所以多抓鱼并不推荐买少量的书,而是推荐大量购入书的时候买入。

二手书对于书本爱惜的人慎入。

关于借书:

买书还有一种特殊的形式是借书,通过借书的形式推动自己看书也是一种办法,不过需要注意借书不能在上面进行勾画,所以需要好好爱惜。

借书的成本一般都比较低,有些公司或者学校的图书馆都提供免费的书籍借阅,还是挺不错的。建议多借一些和公司业务贴合以及专业的书,对于自己的成长也是有帮助的

读书笔记该怎么写:

读书笔记最主要的是自己从中学到了什么,精简自己学到的内容。最后归总到自己的笔记里面。然后就是定期回顾,不断总结和完善。

读书之前思考:

读书是一件代价很大的事情(时间成本),在看书之前一定要考虑好这本书要从里面学到那些内容。下面就读书的思考说一下个人的观点。

  1. 我为什么要看这本书?

    1. 被人推荐
    2. 偶然得知书名
    3. 慕名已久,想看
    4. 无聊
  2. 看了这本书之后,我要学到哪些东西?

    1. 扩展眼界,原来这本书这么有趣
    2. 惊喜,我从书中的内容纠正了以往的观念
    3. 这本书没有营养,以后少看这种书
    4. 鸡汤书,感化心灵却没有实际意义
  3. 画思维导图

    1. 构图:思维导图的大致设计
    2. 想看的内容:内容进行摘录
    3. 切勿模仿,思维导图是根据个人习惯来的,COPY别人的思维导图实际作用不大(个人亲身感受)
  4. 做好阶段笔记

    1. 第一阶段:摘录书中内容,简单批注自己的看法
    2. 第二阶段:整理思维导图,时常回顾
    3. 第三阶段:复盘和总结,提炼知识点
  5. 这本书我要看几遍

    1. 一遍看完:读的快还是慢,是仔细看还是大致扫一眼(个人主要是一遍看完)
    2. 至少看三遍:一遍大致了解,两遍反思内容,三遍精简内容。
    3. 在精不在多,侧重某一处内容。
  6. 这本书我要多久看完

    1. 设置一个很长的时间段,每天看多少页(很强很强的执行力
    2. 随机阅读:有时间就读,有碎片时间就读(很强的自律性
    3. 缩短时间,设置一个死线,在死线时间之前看完(意外情况较多,完成率较低,但是最推荐的方法
    4. 什么时候看完不要紧,只要看完就行了(推荐看书就想睡觉的人

读书方法:

这里可以看一下我的其中一篇读书笔记:

https://juejin.cn/post/687629...

  1. 快速读:每页只看一秒,在5分钟内看完一本书
  2. 读三遍:每一遍都有不同的体验
  3. 想怎么读就怎么读,自由的读
  4. 每次只读30分钟,去干别的事情
  5. 读一点点,不需要全部看完
  6. 抄书
  7. 把看到的东西读出来
  8. N/Z阅读法
  9. 书评视频
这本书也是十分推荐看的一本书,很小一本的口袋书,可以随时拿起来翻翻看,很快乐

实践书中的内容

读书是为了学以致用,当然也不是所有书都可以马上学以致用的,但是我们可以努力回想那些时候可以实践并且写到自己的读书笔记里面。

实现书中的内容,不管是读书还是做笔记都可以分为以下三种:

  1. 克隆:说白了就是把觉得重要的点或者感兴趣的地方做标记,因人而异
  2. 深度思考:这个阶段说明你在消化书中的内容,也说明你真的把书看进去了
  3. 记录和总结:针对作者某一处的点,进行评价和讨论
  4. 举一反三,尽量批判性角度看待书中的内容。之所以说尽量是因为没有人可以完全客观的看待任何一个事物,要知道写书人的经历和读者的经历肯定是完全不同的。

这本书带给你什么?

这里不以书为例,以最近个人看的漫画 《进击的巨人》,网上有评论说这是除开《钢之炼金术士》的又一部神作,里面有一段是将艾伦对于自由的看法,同时个人的理解如下:

巨人漫画:“每个人都是自由的,我为了保护重要的事情做出我的行动是我的自由,你们阻止我达成我的目的是你们的自由,我所向往的自由会阻碍你的自由,而你的自由同样会阻碍我的自由,这时候我们的信念互相冲突,为了我的自由和你们的自由,唯一方式就是战斗”。

个人观点:自由是有尺度的,没有限制的自由最终基本演变为强权和专制。越高的自由,对自由的限制就越为宽泛,我们时常用我们自己的思想妄图去 控制时间,其实这是为了达到我们自己的自由,每个人有每个人的活法,他人的自由想法是他人的意愿,与我无关,尊重他人的自由其实是一件非常难的事情。

可以举例来讲就是最近几年兴起的网络暴力,网络语言的力量是十分强大的,他控制着他人的自由。

另外举一个例证:离开了手机,你能否活下去,答案是 完全不能。所以手机实质上已经限制了我们的“自由生活”。

每个人对于同一段内容的理解都不相同,所以读书首先要确定的是自己的立场,自己怎么看待作者想要表达的意思。如果带给自己的东西给自己的思想有所进步,同时让自己往好的方面进步,那就是对你带来好的帮助,否则带来的收效就很小。

写下自己的感受并且发表:

  1. 永远要记住看书其实就是和作者对话,我们不要被作者花言巧语给骗了,看书一定要写上自己的观点,不管是同意还是不同意,主动学习和思考对于吸收和归纳书籍的内容是很有帮助的。
  2. 其次是发表自己的观点,很多人喜欢把知识藏着掖着,早些年个人也是不太喜欢分享自己的所学所想,怕别人说幼稚或者批评和错误,但是工作之后 脸皮变厚了,很多事情都敢放下脸面去做了
  3. 总结和反馈,是最好的良药,可以随身揣个小本本,遇到好东西或者好的话语直接记在本子上,其实也是一种不错的方式。

做好笔记,最合适的才是最好的:

网上有很多人介绍自己的学习方法,比如近几年比较推崇的 电子化学习方式,个人目前也是用的这种方式,但是个人早期也是一个忠实的 纸质化学习方式执行者。至于为什么,下面说下我的理解:

纸质化笔记和电子笔记对比:

下面就个人理解来对比一下纸质笔记和电子笔记。说下各自的优劣以及我的做法。

纸质笔记的优劣:

优势

  1. 一处内容相当于理解了三遍,肌肉记忆了一遍,大脑记住了一遍,看到了一遍,对于经常做纸质笔记的人来说非常快。
  2. 读书笔记可以随取随时翻页,可以快速的阅览
  3. 纸质笔记最大的优势在于可以前后对比,同时非常快速的进行回顾

劣势

  1. 携带和存储不便,需要一定的环境
  2. 纸质笔记时间长了页面发黄,字体模糊等
  3. 纸质笔记的记录速度比较慢

电子笔记的优劣:

优势

  1. 易存储,可以存放各大APP或者网盘,迁移方便
  2. 打字的速度要比纸质笔记要快很多,记录快速
  3. 方便整理和总结。

劣势

  1. 电子笔记也存在丢失的问题,所以建议多备份
  2. 电子笔记一般存储在三方平台等,不能存储机密信息
  3. 电子笔记无法对比和复盘

为什么我不用纸质笔记

  1. 在外地工作,纸质笔记不仅不便携带,并且搬家成本很高
  2. 电子笔记可以更好的分享而纸质笔记自己偏向个人的感受
  3. 打字的速度终究快于写字的速度
  4. 快节奏的时代想静下心来写写笔记对个人来说是件难事

电子笔记的几点警告:

  • 首先需要确定自己会不会分神,就好比我们想打开电脑或者手机想看本书的时候,不自觉的就和别人聊天或者干其他的事情
  • 每次只做一件事情,一次把一件事情做好,厚书读薄。
  • 可以写一些便签,贴在最显眼的位置,反复警告自己要去实践和完成。
  • 如果不能控制自己,最好不要买各种多样化的电子平板,否则很容易爱奇艺。

我是如何整理电子笔记的:

  1. 我的笔记和文章都是按照 月份去归纳的,到目前已经记录了两年的笔记,所以一直沿用这周记录方法,很简单,按照 年份+月份的方式放在一个文件夹
  2. 目录很重要,先设计目录,设计标题,然后细化分类,方便自己的回顾和总结
  3. 多备份,不要完全依赖各种APP或者电脑硬盘。个人常备一块移动机械盘进行存盘
  4. 养成定期备份(纸质笔记无法备份的劣势)

刻意练习才能越做越好:

我的观点是:再好的计划和规划不如马上行动。一旦有了想看书的想法就立马下单买一本书。因为你进行了投资,所以

这里同时也推荐一下《刻意练习》这本书,这本书精简起来其实就是:有目标,达成目标不断练习,反馈,产出。

不想看书的推荐去 “樊登读书”看一下樊登的概括。基本和看一遍书类似

笔记做出自己的特色:

这一部分比较主观,还是推荐一些外力工具辅助吧。

  1. 荧光笔:荧光笔适用于自己的书,同时建议多配颜色,内容标色可以有醒目的提示。目前这种形式多见于喜欢纸质笔记的人。
  2. 便签:可以买一些标签贴或者一些标签,直接贴在对应的页码进行备注,写下自己的感受和批注等
  3. 书签:书签和上面读书方法一致,个人买的实体书基本都会配一个标签,和做任务的任务进度一般,可以培养成就感

怎么样才能坚持读书?

借助外力:

远离手机:

远离手机的方式有很多,但是我发现很多干编程的朋友都不喜欢在周末看电脑,个人比较喜欢看电脑,包括文档和学习都是看电脑完成

做一份打卡记录表:

个人目前在某拼团APP买了一本打卡本,每天记录自己打卡的进度,当然个人完成的进度不是很好,还要更加自律才行。

寻找TODO软件:

这里推荐一个APP:时光序,这个APP对于制定目标和每天打卡挺好的,并且可以设置每天打卡提醒自己完成任务。

还有一个是微信的小程序:没有土豆,非常的简单,就是一个打卡的功能,对于习惯单纯打卡功能的人比较好

最后是推荐使用番茄工作法的APP,番茄TODO,很贴合番茄工作法,比较好用。

散步放松心情:

当你学习一大段的内容或者因为某些难题困住的时候,适当放松一下也是有必要的,因为给大脑一个缓存或许思路可以解开。

凝聚注意力:

我们经常被琐事分散注意力,所以最好寻找一段连续的时间来看书,如果经常被打断看书的效率会大打折扣。同时给自己定时定量看书,比长时间看一本书效果要好,同时读书建议定时休息,有助于回顾总结和思考。

自律

​ 我是自学入了编程的行业,所以自律对我来说不是什么难事,自律这种东西还是靠行动,个人不太喜欢做太过详细的计划,只会规定每天要做什么事情,并且要做出什么样的效果,同时要对自己的进度有清醒的认知和把控,哪怕没有完成,也要自我正视,然后去寻求突破和改变。

​ 另外,自律是无法模仿的,每个人的的三观由他所处的环境决定,所以多想想怎么做才适合自己。

躲避诱惑:

很多人会觉得为什么别人可以坚持做一件事情那么久,我想无非就是躲避诱惑力,没错,就是躲避诱惑,我相信绝大多数自制力强的人不是克制诱惑而是躲避诱惑,或者是真的想要改变的人才会达到真正的自律。

总结

本文从买书到读书,到最后的坚持读书都做了一些讨论,希望这些观点对你有帮助,不知不觉又码了很多的字,不知道有多少人可以坚持看完的,这是一篇完整的总结,后续还会不断的回顾这篇内容进行精简。另外,最重要的一点是不要害怕忘记,而是要想办法去留住更多的知识在自己的脑海,把东西学成自己的东西才是最重要的。好了,本文到此结束,希望这篇文章可以给文章开头的评论者一个比较满意的答复=v=

查看原文

赞 0 收藏 0 评论 0

lazytimes 发布了文章 · 1月30日

《SpringBoot实战派》读书笔记

《SpringBoot实战派》读书笔记

前言:

这本书是个人抽奖送的,但是看完的感觉就是心情复杂。看了几天之后算是对于SpringBoot做了一个回顾,但是脑子里没有留下印象太深的东西。这篇读书笔记个人本来不太想要写的。但是秉持着看书必须写读书笔记的习惯,还是评价一下这本书。

推荐程度:

豆瓣7.1的打分。

不值得收藏的书,也不值得购买,80元你可以买好几本更有质量的编程书。甚至买几本出名的小说也可以比这个好。

对于熟悉的人,可以拿来查漏补缺,也可以拿来做半个工具书。

如果你是了解了springboot同时想要深入SpringBoot的,这本书也是没有什么价值。没有学过SSM直接上手SpringBoot看一下。这本书还是可以看一下的。

本书评价:

  • 优点

    • 适合初学者
    • 适合对于spring boot从未接触的人
    • 内容总结比较齐全,涵盖spring boot的应用
  • 缺点

    • 代码占了很多篇幅
    • 内容比较基础和入门
    • 很厚,但是没有营养,不建议买

思维导图:

简单画了一下一些内容,加上自己做的一些笔记

https://share.mubu.com/doc/60...

目录截图:

https://gitee.com/lazyTimes/i...

https://gitee.com/lazyTimes/i...

https://gitee.com/lazyTimes/i...

感悟:

作为小白学习来说,这本书算是不错而且合格的书,但是技术的更迭实在太快了,这些书基本参考大于实际的使用价值。

这次的文章希望可以给想买书的人一点提醒把,这本书对于想要深入SpringBoot的人没有啥价值。

总结:

SpringBoot的东西还是建议多看看官方文档,或者说所有的东西学习都建议看官方文档,毕竟设计出框架的人虽然不见得可以讲得很好,但是讲得东西绝对都会对的。

越来越感觉近几年写的好书越来越少,不得不跑去看以前的一些旧书,这本书还是差点意思。

技术书还是建议多做做笔记,然后思考可以学到什么再去看,这样效果会比较好。最近跑去转转上买了不少书,开始慢慢看咯。

查看原文

赞 0 收藏 0 评论 0

lazytimes 发布了文章 · 1月30日

JAVA基础小项目 - 坦克大战

JAVA基础小项目 - 坦克大战

前言:

这个项目是之前备份电脑资料的时候看到的,不禁一阵感慨自己当初自学编程的心酸和泪水。所以分享一下自己当初写的的垃圾代码。虽然我不是任天堂忠实粉丝,但是对于90后来说坦克大战基本是人人都玩过的一款小霸王游戏机的游戏。

这个项目对于已经入行的人来说没有价值,分享出来主要是希望对于初学编程的人给一点 “吸引”吧,原来代码可以做到这么愉快的事情。这个坦克大战也是自己跟着培训机构的教学视频边看边敲的。

花了一天左右的时间把代码稍微整理了一下代码同时写了这份文档,这里分享出来。

对于入行编程的同学个人的建议如果要快速成长还是多练,多做。很多东西做多了之后,涉及自己的盲区会促使你不断的学习和进步。

PS:代码使用Eclipse写的,这里用IDEA整理了一下代码

此代码如果阅读存在难度,建议看一下韩顺平的JAVA基础坦克大战的课程。这里的源代码也是跟着课程敲的。

https://www.bilibili.com/vide...

真是一个好时代,当初这些资源还要网上翻半天

<!-- more -->

项目地址

https://github.com/lazyTimes/...

后续文档更新请看readne

项目简介:

个人前几年自学的时候从一个教学视频的,韩顺平老师的JAVA初级坦克大战。个人跟着视频边学边敲之后,对于编程的兴趣大大的提升。后面越学越快乐。

所用技术

  1. JAVA GUI(远古技术,千万别深入,看看即可)
  2. JAVA

面向群体:

  1. 初学JAVA者,可以看看这个项目锻炼动手能力
  2. 完全不了解什么是面向对象
  3. 对于小游戏有点兴趣的
  4. 如果你厌恶枯燥的学习,做一个小游戏或许能给你一点动力

项目截图:

操作坦克的方法:

最后一版本有效,早起版本部分功能或者所有功能按键无效

  • WASD
  • J:为射出子弹

需求文档(或许是):

由于当初是跟着视频做的,虽然具体的记忆忘了,但是自己跟着敲的同时忘了做需求的更新,所以这里有部分需求和思路断了。如果有不同的,后续补充GIT的README文档。

/*
 *         需求:
 *             坦克大战:
 *             功能:    
 *                 1.画出坦克,
 *         
 *         思路:
 *             1.首先坦克想象由五个部件组成两个矩形,一个长方形或者正方形,一个圆
 *                 一条直线
 * 
 *             2.画坦克的时候需要使用到画笔工具
 *                 必须在构造函数初始化使用画笔工具
 * 
 *             3.在设置方向以及画出不同方向的坦克
 * 
 *             4.敌方坦克画出来需要使用父类方法
 *                 敌方坦克的坐标需要设置,    
 *                 使用一个集合保存敌方坦克Vector集合便于删除和添加 
 * 
 *             5.发射子弹是一个线程
 *                 具有线程的功能
 *                 另外线程对与子弹方向运动轨迹不同
 * 
 *             6.需要把子弹画出来
 *                 在按下J键的时候发射子弹
 *                     实现连发使用集合存储
 *                 
 *         升级:
 *             1.让敌人能够发射子弹
                解决方法
                    1.敌人发射子弹是一个多线程方法,应当在敌人的run函数当中实现
                    2.坦克发射子弹和移动都是坦克本身具有的功能
 * 
 *             思路:
 *                 1.在敌人类里面需要添加一个射击方法
 *                     与我方一样,但是敌人是自动射击或者说每过几秒射击一次
 * 
 *                 2.我方坦克子弹连发
 *                     使用一个集合保存建立的对象,画出子弹使用集合中的对象
 *                     我方坦克子弹连发过快,需要限定        
 *             
 *                 3.
 *                     我方坦克击中敌人坦克之后,敌人坦克就要消失
 *                     需要获取到敌人的一个定点坐标,然后界定一个范围
 *                     写一个专门的函数判断是否击中敌人
 *                     
 *                     在哪里判断是否击中敌人
 * ·                因为每一颗子弹都要与所有的坦克匹配,并且每一次匹配都要
 *                     双重判断每次都要进行建立对象
 *                     图片问题没有得到解决
 * 
 *         升级
 *                 1.需要实现敌人的坦克不断的移动使用多线程的手段实现
 * 
 *                 2.需要实现敌人能够发射子弹的功能
 *                 实现方法:
 *                     建立一个敌人的子弹集合
 *                     如何敌人何时发射子弹?
 *                     使用多重循环判断是否需要添加薪子弹
 * 
 *                 3.实现自己被子弹击中也会消失
 *                     对于摧毁坦克进行升级
 * 
 *                 4.
 *                     较难!
 *                     实现坦克不覆盖运动,
 *                     1.首先改判断在坦克类中实现
 *                     2.需要用到一个方法获取到生成的坦克类
 *                     3.对于地方其中一辆坦克的选择,都要循环与其他所有坦克进行比对
 *                         并且要事先判断是否为我方坦克
 *                     4.**对于点位的判断要判断两个点,才能够保证不会产生碰撞
 * 
 *                 5.实现选择关卡的功能
 *                     思路:
 *                         1.可以建立一个选择关卡的面板
 *                         2.暂时先实现不同的关卡敌人坦克的数量不同
 *                         3.实现闪烁功能,使用多线程的方法,注意线程的关闭
 *                         4.对于选项添加事件属性,添加事件
 * 
 *                 5.画出我方坦克击中了多少辆地方坦克
 *                     1.对于总体界面进行修改
 *                     2.显示敌人坦克的数量
 *                     扩展:
 *                         1.建立帮助文档
 *                     3.扩展:我方坦克的生命值,当生命值为0的时候游戏结束
 *                     4.记录我方击中了多少地方坦克
 *                         使用文件操作完成
 * 
 *                 6.实现重新开始的功能
 * 
 *                 7.实现存盘退出的功能
 *                     思路:
 *                         选在主界面增加两个按钮
 *                         1.记录所有坦克的坐标
 *                         
 *                 8.实现暂停的功能
 *                     思路:
 *                         暂停功能可以通过一个布尔值进行判断,当按下某个按钮的时候就要进行布尔值的改变
 *                         需要暂停的对象
 *                             将多线程子弹的速度前进功能暂停
 *                             敌人坦克无法转向和前进
 *                             我方坦克无法转向和前进
 * 
 *                 9.实现播放音乐的功能
 *                     自学 - 未实现
 * 
 *                 
 *                     
 * 
 * */

版本迭代和介绍:

介绍:

代码比较多,我会抽几处理解起来比较难以理解的地方说明一下,其他的代码需要看细节。如果有不懂的欢迎在issue提出,个人只要有空一定给予答复。

第一个版本

版本概述:画出坦克(version1)

我们的第一步是画出我们的坦克,画出我方坦克的方法还是非常简单的。

 *         思路:
 *             1.首先坦克想象由五个部件组成两个矩形,一个长方形或者正方形,一个圆
 *                 一条直线
 * 
 *             2.画坦克的时候需要使用到画笔工具
 *                 必须在构造函数初始化使用画笔工具
 * 
 *             3.在设置方向以及画出不同方向的坦克

拥有坦克的第一步是画出坦克

画出坦克的核心代码如下:

/**
     * 画坦克需要提取封装
     * 1.画出来之前先确定颜色,是敌人坦克还是我方坦克
     * 2.参数为坐标做,画笔(重要),以及坦克类型和方向
     */
    private void paintMyTank(int x, int y, Graphics g, int direct, String type) {
        //画之前先确定坦克的颜色
        switch (type) {
            case "mytank": {
                g.setColor(Color.red);
                break;
            }
            case "enemytank": {
                g.setColor(Color.cyan);
                break;
            }
        }
        //向上
        if (direct == 0) {//先画出我的坦克
            //画出左边的矩形,先设置颜色
            g.fill3DRect(x, x, 5, 30, false);

            //画出中间的长方形
            g.fill3DRect(x + 5, x + 5, 10, 20, false);

            //画出中间圆圈,使用填充椭圆
            g.fillOval(x + 6, x + 9, 7, 7);

            //画出一条直线
            g.drawLine(x + 10, x, x + 10, x + 15);

            //画出另一边矩形
            g.fill3DRect(x + 15, x, 5, 30, false);
        }

    }

如果知道数字的意思,直接将数字修改大小就可以知道效果了

第二个版本

版本概述:画出我方坦克不同形状,敌方坦克(version2),我方坦克可以进行行动

在上个版本当中,我们发现我们的坦克只有一个朝向,在这个版本中,增加了坦克的不同朝向。同时增加了敌人的坦克类。

由于敌人有很多个,所以用了一个集合来维护和设置。同时加入了坐标系统,可以实现不同的坦克挪到不同的位置。

这个版本的关键代码,不是在画坦克的上面,而是在于加入了键盘的监听事件:

// version2.DrawTank.java 更多细节请查看
public class DrawTank extends JPanel implements KeyListener { 
    // 省略一大坨代码
    
    
/**
     * 使用wsad进行控制
     * 也可以改为上下左右键
     *
     * @param e
     */
    @Override
    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_W) {
            this.mytank.setDirect(0);
            this.mytank.move_up();
        } else if (e.getKeyCode() == KeyEvent.VK_D) {
            this.mytank.setDirect(1);
            this.mytank.move_right();
        } else if (e.getKeyCode() == KeyEvent.VK_S) {
            this.mytank.setDirect(2);
            this.mytank.move_down();
        } else if (e.getKeyCode() == KeyEvent.VK_A) {
            //改变方向
            this.mytank.setDirect(3);
            this.mytank.move_left();
        }
    }
}
实现KeyListener接口并且监听对应的方法。

JAVA的GUI有一个事件监听驱动模型,意思就是说我们实现对应的驱动接口,并且覆盖对应的方法,在代码运行并且触发相关事件的适合,模型就可以触发我们实现定义好的代码,这里很明显就是设计模式,有兴趣可以去了解一下

第三个版本

从这个版本就开始变得稍微复杂一点了,用了多线程的内容,因为要让我们的坦克和敌人的坦克“动”起来,其实让坦克移动和我方坦克移动的道理都是一样的:高速的擦写和描绘。和我们的鼠标以及计算机显示画面的本质都是一样的。

这个版本中,比较核心的内容是如何发射子弹和让子弹消失:

public class Bullet implements Runnable {

    /**
     * 定义子弹的xy坐标
     */
    private int x, y;
    /**
     * 子弹的颜色
     */
    int color;
    /**
     * 子弹的方向
     */
    int direct;
    /**
     * 子弹移动速度
     */
    int screed;
    /**
     * 判断是否越界
     */
    boolean isOut = false;

    /**
     * 越界范围
     */
    int outx = 400;
    /**
     * 越界范围
     */
    int outy = 300;

    public Bullet(int x, int y, int direct) {
        this.x = x;
        this.y = y;
        this.direct = direct;
        this.screed = 1;
    }
    
    // 省略get/set

    @Override
    public void run() {
        //坦克一旦建立就要运动
        //因为移动的太快,需要减慢速度
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                //
                e.printStackTrace();
            }

            switch (this.direct) {
                case 0:
                    y -= screed;
                    break;
                case 1:
                    x += screed;
                    break;
                case 2:
                    y += screed;
                    break;
                case 3:
                    x -= screed;
                    break;
            }
            System.out.println(x + "..." + y);
            //碰到边缘消失
            if (x < 0 || x > outx || y < 0 || y > outy) {
                isOut = true;
                break;
            }
        }
        // 子弹什么时候消亡?
    }

    /**
     * 判断是否越界
     */
    public void outLine() {
    }
}
  1. 在坦克的内部维护一个变量isOut,判定有没有越界
  2. 如果出现了越界,则flag进行设置

接着,在绘画的方法里面,判定有没有越界:

    /**
     * 绘画方法
     * @param g
     */
@Override
public void paint(Graphics g) {
    super.paint(g);

    //画出背景色
    g.fill3DRect(0, 0, 600, 400, false);
    //画出自己的坦克
    paintMyTank(mytank.getX(), mytank.getY(), g, mytank.getDirect(), mytank.getColor());
    //画出敌人的坦克
    paintEnemyTank(g);

    //画出子弹并且确定没有越界
    if (mytank.but != null && !mytank.but.isOut) {
        g.fill3DRect(mytank.but.getX(), mytank.but.getY(), 5, 5, false);
    }
}

第四个版本:

从这一个版本开始,一个游戏的简单雏形已经有了,这一个版本实现了让敌人移动的同时发射子弹的功能,同时我方的坦克射击敌人的时候,可以让敌人消失

怎么样让敌人可以边移动边发射子弹:

我们需要在敌人的多线程run代码里面,然敌人进行间歇性的走动:

 @Override
    //我们发现坦克在原地抽搐,我们要实现坦克的平稳运行
    //实现坦克运动不会越界
    public void run() {
  
        do {
            switch (this.direct) {
                case 0:
                    for (int i = 0; i < 30; i++) {
                        if (y > 0)
                            y -= sreed;
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {

                            e.printStackTrace();
                        }
                    }
                    break;
                case 1:
                    for (int i = 0; i < 30; i++) {
                        if (x < 500)
                            x += sreed;
                        try {
                            // 短暂的停顿
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    break;
                case 2:
                    for (int i = 0; i < 30; i++) {
                        if (y < 400)
                            y += sreed;
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {

                            e.printStackTrace();
                        }
                    }
                    break;
                case 3:
                    for (int i = 0; i < 30; i++) {
                        if (x > 0)
                            x -= sreed;
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {

                            e.printStackTrace();
                        }
                    }
                    break;

            }
            //不同的方向移动的方向不同
            this.direct = (int) (Math.random() * 4);

        } while (this.isLive);
    }

至于生成子弹,需要定时去轮询所有的坦克,检查坦克中组合的子弹集合是否存在子弹,如果小于一定的数量,需要生成对应的子弹对象同时加入到敌人的坦克当中。由于子弹创建就会开始执行线程进行

@Override
    public void run() {
        //限定一段时间重新绘制
        while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //判断是否击中
            for (int x = 0; x < mytank.vecs.size(); x++) {
                //每一颗子弹和每一个坦克匹配
                //取出一颗子弹之前判断是否有子弹
                buts = mytank.vecs.get(x);

                //判断子弹是否有效
                if (buts.isOut()) {
                    continue;
                }
                //取出每一个坦克与它判断
                for (int y = 0; y < vec.size(); y++) {
                    //判断敌方坦克是否死亡
                    if (vec.get(y).isLive) {
                        en = vec.get(y);
                        //记性判断是否击中操作
                        hitTank(en, buts);
                    }
                }

            }

            //如果子弹数小于一定数目
            for (int x = 0; x < vec.size(); x++) {
                EnemyTank et = vec.get(x);
                //遍历每一辆坦克的子弹集合
                if (!et.isLive()) {
                    continue;
                }
                if (et.vecs.size() < 1) {
                    //对于不同的坦克方向生成子弹的方向也不同
                    Bullet enybut = null;
                    switch (et.getDirect()) {
                        case 0:
                            enybut = new Bullet(et.getX() + 10, et.getY(), 0);
                            //将创建的子弹加入到集合当中
                            et.vecs.addElement(enybut);
                            break;
                        case 1:
                            enybut = new Bullet(et.getX() + 30, et.getY() + 10, 1);
                            et.vecs.addElement(enybut);
                            break;
                        case 2:
                            enybut = new Bullet(et.getX() + 10, et.getY() + 30, 2);
                            et.vecs.addElement(enybut);
                            break;
                        case 3:
                            enybut = new Bullet(et.getX(), et.getY() + 10, 3);
                            et.vecs.addElement(enybut);
                            break;

                    }
                    new Thread(enybut).start();

                }
            }
            //重绘
            this.repaint();
        }
    }

在子弹类当中进行不断的数值改变:

下面的内容表示子弹的类

public class Bullet implements Runnable {
    //隐藏一大段代码:
    
public void run() {
      
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            switch (this.direct) {
                case 0:
                    this.y -= screed;
                    break;
                case 1:
                    this.x += screed;
                    break;
                case 2:
                    this.y += screed;
                    break;
                case 3:
                    this.x -= screed;
                    break;
            }
            //碰到边缘消失
            if (x < 0 || x > outx || y < 0 || y > outy) {
                isOut = true;
                break;
            }
           
        }
    }
}

第五个版本:

在第五个版本当中,我们实现了开始菜单的界面,同时视线菜单的不断显示:

界面会不断的闪烁

接着,敌人增加了子弹可以摧毁我们的方法

接着,我们可以实现爆炸的效果:

由于爆炸的效果不好截图,请看源代码

/**
 * 实现闪烁功能
 * 重构坦克 - 第五版
 * @author zxd
 * @version 1.0
 * @date 2021/1/29 23:54
 */
class SelectIsSallup extends JPanel implements Runnable {
    /**
     * 时间属性
     */
    int times = 0;

    public void paint(Graphics g) {
        super.paint(g);
        g.fillRect(0, 0, 600, 400);
        if (times % 2 == 0) {
            //画出文字
            Font font1 = new Font("华文新魏", Font.BOLD, 20);
            //设置字体的颜色
            g.setColor(Color.yellow);
            g.setFont(font1);
            g.drawString("stage 1", 200, 150);
        }

    }

    @Override
    public void run() {

        while (true) {
            try {
                Thread.sleep(750);
            } catch (InterruptedException e) {

                e.printStackTrace();
            }
            if (times > 500)
                times = 0;
            times++;
            this.repaint();

        }
    }
}

如何让敌人的子弹对我们造成伤害:

/**
     * 建立一个方法,判断是否产生碰撞
     * 是否攻击了其他的坦克
     * @return
     */
    private boolean isTouchOther() {
        // 根据自己的方向进行选择判断
        switch (this.direct) {
            // 坦克向上走的时候
            case 0:
                // 取出所有的坦克对象
                for (int x = 0; x < enevec.size(); x++) {
                    EnemyTank et = enevec.get(x);
                    //如果不是自己的坦克
                    if (et != this) {
                        //如果敌人的坦克朝上或者朝下的时候
                        if (et.direct == 0 || et.direct == 2) {
                            //判断边界
                            //对于第一个点进行判断
                            if (this.x >= et.x && this.x <= et.x + 20
                                    && this.y >= et.y && this.y <= et.y + 30) {
                                return true;
                            }
                            //对于第二个点进行判断
                            if (this.x + 20 >= et.x && this.x + 20 <= et.x + 20
                                    && this.y >= et.y && this.y <= et.y + 30) {
                                return true;
                            }
                        }
                        //如果敌人是朝左边或者右边的时候
                        if (et.direct == 1 || et.direct == 3) {
                            //判断边界
                            //对于第一个点进行判断
                            if (this.x >= et.x && this.x <= et.x + 30
                                    && this.y >= et.y && this.y <= et.y + 20) {
                                return true;
                            }
                            //对于第二个点进行判断
                            if (this.x + 20 >= et.x && this.x + 20 <= et.x + 30
                                    && this.y >= et.y && this.y <= et.y + 20) {
                                return true;
                            }
                        }
                    }

                }
                break;
            //    坦克想右边走的时候
            case 1:
                // 取出所有的坦克对象
                for (int x = 0; x < enevec.size(); x++) {
                    EnemyTank et = enevec.get(x);
                    //如果不是自己的坦克
                    if (et != this) {
                        //如果敌人的坦克朝上或者朝下的时候
                        if (et.direct == 0 || et.direct == 2) {
                            //判断边界
                            //对于第一个点进行判断
                            if (this.x + 30 >= et.x && this.x + 30 <= et.x + 20
                                    && this.y >= et.y && this.y <= et.y + 30) {
                                return true;
                            }
                            //对于第二个点进行判断
                            if (this.x + 30 >= et.x && this.x + 30 <= et.x + 20
                                    && this.y >= et.y && this.y <= et.y + 30) {
                                return true;
                            }
                        }
                        //如果敌人是朝左边或者右边的时候
                        if (et.direct == 1 || et.direct == 3) {
                            //判断边界
                            //对于第一个点进行判断
                            if (this.x + 30 >= et.x && this.x + 30 <= et.x + 30
                                    && this.y + 20 >= et.y && this.y <= et.y + 20) {
                                return true;
                            }
                            //对于第二个点进行判断
                            if (this.x + 30 >= et.x && this.x + 30 <= et.x + 30
                                    && this.y + 20 >= et.y && this.y <= et.y + 20) {
                                return true;
                            }
                        }
                    }

                }

                // 坦克想下的时候
            case 2:
                // 取出所有的坦克对象
                for (int x = 0; x < enevec.size(); x++) {
                    EnemyTank et = enevec.get(x);
                    //如果不是自己的坦克
                    if (et != this) {
                        //如果敌人的坦克朝上或者朝下的时候
                        if (et.direct == 0 || et.direct == 2) {
                            //判断边界
                            //对于第一个点进行判断
                            if (this.x >= et.x && this.x <= et.x + 20
                                    && this.y + 30 >= et.y && this.y + 30 <= et.y + 30) {
                                return true;
                            }
                            //对于第二个点进行判断
                            if (this.x + 20 >= et.x && this.x + 20 <= et.x + 20
                                    && this.y + 30 >= et.y && this.y + 30 <= et.y + 30) {
                                return true;
                            }
                        }
                        //如果敌人是朝左边或者右边的时候
                        if (et.direct == 1 || et.direct == 3) {
                            //判断边界
                            //对于第一个点进行判断
                            if (this.x >= et.x && this.x <= et.x + 30
                                    && this.y + 30 >= et.y && this.y + 30 <= et.y + 20) {
                                return true;
                            }
                            //对于第二个点进行判断
                            if (this.x + 20 >= et.x && this.x + 20 <= et.x + 30
                                    && this.y + 30 >= et.y && this.y + 30 <= et.y + 20) {
                                return true;
                            }
                        }
                    }

                }
                break;

            // 坦克向左移动的时候
            case 3:
                // 取出所有的坦克对象
                for (int x = 0; x < enevec.size(); x++) {
                    EnemyTank et = enevec.get(x);
                    //如果不是自己的坦克
                    if (et != this) {
                        //如果敌人的坦克朝上或者朝下的时候
                        if (et.direct == 0 || et.direct == 2) {
                            //判断边界
                            //对于第一个点进行判断
                            if (this.x >= et.x && this.x <= et.x + 20
                                    && this.y >= et.y && this.y <= et.y + 30) {
                                return true;
                            }
                            //对于第二个点进行判断
                            if (this.x >= et.x && this.x <= et.x + 20
                                    && this.y + 20 >= et.y && this.y + 20 <= et.y + 30) {
                                return true;
                            }
                        }
                        //如果敌人是朝左边或者右边的时候
                        if (et.direct == 1 || et.direct == 3) {
                            //判断边界
                            //对于第一个点进行判断
                            if (this.x >= et.x && this.x <= et.x + 30
                                    && this.y >= et.y && this.y <= et.y + 20) {

                                return true;
                            }
                            //对于第二个点进行判断
                            if (this.x >= et.x && this.x <= et.x + 30
                                    && this.y + 20 >= et.y && this.y + 20 <= et.y + 20) {
                                return true;
                            }
                        }
                    }

                }
        }
        return false;

    }

最终版本:

在最终的版本当中,一个坦克大战的基本游戏算是完成了,当然还有很多需要完成点。

这里主要提示一下暂停这一个功能点:

暂停的主要思想是为坦克加一个状态去控制坦克的所有行为。让暂停的flag为false的时候,线程不在执行,绘画每次都是绘制在同一个位置。这样就造成了“暂停”的假象。

//暂停功能
if(e.getKeyCode()==KeyEvent.VK_P)
{
    if(this.clickcount%2 == 0)
        mytank.setSuspend(false);
    else
        mytank.setSuspend(true);

    //利用循环将坦克类中的子弹速度变成0
    for(int x=0; x<vec.size(); x++)
    {
        en = vec.get(x);
        //敌方坦克移动速度归于0

        //坦克不允许移动
        if(this.clickcount%2 == 0)
            en.setSuspend(false);
        else
            en.setSuspend(true);
        for(int y=0; y<en.vecs.size(); y++)
        {
            //子弹的速度变成0
            if(this.clickcount%2 == 0)
                en.vecs.get(y).setSuspend(false);
            else
                en.vecs.get(y).setSuspend(true);

        }
    }
    this.clickcount++;

}

总结:

这个文档不是最终版本,如果有不懂的欢迎提issue,承诺给予答复但是不会再改动代码了。

查看原文

赞 0 收藏 0 评论 0

lazytimes 发布了文章 · 1月29日

浅谈设计模式 - 策略模式(三)

浅谈设计模式 - 策略模式(三)

前言

这次我们来讲解一下策略模式,策略模式是我们日常开发天天都在用的“模式”,最简单if/else就是策略,而我们用不同的策略(分支)来实现结果的区分。所以策略模式是非常重要的模式,也是理解和应用最为简单的方式(大概)。

这里再次提醒:不要过分拘泥于设计模式的类和形式,只要记住一点:将变与不变抽离的过程就是设计模式

<!-- more -->

什么是策略模式?

策略模式按照最简单的理解就是对if/else的解耦,也是他最常用的场景,最典型的应用场景就是购物的时候,选择用优惠券,还是满2件送一件,或者凑够多少金额满减等等,按照一般的写法,我们经常会写出大量的if/else,在代码量较少的时候,这种写的方式既简单又方便,但是一旦代码复杂,复杂的if/else会让代码越来越屎,策略模式也是为了解决此问题而产生的。

策略模式是一种行为型模式,他将一类相似的行为解耦,并且将策略封装到具体的策略实现类。

策略模式结构图:

下面用一张烂大街的图描绘一下策略模式的结构,切记落实设计模式到代码之后,你会对这个图的印象更加深刻。

下面给出一张工厂模式的图,会发现他们长得非常像:

工厂模式可以看这一篇:工厂模式

什么情况下使用策略模式?

  1. 当代码充斥大量if/else并且他们只是行为不同的时候,建议使用
  2. 将复杂的策略内容封装到单独的类情况下,比如我们的策略内容需要进行非常复杂的计算

策略模式的特点:

  1. 将相似的行为进行封包,客户端指定策略已达到不同行为的切换
  2. 将复杂的业务实现逻辑代码封装到单独的策略,可以通过context组合使用策略

工厂模式和策略模式的异同:

相同点:

  1. 策略的"执行对象"和工厂生产的“抽象对象”,他们都具有相似的行为
  2. 都是为了抽离过程和结果实现本身。

不同点:

  1. 工厂模式是为了创建对象,而策略是为了解决复杂的if/else嵌套
  2. 工厂模式只需要传递工厂需要的参数,而策略模式则需要具体的实现类支撑。
  3. 工厂模式是创建型设计模式,而策略模式是行为型模式。前者专注于对象的创建过程,后者专注于对象的具体行为
如果上面不够清晰,那么下面我给出一个具体一些的案例来说说他们的区别:

​ 我们都知道低价手机的生产基本都是找代工厂,而代工厂可能不止生产一个品牌的手机,他可能承接多个品牌的手机生产,经销商让工厂生产指定的手机,而工厂负责手机的“创建”,这一模式就是典型的 工厂模式,而工厂根据不同的手机品牌,投入不同的生产材料和生成力,这个抉择的过程就是策略模式

实际案例:

光有理论是不够的,我们来实际操作一下策略模式。这次的场景模拟个人觉得还挺有意思的,看下具体的内容:

场景模拟:

​ 一些交易的系统,在遇到特殊情况的时候,需要进行网络监控或者管理,有时候需要根据某种条件下触发监控或者报警,比如网关接受一笔交易,需要根据交易的校验情况,在不同的校验代码段进行钉钉机器人报警,下面给出几种情况:

  • 查不出必要数据的时候,给出对应的告警。提醒运营人员排查线上环境
  • 当数据量到达指定的限制量的时候,给出风险告警。
  • 当出现黑名单人员进行交易拦截的时候,进行日志记录,不进行警告

...

不使用设计模式:

兵来将挡,谁来土掩,发现那里需要告警,就往对应的地方添加代码,这样子做完成任务是很快,当然代码烂起来也是很快的。下面看一下具体的实现:

看到这里下面的代码有可能会觉得,不是说策略模式是用来解决if/else的么,你这看上去也没有什么if/else呀,这时候就是仁者见仁智者见智了,我还是保持一个观点:设计模式用来解决实际问题,而不是拘泥于套版。
/**
 * 策略模式:
 * 不使用设计模式实现告警
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/26 23:41
 */
public class Main {

    /**
    * 不使用模式
    */
    public static void main(String[] args) {
        System.out.println("接受交易");
        service1();
        service2();
        service3();
        System.out.println("完成交易");
    }

    /**
     * 模拟触发了业务场景1
     * 出现机房断电或者查不出必要数据的时候,给出对应的告警。提醒运营人员排查线上环境
     */
    private static void service1() {
        // 为了模拟异常情况,我们用 1/0 触发一个异常
        try {
            // 程序到了这一步算不下去了
            int result = 1/0;
            System.out.println("具体的业务");
        } catch (Exception e) {
            System.err.println("警告,服务器出现异常");
            System.out.println("开始执行报警");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            System.err.println("执行报警完成");
            throw e;
        }

    }

    /**
     * 模拟触发了业务场景2
     * 当数据量到达指定的限制量的时候,给出风险告警。
     */
    private static void service2() {
        int limit = 1000;
        int count = 2000;
        if(count > limit){
            System.out.println("开始执行报警");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            //.....
            // logger.info("警告,当前数据请求量达到限制值")
            System.err.println("执行报警完成");
        }

    }

    /**
     * 模拟触发了业务场景3
     * 当出现黑名单人员进行交易拦截的时候,进行日志记录,不进行警告
     */
    private static void service3() {
        boolean flag = true;
        if(flag){
            // 触犯黑名单:
            // logger.info("警告,当前请求");
            // 提前退出,结束交易
            return;
        }
        System.out.println("正常完成下面的步骤");
    }
}

如上面的所示,单就这个类就以肉眼可见的速度在膨胀代码,特别是如果我们在告警的代码需要大量的操作的时候,我们会把告警的业务和原有的业务逻辑不断纠缠,最后代码就变成了 面向实现编程,下一个接手的人看到这样的代码,也会接着往后面累加,一个臃肿的结构就此诞生了。

上面的代码存在如下的问题:

  1. 当我们需要新增一处监控的时候,需要在对应的代码块增加监控和报警的逻辑
  2. 所有的改动都在一处,如果代码内容复杂会造成业务逻辑混淆
  3. 当告警的业务日趋复杂,告警的代码将变得难以维护

使用工厂模式:

没有学习策略模式的时候,我们尝试使用工厂模式尝试改写一下这一段代码,同时在使用工厂模式之前,我们回顾一下工厂模式的图,下面画图:

下面是使用工厂模式设计出来的关系类

+ BlackListStrategy.java 黑名单策略
+ NoResultStrategy.java 无返回值
+ QuantityStrategy.java 数量监控策略
+ 测试类
+ StrategyFactory.java 策略工厂,负责生产需要的策略

策略工厂,用于生产策略:

/**
 * @author zhaoxudong
 * @version v1.0.0
 * @Package : com.headfirst.strategy.factory
 * @Description : 策略工厂,根据参数生产对应的策略条件
 * @Create on : 2021/1/27 13:24
 **/
public class StrategyFactory {

    /**
     * 创建策略
     * @param service
     * @return
     */
    public CaveatStrategy createStrategy(String service){
        // 数量监控
        if(Objects.equals(service, "quantity")){
            return new QuantityStrategy();
        }else if(Objects.equals(service, "noresult")){
            // 没有返回值
            return new NoResultStrategy();
        }else if(Objects.equals(service, "blacklist")){
            // 黑名单
            return new BlackListStrategy();
        }
        return null;
    }
}

黑名单策略类:

/**
 * 当出现黑名单人员进行交易拦截的时候,进行日志记录,不进行警告
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/28 21:52
 */
public class BlackListStrategy implements CaveatStrategy {

    @Override
    public void warning(Map<String, Object> params) {
        boolean flag = Boolean.parseBoolean(params.get("flag").toString());
        if (flag) {
            System.err.println("触犯黑名单列表,但不警告");
        }
    }
}

数量监控策略类:

/**
 * @author zhaoxudong
 * @version v1.0.0
 * @Package : com.headfirst.strategy.use
 * @Description : 数量监控
 * @Create on : 2021/1/27 13:27
 **/
public class QuantityStrategy implements CaveatStrategy {
    @Override
    public void warning(Map<String, Object> params) {
        int limit = Integer.parseInt(params.get("limit").toString());
        int count = Integer.parseInt(params.get("count").toString());
        if(count > limit){
            System.err.println("警告,当前数据内容无法获取返回值");
        }
    }
}

单元测试:

/**
 * 单元测试
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/28 22:01
 */
public class Main {

    /**
     *
     * @param args
     */
    public static void main(String[] args) {
        // 模拟交易流转参数对象
        Map<String, Object> objectObjectHashMap = new HashMap<>();
        StrategyFactory strategyFactory = new StrategyFactory();
        CaveatStrategy strategy = strategyFactory.createStrategy("quantity");

        // 表示除数和被除数
        objectObjectHashMap.put("limit", "1000");
        objectObjectHashMap.put("count", "2000");
        strategy.warning(objectObjectHashMap);

        strategy = strategyFactory.createStrategy("noresult");
        objectObjectHashMap.put("divisor", "1");
        objectObjectHashMap.put("dividend", "0");
        strategy.warning(objectObjectHashMap);

        strategy = strategyFactory.createStrategy("blacklist");
        objectObjectHashMap.put("flag", true);
        strategy.warning(objectObjectHashMap);
    }/*结果如下:
    警告,当前数据内容无法获取返回值
    触犯黑名单列表,但不警告
    */
}

上面的代码存在如下的问题:

  1. 策略工厂虽然解决了策略的生产问题,但是需要自己指定策略,而且每次更换策略内容会导致工厂的代码也需要随之改动
  2. 维护和扩展都需要依赖工厂,我们每多一个策略都需要更换工厂的内容
  3. 当告警的业务日趋复杂,工厂的代码将会越发的臃肿

使用策略模式:

在具体的实现之前,我们根据上面提到的图,照着模样画葫芦画一个图出来:

策略的实现类在上上面的工厂模式,这里给出上下文以及使用的具体方法:

+ StrategyContext 策略上下文
+ 策略模式的单元测试

策略类的上下文:

/**
 * 策略的上下文
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/28 22:52
 */
public class StrategyContext {

    private CaveatStrategy strategy;

    public StrategyContext(CaveatStrategy strategy) {
        this.strategy = strategy;
    }

    public void doStrategy(Map<String, Object> params){
        strategy.warning(params);
    }
}

策略模式的单元测试:

/**
 * 单元测试
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/28 22:53
 */
public class Main {

    /**
     * 使用策略模式
     * @param args
     */
    public static void main(String[] args) {
        Map<String, Object> objectObjectHashMap = new HashMap<>();
        objectObjectHashMap.put("limit", "1000");
        objectObjectHashMap.put("count", "2000");
        objectObjectHashMap.put("divisor", "1");
        objectObjectHashMap.put("dividend", "0");
        objectObjectHashMap.put("flag", true);

        CaveatStrategy blackListStrategy = new BlackListStrategy();
        CaveatStrategy noResultStrategy = new NoResultStrategy();
        CaveatStrategy quantityStrategy = new QuantityStrategy();
        // 三种策略独立
        StrategyContext strategyContext = new StrategyContext(blackListStrategy);
        strategyContext.doStrategy(objectObjectHashMap);
        StrategyContext strategyContext2 = new StrategyContext(noResultStrategy);
        strategyContext2.doStrategy(objectObjectHashMap);
        StrategyContext strategyContext3 = new StrategyContext(quantityStrategy);
        strategyContext3.doStrategy(objectObjectHashMap);

        // 简化一下:
        StrategyContext strategyContext4 = new StrategyContext(blackListStrategy);
        strategyContext4.doStrategy(objectObjectHashMap);
        strategyContext4 = new StrategyContext(noResultStrategy);
        strategyContext4.doStrategy(objectObjectHashMap);
        strategyContext4 = new StrategyContext(quantityStrategy);
        strategyContext4.doStrategy(objectObjectHashMap);
    }/*
    触犯黑名单列表,但不警告
    警告,当前数据内容无法获取返回值
    触犯黑名单列表,但不警告
    警告,当前数据内容无法获取返回值
    */
}

从上面的内容可以看出,我们只需要把策略传给上下文,上下文会根据传入的策略自动匹配对应的策略执行报警。

但是我们也发现了一些问题:

  1. 代码存在new策略类,这又回到以前不使用工厂的时候情况了
  2. 如果我们用策略组合,虽然少了很多的if/else,但是建立策略的细节依旧在客户端。

答案已经很明显了,策略和工厂双方各有利弊,果断用策略和工厂模式组合起来进行重写。

简单工厂和策略模式结合:

工厂和策略结合之后,这里我们结合context上下文和工厂看一下效果:

/**
 * 改写策略的上下文
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/28 22:52
 */
public class StrategyContext {

    private CaveatStrategyFactory caveatStrategyFactory = new CaveatStrategyFactory();

    public void doStrategy(String service, Map<String, Object> params){
        caveatStrategyFactory.createStrategy(service).warning(params);
    }
}

这个工厂和上面工厂模式的工厂没有区别,个人为了区分换了个名字:

/**
 * 警告策略的生成厂
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/26 23:28
 */
public class CaveatStrategyFactory {

    /**
     * 创建策略
     * @param service 策略
     */
    public CaveatStrategy createStrategy(String service){
        // 数量监控
        if(Objects.equals(service, "quantity")){
            return new QuantityStrategy();
        }else if(Objects.equals(service, "noresult")){
            // 没有返回值
            return new NoResultStrategy();
        }else if(Objects.equals(service, "blacklist")){
            // 黑名单
            return new BlackListStrategy();
        }
        return null;
    }
}

上面的代码有了如下的好处:

  1. 客户端不在需要手动new对象,由工厂来完成
  2. 指定策略只需要的参数和指定策略的名称,上下文“自动”帮我们完成结果
  3. 将策略的生成过程和策略的执行过程更进一步的解耦

到此,这样的代码可维护性和阅读性能大大提高,后续如果还需要扩展策略直接实现抽象接口同时工厂新增判断,然后客户端指定新的策略服务名称即可让整个流程自动化。

顺带一提的是,策略和简单工厂的结合是受到了 《大话设计模式》的启发,大致的思路也做了参考,顿时感觉这样才算是有点学以致用的感觉,撸完代码的感觉还是非常快乐。

更好的“策略”:

上面的代码还不是最优的,在spring当中,我们的策略一般会作为一个bean使用,而不需要每次都使用new去构建我们的策略,因为我们的策略基本都是单例的。下面给出一些建议的写法:

这里我们按照单独的策略类为例,他应该如下:

被spring管理的策略bean:

  • NoResultStrategyImpl 无返回值的策略实现bean
/**
 * 无结果的业务实现
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/28 23:08
 */
//@Service
public class NoResultStrategyImpl implements CaveatStrategy {


    // 一般此处会组合一些mapper或者引入一些日志记录logger

    @Override
    public void warning(Map<String, Object> params) {
        // loggger.info("记录需要的信息");
        int divisor = Integer.parseInt(params.get("divisor").toString());
        int dividend = Integer.parseInt(params.get("dividend").toString());
        try {
            int result = dividend / divisor;
        } catch (Exception e) {
            System.err.println("警告,服务器出现异常");
            System.out.println("开始执行报警");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            // logger.info("日志记录");
            System.err.println("执行报警完成");
            throw e;
        }
        // 执行一些策略等


    }
}
  • SpringCaveaStrategy spring工具类,使用工具类获取注解对应的bean,这样可以实现从一个接口获取他所管理的多个子类(建议自定义service的Bean名称防止冲突)
/**
 * 使用Spring 工具获取指定的Bean
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/28 23:11
 */
//@Component
public class SpringCaveaStrategy {

    //使用spring编写的工具类进行bean的获取
    public CaveatStrategy getBean(String service){
        // return SpringUtils.getBean(service);
        // 不建议直接调用,做一下null指针判断
        return SpringUtils.getBean(service).warning(params);
    }
}

在最后我们结合spring实现 单例之后,我们成功将 单例 + 策略 + 简单工厂进行了整合,这样的代码写起来才爽呀,然而现实生活中我们大多数在一个已经建立好的结构上做优化,这时候就需要更多思考了......

总结:

本文在策略模式上做了进一步的深入思考,我对比了一下简单工厂和策略工厂,这两个模式可以说长得还是非常像的,仅仅靠这些简单的案例是不够的,还需要更多的灵活运用。

个人学习的思路一致按照 模仿 -> 熟练 ->创新,同时按照自己的理解去设计场景,这样给自己的学习是很大的,能看到自己知识的模糊点。

如果这篇文章对你有帮助或者有任何建议意见欢迎讨论。后续会更新更多关于设计模式的内容。

查看原文

赞 0 收藏 0 评论 0

lazytimes 发布了文章 · 1月26日

浅谈设计模式 - 简单工厂模式(二)

前言:

​ 对于学习设计模式,我推荐:《HeadFirst设计模式》《大话设计模式》。另外设计模式推崇学以致用。看到任何知识之前,先想想我能学到什么,带着问题去看待问题,将会使得学习事半功倍,否则就是事倍功半。

​ 不要过分拘泥于设计模式的类和形式,只要记住一点:将变与不变抽离的过程就是设计模式

为什么设计模式学了就忘?

  • 不敢尝试(当然也不要过度自信,看到代码就想用设计模式)
  • 过于关注设计模式的结构,忘记了业务本身。

    • 很多时候我们拘泥于形式和设计,纠结于用什么设计模式,其实设计模式本身就是继承,封装,多态的三者结合,很多时候只要可以解决问题,就不需要用过多的技巧
  • 学习之前神志不清

    • 大致就是邯郸学步,看到别人学设计模式,自己也跑去学设计模式

很多人学了设计模式之后隔了一段时间之后,发现自己不使用,忘得一干二净(我也是)。所以希望这些设计模式更多的是结合一些比较实际一点的需求(尽量),毕竟设计模式学了就是拿来用的,如果不用不如不要学,去看点动漫电视剧啥的放松一下。

什么是简单工厂模式?

现实理解:

简单工厂从字面意思来看,就如同我们平常的工厂一般,我们想要重复的生产某样物品,就需要建设工厂不断生产。我们需要给工厂下达指令,比如生产一批“苹果”,生成一批“香蕉”。我们只需要只会工厂生产,而不需要去理会内部的细节。

工厂模式:

简单工厂模式,是一种创建型设计模式,定义简单工厂,负责为具体操作对象生成需要的操作类,把创建对象和使用对象进行分开,使用对象方只需要传入调用简单工厂的工厂方法进行创建对象。

简单工厂的特点:

  1. 返回抽象的接口或者父类,由工厂管理子类创建过程
  2. 让创建过程变成一个黑盒
  3. 封闭创建过程,客户端只需要关注结果。

工厂模式优缺点:

优点:

  1. 使用创建工厂的方法,我们实现了获取具体对象和生产对象的解耦,由生产对象的工厂通过我们传入的参数生产对应的对象,调用方只需要传递需要生产的对象来实现具体的效果。
  2. 解耦了创建和被创建的过程。
  3. 根据不同的逻辑判断生成不同的具体对象。

缺点:

  1. 每增加一个工厂对象具体的实现类,就需要增加if/else不利于维护
  2. 大量的子类会造成工厂类的迅速膨胀和臃肿
  3. 简单工厂的方法一般处理简单的业务逻辑,如果创建逻辑复杂不建议使用。

实际案例:

下面的案例是个人理解,可能存在偏差,不同人理解有差异。欢迎给出建议。

场景模拟:

​ 我们以经典的任天堂游戏坦克大战为例,在进入游戏的关卡的时候,会出现我方的坦克和敌人的坦克,我方坦克和地方坦克不仅形状不同,而且很脆,但是敌人的坦克根据颜色需要打好几枪才会毁灭,那么如果用代码来模拟是什么样的呢?

不使用设计模式:

根据场景,我设计了如下的图表

​ 按照正常方式,我们的定义了一个坦克的父类,接着我们需要定义三个子类来继承父类坦克,以实现自己的扩展。当我们需要创建坦克的时候,我们需要纠结所有的细节,比如到底是创建我方坦克还是敌人坦克,我方的坦克位置,敌人的坦克位置,我方的血量,敌人的血量,等等,从创建坦克到销毁坦克的所有过程,都由我们进行参与。

+ 坦克抽象类 Tank.java
+ 老鼠坦克 MouseTank.java
+ 我方坦克 MyTank.java
+ 巨型坦克 BigTank.java
+ 测试类 Main.java

具体的代码实现如下:

  • 坦克的抽象类:
/**
 * 坦克的抽象类,定义坦克的行为
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/25 0:14
 */
public abstract class Tank {


    /**
     * 坦克hp
     */
    protected int hp;

    /**
     * 坦克子弹
     */
    protected List<Object> bullet;

    /**
     * 移动的方法s
     */
    abstract void move();

    /**
     * 攻击
     */
    abstract void attack();

    /**
     * 停止
     */
    abstract void stop();
}
  • 老鼠坦克
/**
 * 老鼠坦克
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/25 22:02
 */
public class MouseTank extends Tank implements Runnable {

    public void display() {
        System.err.println("长得尖尖的,很像老鼠");
    }

    public MouseTank() {
        // 坦克假设只有一条命
        hp = 1;
        new Thread(this).start();
        bullet = new ArrayList<>();
        // 初始化添加六发子弹
        bullet.add(new Object());
        bullet.add(new Object());
        bullet.add(new Object());
        bullet.add(new Object());
        bullet.add(new Object());
    }

    @Override
    void move() {
        System.err.println("老鼠坦克移动");
    }

    @Override
    void attack() {
        System.err.println("老鼠坦克开枪");
        // ..弹出子弹
        if (bullet.size() <= 0) {
            System.err.println("老鼠坦克没有子弹了");
            return;
        }
        // 老鼠坦克一次性开两枪
        bullet.remove(bullet.get(bullet.size() - 1));
    }

    @Override
    void stop() {
        System.err.println("停止");
    }

    @Override
    public void run() {
        while (true) {
            // 一旦创建就开始移动
            move();
            // 漫无目的开枪
            attack();
            attack();
            // 做完一轮操作歇一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 随机停止
            if (new Random(100).nextInt() % 2 == 0) {
                stop();
            }
        }
    }
}
  • 巨型坦克
/**
 * 巨型坦克
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/25 22:14
 */
public class BigTank extends Tank implements Runnable{

    public void display() {
        System.err.println("巨型坦克");
    }

    public BigTank() {
        // 带颜色的坦克有很多条命
        hp = 5;
        new Thread(this).start();
        bullet = new ArrayList<>();
        // 初始化添加三发子弹
        bullet.add(new Object());
        bullet.add(new Object());
        bullet.add(new Object());
    }

    @Override
    void move() {
        System.err.println("巨型坦克移动");
    }

    @Override
    void attack() {
        System.err.println("巨型坦克开枪");
        // ..弹出子弹
        if (bullet.size() <= 0) {
            System.err.println("巨型坦克没有子弹了");
            return;
        }
        // 老鼠坦克一次性开两枪
        bullet.remove(bullet.get(bullet.size() - 1));
    }

    @Override
    void stop() {
        System.err.println("巨型坦克停止");
    }

    @Override
    public void run() {
        while (true) {
            // 一旦创建就开始移动
            move();
            // 漫无目的开枪
            attack();
            // 做完一轮操作歇两秒,
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 随机停止,活动没有老鼠坦克频繁
            if (new Random(1000).nextInt() % 2 == 0) {
                stop();
            }
        }
    }
}
  • 我方坦克

/**
 * 我方坦克
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/25 21:58
 */
public class MyTank extends Tank{

    public MyTank() {
        // 我方坦克假设只有一条命
        hp = 1;
        bullet = new ArrayList<>();
        // 初始化添加三发子弹
        bullet.add(new Object());
        bullet.add(new Object());
        bullet.add(new Object());
    }

    @Override
    void move() {
        System.err.println("移动");
    }

    @Override
    void attack() {
        System.err.println("攻击地方坦克");
        // ..弹出子弹
        if(bullet.size() == 0){
            System.err.println("没有子弹了");
            return;
        }
        bullet.remove(bullet.get(bullet.size() -1));
    }

    @Override
    void stop() {
        System.err.println("停止");
    }
}
  • 测试类:
建议使用单元测试,这里图方便没有用
/**
     * 这种频繁的new,让我们逐渐变成面向过程编程。。。。
     * @param args
     */
    public static void main(String[] args) {
        // 虽然我们可以自己生产坦克,但是我们每次都需要自己手动去生产对应的坦克。这种频繁的new 操作
        Tank bigTank1 = new BigTank();
        Tank bigTank2 = new BigTank();
        Tank bigTank3 = new BigTank();
        Tank bigTank4 = new BigTank();
        // 有多少个对象,就有多少个new
        Tank mouseTank1 = new MouseTank();
        Tank mouseTank2 = new MouseTank();
        Tank mouseTank3 = new MouseTank();
        Tank mouseTank4 = new MouseTank();

        // 我方坦克,需要自己操作
        Tank myTank1 = new MyTank();
        Tank myTank2 = new MyTank();

    }/*//运行结果:
        停止
        老鼠坦克移动
        老鼠坦克开枪
        老鼠坦克开枪
        停止
        老鼠坦克移动
        老鼠坦克开枪
        巨型坦克移动
        巨型坦克开枪
        老鼠坦克开枪
        老鼠坦克没有子弹了
        停止
        老鼠坦克移动
        老鼠坦克开枪
        老鼠坦克没有子弹了
        老鼠坦克开枪
        老鼠坦克没有子弹了
        巨型坦克移动
        巨型坦克开枪
    */

上面的代码有什么问题:

咋看一下好像没啥问题呀,我们既有定义抽象的父类,同时又定义了子类去继承,在需要的时候我们直接new就是了。

其实问题就出在new这一步,可以说我们写烂代码的第一步就是new。因为我们掉进了“细节”的陷阱,下面我们分析一下我们的代码有什么问题:

  1. 我要加一个坦克,虽然可以继承,但是如果要加入到战场,需要我们记住新坦克,并且new出来
  2. 我想要老鼠坦克,却不小心new了一个普通地方坦克,当代码较少的时候可能没啥问题,但是如果代码多了,我们要花大量时间查找
  3. 我们的测试类掌控了一切,他的活太重了,不仅需要new,还需要new之后的所有操作。

用简单工厂模式改进:

既然知道了有什么问题,那么我们可以加入一个简单工厂类来管理坦克的创建过程

增加工厂类 TankFactory.java

用工厂来管理具体的坦克创建过程:

/**
 * 坦克工厂,专门负责生产坦克
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/25 22:27
 */
public class TankFactory {

    /**
     * 创建坦克
     * @return
     */
    public Tank createTank(String check){
        Tank tank = null;
        if(Objects.equals(check, "my")){
            tank = new MyTank();
        }else if(Objects.equals(check, "mouse")){
            tank = new MouseTank();
        }else if (Objects.equals(check, "big")){
            tank = new BigTank();
        }else {
            throw new UnsupportedOperationException("当前坦克不支持生产");
        }
        return tank;
    }
}

我们重写单元测试:

/**
 * 单元测试
 *
 * @author zxd
 * @version 1.0
 * @date 2021/1/25 22:15
 */
public class Main {

    /**
     * 我们将生产坦克的过程全部交给了工厂来处理
     * 可能还是奇怪,这和刚才没有什么区别呀?
     * 我们来看下区别:
     * 1. 创建的过程没有了,虽然是一个简单的new,但是new的过程交给了工厂
     * 2. 我们后续如果要在坦克加入别的东西,只需要去改工厂类和具体的实现类,不需要该此处代码
     * 3. 如果不支持的操作,工厂还可以通知我们这样做不对
     * @param args
     */
    public static void main(String[] args) {

        TankFactory tankFactory = new TankFactory();

        Tank my = tankFactory.createTank("my");
        Tank mouse = tankFactory.createTank("mouse");
        Tank big = tankFactory.createTank("big");
        // 我要一个没有的设计过的坦克
        Tank mybig = tankFactory.createTank("mybig");


    }/*//
    运行结果:
    Exception in thread "main" 老鼠坦克移动
    巨型坦克移动
    老鼠坦克开枪
    巨型坦克开枪
    老鼠坦克开枪
    java.lang.UnsupportedOperationException: 当前坦克不支持生产
    at com.headfirst.factory.use.TankFactory.createTank(TankFactory.java:27)
    at com.headfirst.factory.use.Main.main(Main.java:33)
    */



}

改进之后有什么变化:

  1. 首先,我们把创建的具体过程交给了工厂,不在需要关注创建的细节
  2. 如果需要修改创建的过程,不需要改客户端代码,只需要修改工厂的代码
  3. 扩展同样只需要继承工厂的生产抽象对象即可。

简单工厂模式在spring中的体现:

@Bean注解让我们可以在被Spring管理的对象定义Bean的创建过程,而此时这个类就类似一个工厂,对象的创建细节被封装在具体的方法之中,同时这种方式也是一种单例设计模式,我们定义的@Bean是单例的,在需要的地方可以使用Spring的注解进行注入而不需要自己new对象。

总结:

案例可能不是十分贴切,因为仅仅只有一个new方法是不需要用工厂模式的,但是这里是个人思考之后觉得最能够联想到的情况,就使用了坦克这个例子作为文章的主体。

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 6 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-05-10
个人主页被 1.6k 人浏览