maclxf

maclxf 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

maclxf 赞了文章 · 10月3日

手摸手带你 Docker 从入门到实践

bg

在下最近遇到要在服务器上安装 Mysql、Nginx、EasyMock 等工具的场景,这里记录一下我使用 Docker 安装的过程,希望也能在类似的场景中帮助到大家~

本文前备知识需要一些 Linux 的一些基本命令,推介先看一下 <半小时搞会 CentOS 入门必备基础知识> 这篇文章。

CentOS 版本: 7.6

Nginx 版本: 1.16.1

Docker 版本: 19.03.12

你多学一样本事,就少说一句求人的话

1. 介绍

1.1 出现的原因

前后端开发到测试到生产的过程中,经常会遇到一个问题,明明我在本地跑没问题,为什么到测试环境或者生产环境就报错了了呢,常常这是因为开发、测试、生产的环境与配置不同导致的。

折腾过环境配置的人都明白其中麻烦,换一台系统、虚拟机、机器,就又要重来一次,费力费时。由于环境和配置的原因,各种奇奇怪怪因为环境和配置的 Bug,总是像打地鼠游戏里面的地鼠一样不断冒出来 🐹

Docker

Docker 对这个问题给出了一个很好的解决方案,通过镜像将除了系统之外所需要的系统环境由下而上打包,达到服务跨平台的无缝运作。也就是说,安装的时候,把特定的环境一模一样地搬过来,从而解决「在我的电脑上能跑,在 xx 环境就跑不了」的情况。

另外一个重要的原因,就是轻量,基于容器的虚拟化,Docker 的镜像仅包含业务运行所需的 runtime 环境,一个 CentOS/Ubuntu 基础镜像仅 170M,因为轻量一个宿主机可以轻松安装数百个容器。

1.2 是什么

Docker 是基于 Go 语言实现的云开源项目,从 2013 年发布到现在一直广受关注。Docker 可以让你像使用集装箱一样快速的组合成应用,并且可以像运输标准集装箱一样,尽可能的屏蔽代码层面的差异。它将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。

程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。

本文就不对比虚拟机跟 Docker 的区别和优劣了,每个文章都有,说烂了,想了解的话可以百度一下 😂,我这里就不多说了,下面直接看看怎么安装怎么用起来吧。

2. 安装 & 配置

2.1 Mac 下安装

在下直接使用 Homebrew Cask 来安装,Mac 下:

# Homebrew 安装
$ braw cask install docker

即可,安装完输入命令,直接报错!

➜  ~ docker
zsh: command not found: docker  # 报错

遇到这个报错别担心,安装完之后要在应用列表里面双击 Docker 应用,输入密码之后就可以使用这个命令了 😅。

2.2 CentOS 下安装

Docker 要求 CentOS 版本必须在 6.5 及以上才可以安装。

# 安装
$ sudo yum install yum-utils device-mapper-persistent-data lvm2
$ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
$ sudo yum install docker-ce

# 开启 Docker
$ sudo systemctl start docker

在 Windows 上可以直接下载安装包来安装,或者 Mac 上不使用 Homebrew 也可以去官网直接下载安装包来安装,百度一下到处都是安装方法,其他的就不用多说。

3. 简单配置并跑起来

3.1 配置镜像加速

在 MacOS 的 Docker 配置 Perferences -> Docker Engine 或者 Windows 的 Settings -> Deamon 中的 JSON 中增加一项 registry-mirrors 如下

Docker镜像加速配置

配置完之后在命令行中 docker info 就可以查看到我们配置的镜像加速地址了。

➜  ~ sudo docker info
...
 Registry Mirrors:
  https://reg-mirror.qiniu.com/
  http://hub-mirror.c.163.com/
  https://registry.docker-cn.com/
...

如果你的系统的 Docker 没有客户端,比如 CentOS 中,可以直接修改 deamon 配置文件:

# 修改/创建 docker 的 deamon 配置文件
$ sudo vi /etc/docker/daemon.json

# 修改为如下配置
{
  "experimental": false,
  "debug": true,
  "registry-mirrors": [
    "https://reg-mirror.qiniu.com",
    "http://hub-mirror.c.163.com",
    "https://registry.docker-cn.com"
  ]
}

# 修改完 :wq 重启
$ sudo systemctl restart docker

3.2 Hello World !

然后就可以快乐跑起来我们第一个 Docker 指令 Hello World 了

Docker跑起Helloworld

Good start ! 🎉

4. 镜像 & 容器 & 仓库

镜像和容器的关系就像类和类的实例,一个镜像可以同时跑多个容器,单个容器实例又可以创建新的镜像。如下图:

镜像容器仓库

下面解释一下这个图里面出现的元素

概念说明
Docker 镜像 Images用于创建 Docker 容器的只读模板,比如 Ubuntu 16.04系统、Nginx 1.16.0 等,是一个特殊的文件系统,包括容器运行时需要的程序、库、资源、参数等,但不包含任何动态数据,内容在构建后也不会被改变,一个镜像可以创建多个容器
Docker 容器 Container容器是独立运行、相互隔离的一个或一组应用,是镜像创建的运行实例,实质是进程,可以看作为一个简易版的 Linux 环境 + 运行在其中的应用程序
Docker 客户端 Client客户端通过命令行或者其他工具使用 Docker SDK (https://docs.docker.com/devel... 与 Docker 的守护进程通信
Docker 主机 Host一个物理或者虚拟的机器用于执行 Docker 守护进程和容器
Docker 仓库 Repository集中存放镜像文件的地方,分为公有仓库和私有仓库。
Docker 注册服务器 Registry是一个集中存储、分发镜像的服务,官方的叫 Docker Hub。一个 Docker Registry 中可包含多个仓库,每个仓库可以包含多个标签 Tag 的镜像,不同的标签对应不同的版本
Docker MachineDocker Machine 是一个简化 Docker 安装的命令行工具,通过一个简单的命令行即可在相应的平台上安装Docker,比如 VirtualBox、 Digital Ocean、Microsoft Azure

容器的生命周期图示

容器的生命周期

容器的五个核心状态,也就是图中色块表示的:Created、Running、Paused、Stopped、Deleted:

  1. Created:容器已经被创建,容器所需的相关资源已经准备就绪,但容器中的程序还未处于运行状态。
  2. Running:容器正在运行,也就是容器中的应用正在运行。
  3. Paused:容器已暂停,表示容器中的所有程序都处于暂停 ( 不是停止 ) 状态。
  4. Stopped:容器处于停止状态,占用的资源和沙盒环境都依然存在,只是容器中的应用程序均已停止。
  5. Deleted:容器已删除,相关占用的资源及存储在 Docker 中的管理信息也都已释放和移除。

本文主要关注于使用,就不太赘述这些状态的切换等,下面直接上手。

5. 基本使用

5.1 操作命令

# 开启 Docker 开机自启动
$ sudo systemctl enable docker

# 关闭 Docker 开机自启动
$ sudo systemctl disable docker

5.2 镜像命令

# 去下载镜像,先从本地找,没有去镜像,最后没有去 hub,标签不写默认为 lastest
$ docker pull [镜像名]:[标签Tag]

# 列出本机的所有 image 文件,-a 显示本地所有镜像(包括中间镜像),-q 只显示镜像ID,--digests 显示镜像的摘要信息
$ docker image ls
$ docker images

# 删除 image 文件, -f 强制删除镜像
$ docker rmi [镜像名][:标签Tag]
$ docker rmi [镜像名1][:标签Tag] [镜像名2][:标签Tag]    # 删多个
$ docker rmi $(docker ps -a -q)    # 删全部,后面是子命令

# 查询镜像名称,--no-trunc 显示完整的镜像描述,--filter=stars=30 列出star不少于指定值的镜像,--filter=is-automated=true 列出自动构建类型的镜像
$ docker search [关键字]

# 下载镜像,标签 tag 不写默认为 lastest,也可以自己加比如 :3.2.0
$ docker pull [镜像名][:标签Tag]

5.3 容器命令

# 列出本机正在运行的容器,-a 列出本机所有容器包括终止运行的容器,-q 静默模式只显示容器编号,-l 显示最近创建的容器
$ docker container ls     # 等价于下面这个命令
$ docker ps

# 新建并启动容器
$ docker run [option] [容器名] 

# 启动容器
$ docker start [容器ID]/[容器Names]

# 重启容器
$ docker restart [容器ID]/[容器Names]

# 终止容器运行
$ docker kill [容器ID]  # 强行终止,相当于向容器里面的主进程发出 SIGKILL 信号,那些正在进行中的操作会全部丢失
$ docker kill $(docker ps -a -q) # 强行终止所有容器
$ docker stop [容器ID]  # 从容终止,相当于向容器里面的主进程发出 SIGTERM 信号,然后过一段时间再发出 SIGKILL 信号
$ docker stop $(docker ps -a -q) # 终止所有容器

# 终止运行的容器文件,依然会占据硬盘空间,可以使用 docker container rm 命令删除,-f 强制删除可以删除正在运行的容器
$ docker rm [容器ID]
$ docker rm `docker ps -aq`    # 删除所有已经停止的容器,因为没停止的rm删不了需要加-f

# 查看容器的输出,-t加入时间戳,-f跟随最新日志打印,--tail数字显示最后多少条,如果docker run时,没有使用-it,就要用这个命令查看输出
$ docker logs [容器ID]

# 查看容器进程信息
$ docker top [容器ID]/[容器Names]
$ docker port [容器ID]/[容器Names]

# 退出容器
$ exit           # 容器退出
ctrl + p + q     # 容器退出,快捷键

# 进入容器
$ docker attach [容器ID]      # 退出容器时会让容器停止,本机的输入直接输到容器中
$ docker exec -it [容器ID]    # 退出容器时不会让容器停止,在已运行的容器中执行命令,不创建和启动新的容器

# 设置容器在docker启动时自动启动
$ docker container update --restart=always [容器名字]

这里要特别说一下 docker runoption,因为最常用:

  1. --name 为容器指定一个名称;
  2. -d 容器启动后进入后台,并返回容器 ID,即启动守护式容器;
  3. -P 随机端口映射;
  4. -p 80:8080 将本地 80 端口映射到容器的 8080 端口;
  5. bash 容器启动以后,内部第一个执行的命令。这里启动 bash,保证用户可以使用 Shell;
  6. -i 以交互模式运行容器,通常与 -t 同时使用;
  7. -t 为容器重新分配一个伪输入终端,容器的 Shell 会映射到当前的 Shell,然后在本机窗口输入的命令,就会传入容器,通常与 -i 同时使用;
  8. --rm 在容器终止运行后自动删除容器文件;
  9. --restart=always 设置容器自启动;
  10. -v /xxx:/yyy 映射命令,把本机的 xxx 目录映射到容器中的 yyy 目录,也就是说改变本机的 xxx 目录下的内容, 容器 yyy 目录中的内容也会改变;

比如我在 CentOS 下跑起来一个 CentOS 的 Docker 容器:

# 下载
$ docker pull centos

# 在上面下载的 centos 镜像基础上,新建一个容器名为 mycentos0901 的 centos 实例,并进入这个容器的 bash
$ docker run -it --name mycentos0901 0d120b6ccaa8

[root@169c9fffeecd /]   # 进入容器,下面输入命令,注意这里 root 后面的一串 ID
$ ls       # 可以看到centos的根目录文件列表
$ docker   # bash: docker: command not found 这个容器没有安装docker

是不是很神奇,我们可以在一开始的 CentOS 下面执行 docker ps 来查看容器列表:

image-20200901225909737

你会发现上面那个 ID,正是下面列表中跑起来的这个容器的 ID,镜像的 ID 也是我们前面 pull 下来的 CentOS 镜像 ID,名字也是我们起的 mycentos0901

如果 docker run 之后报 Conflict. The container name "xxxx" is already in use by container 就直接运行 docker rm $(docker ps -a -q) 删除已停止的容器,或者精确删除 docker rm [containerID] 也可以,就可以了。

5.4 几个常见场景的命令使用

守护式启动容器

使用 centos 以后台模式启动一个容器 docker run -d --name mycentos0903 0d120b6ccaa8,启动之后 docker ps -a 查看,发现容器并不在运行中,这是因为 Docker 的运行机制:Docker 容器后台运行,必须有一个前台进程

容器运行的命令如果不是那些一直挂起的命令,比如 toptail ,运行结束会自动退出。所以为了让容器持续在后台运行,那么需要将运行的程序以前台进程的形式运行。

比如这里在后台运行一个命令,这个命令一直在打印 docker run -d centos /bin/sh -c "while true; do echo hello zzyy; sleep 2; done",然后我们 logs 查看一下:

docker_logs

退出容器后对容器操作

退出容器后可以通过 exec 方法对正在运行的容器进行操作:

image-20200911142617186

在容器中拷贝文件到外部

拷贝文件使用 cp 命令

$ docker cp [容器ID]/[容器Names]:[要拷贝的文件目录] [本机目录]   # 容器文件拷贝到本机
$ docker cp [本机目录] [容器ID]/[容器Names]:[要拷贝的文件目录]   # 本机文件拷贝到容器

cp 不仅能把容器中的文件/文件夹拷贝到本机,也可以把本机中的文件/文件夹拷贝到容器。

演示一下,这里先到容器里面创建一个无聊的文件 xixi.txt,然后拷贝到本机:

image-20200921210352644

实用的时候,我们可以拷贝配置、日志等文件到本地。

6. 安装 MySQL

# 查询镜像
$ docker search mysql

# 下载镜像,实测没配置镜像加速的时候会比较慢,配置了就好一些
$ docker pull mysql

# 查看镜像
$ docker images

# 创建并运行容器
$ docker run -d -p 3307:3306 -e MYSQL_ROOT_PASSWORD=888888 -v /Users/sherlocked93/Personal/configs/mysql.d:/etc/mysql/conf.d --name localhost-mysql mysql

稍微解释一下上面的参数:

  1. -p 3307:3306 将本机的 3307 端口映射到 mysql 容器的 3306 端口,根据需要自行更改;
  2. -e MYSQL_ROOT_PASSWORD=<string> 设置远程登录的 root 用户密码;
  3. --name <string> 可选,设置容器别名;
  4. -v xxx/mysql.d:/etc/mysql/conf.d 将本地目录下设置文件夹映射到容器的 /etc/mysql/conf.d
  5. -v xxx/logs:/logs 将本机指定目录下的 logs 目录挂载到容器的 /logs
  6. -v xxx/data:/var/lib/mysql 将主机制定目录下的 data 目录挂载到容器的 /var/lib/mysql

运行截图:

安装Mysql

然后去 Navicat 中就可以连接到 MySQL 了。

这也太爽了!真的是几行命令就装好了啊,比之前真是快乐多了 😂

7. 安装 Nginx

Nginx 的安装和其他的类似,如果你还不太了解 Nginx 如何使用,可以参看 <Nginx 从入门到实践,万字详解> 这篇文章,看完基本就了解如何使用和配置了。

# 查询/下载镜像
$ docker search nginx
$ docker pull nginx

image-20200922203203685

然后创建一个临时的容器,目的是把默认配置拷贝到本机,我这里把配置文件放到 /mnt 目录下,主要是三个配置文件夹:

  1. /etc/nginx 放置 Nginx 配置文件;
  2. /var/log/nginx/ 放置 Nginx 日志文件;
  3. /usr/share/nginx/html/ 放置 Nginx 前端静态文件都放在这个文件夹;

分别把这几个目录都拷贝到本机的 /mnt 文件夹下的 nginxnginx_logshtml 文件夹。

刚刚创建的临时容器没用了 docker rm -f [临时容器ID] 把临时容器干掉,然后 docker run 重新创建 Nginx 容器:

$ docker run -d --name localhost-nginx -p 8082:80 \
-v /mnt/nginx:/etc/nginx \
-v /mnt/nginx_logs:/var/log/nginx \
-v /mnt/html:/usr/share/nginx/html \
--privileged=true nginx

--privileged=true 表示容器内部对挂载的目录拥有读写等特权。

其他配置刚刚上面之前已经讲过,应该不用讲了。

image-20200922204931582

然后在你自己浏览器上就可以访问了,如果是云服务器,记得开放对应端口。

8. 安装 Easy Mock

因为 Easy Mock 依赖 Redis 和 MongoDB,因此本地环境使用 docker-compose 来搭建 Easy Mock 应该算是最佳实践了。

安装 docker-compose

官方文档:https://docs.docker.com/compose/install/

首先你得确定拥有 docker 环境,如果你是 Windows / Mac 用户,那么安装客户端,就会自带 docker-compose 了。

因为本次我们是在云服务器 CentOS7.6 上搭建,所以我们需要自行安装 docker-compose,运行如下命令,下载当前稳定版本的 docker-compose

$ sudo curl -L "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

修改文件权限为可执行文件

$ sudo chmod +x /usr/local/bin/docker-compose

验证是否安装成功

$ docker-compose version

编写 docker-compose.yml 配置文件

可以参考官方文档给出的部署文档,也可以参考我下面的配置过程。

首先新建文件 docker-compose.yml 并将下面 docker-compose 文件内容复制进入 docker-compose.yml,然后将内容中注释位置替换为自己需要的本地地址

version: '3'

services:
  mongodb:
    image: mongo:3.4.1
    volumes:
      #  /apps/easy-mock/data/db 是数据库文件存放地址,根据需要修改为本地地址
      - '/apps/easy-mock/data/db:/data/db'
    networks:
      - easy-mock
    restart: always

  redis:
    image: redis:4.0.6
    command: redis-server --appendonly yes
    volumes:
      #  /apps/easy-mock/data/redis 是 redis 数据文件存放地址,根据需要修改为本地地址
      - '/apps/easy-mock/data/redis:/data'
    networks:
      - easy-mock
    restart: always

  web:
    image: easymock/easymock:1.6.0
    # easy-mock 官方给出的文件,这里是 npm start,这里修改为 npm run dev
    command: /bin/bash -c "npm run dev:server"
    ports:
      - 7300:7300  # 改为你自己期望的映射
    volumes:
      # 日志地址,根据需要修改为本地地址
      - '/apps/easy-mock/logs:/home/easy-mock/easy-mock/logs'
    networks:
      - easy-mock
    restart: always

networks:
  easy-mock:

启动 Easy Mock

在 docker-compose 文件目录下,运行如下命令:

$ docker-compose up -d

如果遇到 easymock docker 实例报文件权限错误

Error: EACCES: permission denied....

要在项目根目录执行以下命令

$ chmod 777 /yourfile/logs

然后就可以通过浏览器上的 你的域名.com:7300 访问到 easy-mock 了!

如果你觉得域名后面跟着端口号挺难看的,你可以通过配置 Nginx 的二级域名来访问你部署的 easy-mock,配置二级域名的方法参见 这篇文章

9. 可视化管理

关于可视化查询工具,这里就简单推介一个 LazyDocker,由于是在终端运行的,而且支持键盘操作和鼠标点击,就挺骚气的,有了这个一些查询语句可以少打几次了。

lzd

安装比较简单,运行下面的命令:

$ docker run --rm -it -v \
/var/run/docker.sock:/var/run/docker.sock \
-v ~/.config/lazydocker:/.config/jesseduffield/lazydocker \
lazyteam/lazydocker

可以设置一个终端的 alias

$ alias lzd='docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock -v ~/.config/lazydocker:/.config/jesseduffield/lazydocker lazyteam/lazydocker'

然后你在终端输入 lzd 就可以浏览你的镜像、容器、日志、配置、状态等等内容了。

10. 结语

由于在下目前使用 Docker 的主要场景是 MySQL、Nginx 之类工具的安装,所以本文所介绍的内容也大多属于这个场景。

篇幅原因 Docker 还有一些内容本文没有介绍,但上面的内容已基本满足日常的使用,其他 Docker 的内容可以关注一下在下的后续文章~


网上的帖子大多深浅不一,甚至有些前后矛盾,在下的文章都是学习过程中的总结,如果发现错误,欢迎留言指出~

参考文档:

  1. Empowering App Development for Developers | Docker 官方网站
  2. Docker核心技术(基础篇)
  3. Docker安装mysql
  4. Docker文档
  5. Docker-compose文档
  6. 使用 docker 运行 easy-mock - 知乎
  7. docker-compose easy-mock - 简书
  8. 使用 docker 运行 easy-mock | CodingDiary
  9. easymock官方docker仓库:easy-mock/easy-mock-docker
  10. 使用docker安装nginx

作者其他高赞文章:

  1. JS 中可以提升幸福度的小技巧
  2. Vue 使用中的小技巧
  3. Nginx 从入门到实践,万字详解!
  4. 半小时搞会 CentOS 入门必备基础知识
  5. 手摸手 Webpack 多入口配置实践
  6. 前端路由跳转基本原理

PS:本人博客地址 Github - SHERlocked93/blog,也欢迎大家关注我的公众号【前端下午茶】,一起加油吧~

查看原文

赞 32 收藏 23 评论 4

maclxf 收藏了文章 · 9月16日

值得收藏的在线设计工具

作者:abhiprojectz

翻译:疯狂的技术宅

原文:https://dev.to/abhiprojectz/t...

未经允许严禁转载

介绍

这是一些精心挑选的在线工具,这些工具都经过了测试,我发现它们非常有用,并与大家分享。

背景

1. SVG BACKGROUNDS

SVGB

与PNG、JPG 或 GIF一样,SVG 图像也可以用作 CSS 中的背景图像。这是一种很酷的方法,因为我们可以将主要元素本身与纯色背景颜色匹配,并使页面背景渗入 SVG 内部的负空间。

网址:https://www.svgbackgrounds.com/

2. FREEPIK BACKGROUND COLLECTION

FREEPIK BACKGROUND COLLECTION

可以检索并下载大量免费的 SVG 背景资源。 ✓免费供商业用途 ✓高质量图像。

网址:https://www.freepik.com/free-...

3. HERO PATTERNS

HERO PATTERN

一组可供你平铺的 SVG 背景图案,供你在 Web 项目中使用。

网址:https://www.heropatterns.com/

4. FLATICONS

可以通过简单的步骤创建精美的设计。

  1. 搜索图标,选择你的收藏夹并将其放在工作区中
  2. 你可以更改大小,颜色和背景。
  3. 以你喜欢的格式和大小下载设计。

网址:https://www.flaticon.com/patt...

5. LOADING.IO PATTERNS

LOADING.IO PATTERNS

这是个很棒的工具,让你的背景看起来很漂亮。

网址:https://loading.io/pattern/



COLOR

颜色

1. KHROMA

KHROMA

Khroma 是用来发现、搜索和保存色彩组合和调色板的最快方法。可以创建无限的收藏夹库供以后参考。

网址:http://khroma.co/

2. COLOR HUNT

COLOR HUNT

Color Hunt 是一个免费开放的平台,可通过数千种精选的时尚调色板来激发色彩灵感。

网址:https://colorhunt.co/

3. COOLORS

Create the perfect palette or get inspired by thousands of beautiful color schemes. Start the generator! Explore trending palettes
创建完美的调色板或从数千种美丽的配色方案中汲取灵感。启动发电机!探索流行的调色板

网址:https://coolors.co/

4. PALETTON

PALETTON

这是一个调色板工具,可以使用各种颜色模型将相邻颜色或互补色组合到主色调。

网址:https://paletton.com/

5. COLOR MIND

一键生成颜色组合。 Colormind 使用深层神经网络创建了具有凝聚力的配色方案。

网址:http://colormind.io/


SVG动画

1. SVG GATOR

SVG GATOR

使制作 SVG 动画变得非常简单,无需编码,基于浏览器,完全免费。可以制作精美的动画并导出单个动画 SVG 文件。

网址:https://www.svgator.com/

2. INKSACPE

INKSACPE

Sozi 是一种演示应用程序,它通过将 JavaScript 嵌入 SVG 来生成平移、缩放和旋转等效果,以便在 Web 浏览器中进行查看。

网址:https://inkscape.org/learn/an...

3. ADOBE ANIMATE

ADOBE ANIMATE

Adobe 创建了 Animate —— 一个功能强大的 SVG 动画工具。这是一种创建精美动画的简便方法。

网址:https://www.adobe.com/product...

其他工具

1. BLOB MAKER

BLOB MAKER

为你设计自己的 SVG 形状。可以修改复杂度、对比度和颜色。

网址:https://www.blobmaker.app/

2. GET WAVES

GET WAVES

如果你喜欢 SVG 图形形生成器,可以尝试 getwaves.io,它能够为你的网页做一些很酷的波纹转换。

网址:https://getwaves.io/

3. CSS GRADIENTS

CSS GRADIENTS

一个免费的 CSS 渐变生成器工具,可以为你的网站、博客或社交媒体资料创建彩色渐变背景。

网址:https://cssgradient.io/

4. UNDRAW

可以在这里找到适合你需求的图像,然后下载。可使用实时彩色图像生成来匹配你的品牌标识。

网址:https://undraw.co/illustrations

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

maclxf 赞了回答 · 8月28日

解决我该如何优化这个句SQL??

把你表结构贴出来。
还有 explain 的信息截取完整。
在看看 status=0 的数据是不是有2 万多条。
你取一条的话,可以加 limit 1

关注 5 回答 5

maclxf 赞了回答 · 8月28日

解决我该如何优化这个句SQL??

SELECT id FROM number WHERE status = 0 limit 1,让你的sql快到飞起

关注 5 回答 5

maclxf 提出了问题 · 8月19日

解决我该如何优化这个句SQL??

有一张快递单号表number,表中有大概5w条数据
每次要使用时取出一条出来,语句如下

SELECT id FROM number WHERE status = 0

status 默认为0待使用 ,1使用中,2已使用
但是每次执行时间超长,大概7秒左右,已给 status 建了索引,但是效果不佳

Explain结果

image.png


更新内容 表结构

CREATE TABLE `number` (
  `id` int(11) NOT NULL,
  `number` varchar(32) NOT NULL,
  `number_code` varchar(64) NOT NULL,
  `order_id` int(11) DEFAULT NULL,
  `oid` varchar(32) DEFAULT NULL,
  `src` varchar(128) NOT NULL,
  `added_user_id` int(11) NOT NULL,
  `added_user_name` varchar(32) NOT NULL,
  `added_date` datetime NOT NULL COMMENT '添加时间',
  `added_ip` varchar(16) NOT NULL COMMENT '添加时的IP',
  `updated_user_id` int(11) DEFAULT NULL COMMENT '更新人id',
  `updated_user_name` varchar(32) DEFAULT NULL,
  `updated_date` datetime DEFAULT NULL,
  `updated_ip` varchar(16) DEFAULT NULL,
  `push_json` text,
  `push_date` datetime DEFAULT NULL,
  `return_json` text,
  `return_retstatus` varchar(256) DEFAULT NULL,
  `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT 'status 默认为0待使用 ,1使用中,2已使用'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

ALTER TABLE `number`
  ADD PRIMARY KEY (`id`),
  ADD UNIQUE KEY `oid` (`oid`),
  ADD UNIQUE KEY `order_id` (`order_id`),
  ADD KEY `order_id_2` (`order_id`),
  ADD KEY `status` (`status`);
  
ALTER TABLE `number`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;

关注 5 回答 5

maclxf 赞了回答 · 7月29日

解决微信小程序的背景图要怎么显示?

background-image 只能用网络url或者base64 . 本地图片要用image标签才行。

关注 10 回答 6

maclxf 收藏了文章 · 6月22日

PHP yield 协程 生成器 用法探究(一)

php yield

写在前面

这篇文章,要和大家探讨的是 PHP yield 在 生成器用法,不带 foreachfor, while 循环的那种。就讨论 yield 将一个函数变成为生成器的用法。

关于yield 特性,是在开发 PHP5 时被提上日程,PHP5.5 版本正式加入。

关于yield的使用,我看到大部分文章都停留在,使用yield如何在foreach中传出数据,今天想给大家讲讲 生成器 所有语法。

三部曲

官网讲解

生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组, 那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样, 和普通函数只返回一次不同的是, 生成器可以根据需要 yield 多次,以便生成需要迭代的值。

看了下官网对他讲解:php.net 生成器语法 . 每个字都认识,但似乎还是体会到它讲的内涵。官网我们主要看两部分内容:

  1. yield 的语法。
  2. 使用例子。

先说语法, yield 的左边是一个赋值语句,右边可以是值(也可是表达式) 。而yield 会先执行右边的表达式,并把值$value送到生成器外面。当生成器收到值后,会执行yield左边的语句,赋值给$data.

<?php
$data = (yield $express);

语法讲完了,估计大家还是有些懵,那就看看官网下面代码例子吧,我看里面例子参差不齐。

通过例子来了解它

不论是学 人类语言,计算机语言,都是模仿开始

对于一个用人类语言来描述,都不那么明晰时,所以那就通过例子告诉你它能做什么,不能做什么。

相关代码,我放到gitee了,希望你能复制到你本地运行下,亲自运行感受下,有助于了理解接下来的内容。

git clone https://gitee.com/xupaul/PHP-...

怎样才能产生 Generator

先定义一个函数,在函数内 写个 yield 关键词,将这个函数调用赋值给一个变量。一个生成器就产生了。

由于例子代码很多,我把例子放到 gitee 了。同时以下的文章中提到"例子"就是在 gitee 中的代码。

代码 /php-yield-test/yieldFunctions.php 是生成器按照不同语法组合定义了多个生成器。

测试代码 /php-yield-test/whatIsGenerator.php,用来检查哪些函数能构成生成器,哪些不能。运行结果如下

20200511122351.png

  1. 函数内必须有 yield 关键词,函数可以是全局函数,或者类的方法。
  2. 哪怕 yield 肯定不会被执行,也会产生生成器。见:yield_func4
  3. 光秃秃 的 yield 关键词就行(不向外送出,不处理外面的输入)。见: yield_func2
  4. 函数内使用 生成器 并不能让自己也成为生成器,见:yield_func5
  5. eval函数中直接运行 yield 会报错, 见:yield_func11
是的,函数内有没有foreach,while,for 语句都不是关键,关键是 yield. 生成器的类型判断用 $gen instanceof Generator

生成器的函数

Generator 对象是从 generators返回的.

Generator 对象不能通过 new 实例化.
摘自 php.net generator

看着以上方法,是不想起了Iterator, 他们的确很像。同时注意,官网zh语言版本的文档没有索引方法getReturn,访问也是404。文档以en版为准,ch做参考。

以上就是生成器所有的方法,我们一个个来看。

测试方法代码 /php-yield-test/generatorMothod.php, 这里面对每个方法都有使用举例,运行结果如下。

20200511122951.png

20200511123119.png

好上面的运行结果,干巴巴的,不详细。我以下面的程序画个流程图:

<?php
function yield_func()
{
    echo 'run yield_func' . PHP_EOL;
    $get = (yield 12);
    echo $get . PHP_EOL;
    $get2 = (yield 55);
    echo $get2 . PHP_EOL;
    return 'a';
}
$gen = yield_func();

$re = $gen->current();
echo 'get re: ' . $re . PHP_EOL;
$gen->send('cc');
$re2 = $gen->current();
echo 'get re2: ' . $re2 . PHP_EOL;
$gen->send('hello');
$re3 = $gen->getReturn();
echo 'get return: ' . $re3 . PHP_EOL;

gaitubao_新建项目.png

图中,看到yield_func()函数作为生成器后,被yield 分成了一块一块的代码段,没执行一段后,就跳出,等待外部程序的调度,这就好比多线程一样,执行时,随时都有可能被打断,让出CPU,不过协程是手动调用yield让出,程序运行顺序是可预期的。调用了current()开始执行,当运行到yield代码,让出cpu时,调用send(),又能让生成器继续运行了。最后getReturn()获取生成器的返回值。

好的,先有个大致概念,现在我们仔仔细细了解下各个函数。

Generator::current

  • 返回当前产生的值
<?php
function yield_func()
{
    yield 12;
    return 'a';
}

$gen = yield_func();
$re = $gen->current();
echo 'current return : ' . $re;

输出:

current return : 12

看到 php-yield-test/generatorMothod.php 代码。

通过第一个代码事例,可得,对一个generator调用current方法,才算真正开始执行。执行到yield为止。如果不能命中yield,则执行到函数结束。

非generoator会立马执行并得到结果,而非一个生成器对象。

通过例子2,调用current一次,两次呢,第一次可以看到代码执行日志,第二次,只是把上一次的结果返回给我们而已,并不是让该生成器重新执行。

通过例子1,调用该函数还会获取到返回值,返回的内容就是 yield 表达式左边的内容。如果表达式无内容,则是NULL.

Generator::send

  • 向生成器中传入一个值
<?php
function yield_func()
{
    $data = yield 12;
    echo 'get yield data: ' . $data;
    return 'a';
}

$gen = yield_func();
$re = $gen->current();
$gen->send(32);

输出:

get yield data: 32

例子3,是一个current,send的常规调用。调用current代码运行yield等到用户send输入参数。接收到输入后,继续运行。current能够接收到yield弹出的值,send返回值为空。

例子4,直接调用send,相当于调用current,send。不过current的返回值,并不会通过send传给用户。

也就是说:跳过current,直接调用send,会丢失yield的弹出值。

转载著名出处 sifou

Generator::next

  • 让生成器继续执行
<?php
function yield_func()
{
    echo 'run to code line: ' . __LINE__ . PHP_EOL;
    yield;
    echo 'run to code line: ' . __LINE__ . PHP_EOL;
    return $result;
}

$gen = yield_func();
$gen->current();
echo 'current called' . PHP_EOL;
$gen->next();

输出:

run to code line: 4
current called
run to code line: 6

例子5,这是一个较为常规的调用,调用current代码运行yield等到用户输入,这是调用next跳过,让代码继续运行。

例子6,直接调用next,相当于调用currentnext。而且通过最后打印$result, 我们发现怎么有点像在调用 $gen->send(NULL);

Generator::rewind

  • 重置迭代器
<?php
function yield_func()
{
    echo 'run to code line: ' . __LINE__ . PHP_EOL;
    $result = yield 12;
    echo 'run to code line: ' . __LINE__ . PHP_EOL;
}

$gen = yield_func();
echo 'call yield_func rewind ' . PHP_EOL;
$gen->rewind();

输出:

call yield_func rewind 
run to code line: 4

例子7,8 中,发现调用该方法,会导致隐式调用current

例子9 中,发现在执行过一个yield代码段后,再次调用该方法,会导致报错(哪怕该 生成器已结束)。

Generator::throw

  • 向生成器中抛入一个异常
<?php
function yield_func()
{
    try {
        $re = yield 'exception';
    } catch (Exception $e) {
        echo 'catched exception msg: ' .$e->getMessage();
    }
}

$gen = yield_func();
$gen->throw(new \Exception('new yield  exception'));

输出:

catched exception msg: new yield  exception

通过以上简单的例子可得,throw 就是让yield这行代码产生异常,让外面的try catch 捕获我们生成的那个异常。

例子11中,构造生成器,并调用current方法,运行到yield处,再调用throw,就能捕获到异常。

例子12中,当调用send方法,跳过函数内yield代码时,再调用throw传入异常,就没法捕获了。

Generator::valid

  • 检查迭代器是否被关闭
<?php
function yield_func()
{
    yield 12;
    return 'a';
}

$gen = yield_func();
$gen->send(1);
$check = $gen->valid();
echo 'the generator valid ? ' . intval($check);

输出:

the generator valid ? 0

例子12中,发现current被隐式调用。

例子13中,可得,当生成器运行到yield代码段时,用valid函数检查,都会返回true

所以,别问我是否已运行,问就是运行。该方法用来获取是否关闭状态,不是 是否运行状态!运行到底,运行到return就是 关闭状态。

Generator::key

  • 返回当前产生的键
<?php
function yield_func()
{
    yield 1 => 'abc';
}

$gen = yield_func();
echo 'value is :' . $gen->current() . PHP_EOL;
echo 'key is: ' . $gen->key() . PHP_EOL;

输出:

value is :abc
key is: 1

从以上例子中,可得yield可显示设置返回的key.

例子15 中,发现key的分发规律和PHP数组键值发放策略是差不多的,默认从0开始,未指定则是以上一个数字key+1作为当前的key.

例子16 中,我们又发现current被隐式调用。

Generator::__wakeup

  • Generator::__wakeup — 序列化回调
<?php
function yield_func()
{
    yield 1 => 'abc';
}
$gen = yield_func();
try {
$ser = serialize($gen);
} catch (\Exception $e) {
    print_r($e->getMessage());
}

输出:

Serialization of 'Generator' is not allowed

这是一个魔术方法,见 PHP 魔术方法,也就是说 生成器 不能被序列化成一个字符串。

例子17就不用说了,看下例子18,看样子序列化成功了。也就是说一个生成器做为一个方法可以被序列化,当函数变成生成器时,就不能被序列化了。

Generator::getReturn

<?php
function yield_func()
{
    yield 1 => 'abc';
    return 32;
}

$gen = yield_func();
$gen->send(0);
echo 'call yield_func return, and get: ' . $gen->getReturn();

输出:

call yield_func return, and get: 32

该函数就是获取生成器最后的返回值。如果没有return语句,或者没有执行到return语句,调用该函数得到的就是NULL。

例子19 可得,getReturn 能够获取到生成器最后的返回值。

例子19、20 可得,当生成器没有执行到return语句,或者没有执行到最后时,调用getReturn是会导致报错。

综上所述

到这里,我们就发现rewind,next__wakeup 这两个函数感觉没啥叼用呢,为啥还存在呢,因为Generator继承Iterator,自然就有了rewind, next方法,PHP 虽然支持方法覆盖,但子类的访问修饰符 不能缩紧,所以Generator只能重写这两个方法。 __wakeup 继承自 stdClass

状态转换

看图:

php yield 状态图.png

画了两个状态转换图,上面的要细致,繁复一点。下面的精简版,便于快速理解。

总结

以上就是关于 PHP 生成器所有内容,希望你能学会掌握这门强大的语法,下一讲,我们手把手一起来做一个任务调度器,实战一下。

有问题欢迎提问,谢谢大家!

没人比我更懂 PHP yield

查看原文

maclxf 赞了文章 · 3月22日

设计模式在美团外卖营销业务中的实践

一、前言

随着美团外卖业务的不断迭代与发展,外卖用户数量也在高速地增长。在这个过程中,外卖营销发挥了“中流砥柱”的作用,因为用户的快速增长离不开高效的营销策略。而由于市场环境和业务环境的多变,营销策略往往是复杂多变的,营销技术团队作为营销业务的支持部门,就需要快速高效地响应营销策略变更带来的需求变动。因此,设计并实现易于扩展和维护的营销系统,是美团外卖营销技术团队不懈追求的目标和必修的基本功。

本文通过自顶向下的方式,来介绍设计模式如何帮助我们构建一套易扩展、易维护的营销系统。本文会首先介绍设计模式与领域驱动设计(Domain-Driven Design,以下简称为DDD)之间的关系,然后再阐述外卖营销业务引入业务中用到的设计模式以及其具体实践案例。

二、设计模式与领域驱动设计

设计一个营销系统,我们通常的做法是采用自顶向下的方式来解构业务,为此我们引入了DDD。从战略层面上讲,DDD能够指导我们完成从问题空间到解决方案的剖析,将业务需求映射为领域上下文以及上下文间的映射关系。从战术层面上,DDD能够细化领域上下文,并形成有效的、细化的领域模型来指导工程实践。建立领域模型的一个关键意义在于,能够确保不断扩展和变化的需求在领域模型内不断地演进和发展,而不至于出现模型的腐化和领域逻辑的外溢。关于DDD的实践,大家可以参考此前美团技术团队推出的《领域驱动设计在互联网业务开发中的实践》一文。

同时,我们也需要在代码工程中贯彻和实现领域模型。因为代码工程是领域模型在工程实践中的直观体现,也是领域模型在技术层面的直接表述。而设计模式,可以说是连接领域模型与代码工程的一座桥梁,它能有效地解决从领域模型到代码工程的转化。

为什么说设计模式天然具备成为领域模型到代码工程之间桥梁的作用呢?其实,2003年出版的《领域驱动设计》一书的作者Eric Evans在这部开山之作中就已经给出了解释。他认为,立场不同会影响人们如何看待什么是“模式”。因此,无论是领域驱动模式还是设计模式,本质上都是“模式”,只是解决的问题不一样。站在业务建模的立场上,DDD的模式解决的是如何进行领域建模。而站在代码实践的立场上,设计模式主要关注于代码的设计与实现。既然本质都是模式,那么它们天然就具有一定的共通之处。

所谓“模式”,就是一套反复被人使用或验证过的方法论。从抽象或者更宏观的角度上看,只要符合使用场景并且能解决实际问题,模式应该既可以应用在DDD中,也可以应用在设计模式中。事实上,Evans也是这么做的。他在著作中阐述了Strategy和Composite这两个传统的GOF设计模式是如何来解决领域模型建设的。因此,当领域模型需要转化为代码工程时,同构的模式,天然能够将领域模型翻译成代码模型。

三、设计模式在外卖营销业务中的具体案例

3.1 为什么需要设计模式

营销业务的特点

如前文所述,营销业务与交易等其他模式相对稳定的业务的区别在于,营销需求会随着市场、用户、环境的不断变化而进行调整。也正是因此,外卖营销技术团队选择了DDD进行领域建模,并在适用的场景下,用设计模式在代码工程的层面上实践和反映了领域模型。以此来做到在支持业务变化的同时,让领域和代码模型健康演进,避免模型腐化。

理解设计模式

软件设计模式(Design pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码,让代码更容易被他人理解,保证代码可靠性,程序的重用性。可以理解为:“世上本来没有设计模式,用的人多了,便总结出了一套设计模式。”

设计模式原则

面向对象的设计模式有七大基本原则:

  • 开闭原则(Open Closed Principle,OCP)
  • 单一职责原则(Single Responsibility Principle, SRP)
  • 里氏代换原则(Liskov Substitution Principle,LSP)
  • 依赖倒转原则(Dependency Inversion Principle,DIP)
  • 接口隔离原则(Interface Segregation Principle,ISP)
  • 合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
  • 最少知识原则(Least Knowledge Principle,LKP)或者迪米特法则(Law of Demeter,LOD)

简单理解就是:开闭原则是总纲,它指导我们要对扩展开放,对修改关闭;单一职责原则指导我们实现类要职责单一;里氏替换原则指导我们不要破坏继承体系;依赖倒置原则指导我们要面向接口编程;接口隔离原则指导我们在设计接口的时候要精简单一;迪米特法则指导我们要降低耦合。

设计模式就是通过这七个原则,来指导我们如何做一个好的设计。但是设计模式不是一套“奇技淫巧”,它是一套方法论,一种高内聚、低耦合的设计思想。我们可以在此基础上自由的发挥,甚至设计出自己的一套设计模式。

当然,学习设计模式或者是在工程中实践设计模式,必须深入到某一个特定的业务场景中去,再结合对业务场景的理解和领域模型的建立,才能体会到设计模式思想的精髓。如果脱离具体的业务逻辑去学习或者使用设计模式,那是极其空洞的。接下来我们将通过外卖营销业务的实践,来探讨如何用设计模式来实现可重用、易维护的代码。

3.2 “邀请下单”业务中设计模式的实践

3.2.1 业务简介

“邀请下单”是美团外卖用户邀请其他用户下单后给予奖励的平台。即用户A邀请用户B,并且用户B在美团下单后,给予用户A一定的现金奖励(以下简称返奖)。同时为了协调成本与收益的关系,返奖会有多个计算策略。邀请下单后台主要涉及两个技术要点:

  1. 返奖金额的计算,涉及到不同的计算规则。
  2. 从邀请开始到返奖结束的整个流程。

3.2.2 返奖规则与设计模式实践

业务建模

如图是返奖规则计算的业务逻辑视图:

从这份业务逻辑图中可以看到返奖金额计算的规则。首先要根据用户状态确定用户是否满足返奖条件。如果满足返奖条件,则继续判断当前用户属于新用户还是老用户,从而给予不同的奖励方案。一共涉及以下几种不同的奖励方案:

新用户

  • 普通奖励(给予固定金额的奖励)
  • 梯度奖(根据用户邀请的人数给予不同的奖励金额,邀请的人越多,奖励金额越多)

老用户

  • 根据老用户的用户属性来计算返奖金额。为了评估不同的邀新效果,老用户返奖会存在多种返奖机制。

计算完奖励金额以后,还需要更新用户的奖金信息,以及通知结算服务对用户的金额进行结算。这两个模块对于所有的奖励来说都是一样的。

可以看到,无论是何种用户,对于整体返奖流程是不变的,唯一变化的是返奖规则。此处,我们可参考开闭原则,对于返奖流程保持封闭,对于可能扩展的返奖规则进行开放。我们将返奖规则抽象为返奖策略,即针对不同用户类型的不同返奖方案,我们视为不同的返奖策略,不同的返奖策略会产生不同的返奖金额结果。

在我们的领域模型里,返奖策略是一个值对象,我们通过工厂的方式生产针对不同用户的奖励策略值对象。下文我们将介绍以上领域模型的工程实现,即工厂模式策略模式的实际应用。

模式:工厂模式

工厂模式又细分为工厂方法模式和抽象工厂模式,本文主要介绍工厂方法模式。

模式定义:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法是一个类的实例化延迟到其子类。

工厂模式通用类图如下:

我们通过一段较为通用的代码来解释如何使用工厂模式:

//抽象的产品
public abstract class Product {
    public abstract void method();
}
//定义一个具体的产品 (可以定义多个具体的产品)
class ProductA extends Product {
    @Override
    public void method() {}  //具体的执行逻辑
}
//抽象的工厂
abstract class Factory<T> {
    abstract Product createProduct(Class<T> c);
}
//具体的工厂可以生产出相应的产品
class FactoryA extends Factory{
    @Override
    Product createProduct(Class c) {
        Product product = (Product) Class.forName(c.getName()).newInstance();
        return product;
    }
}

模式:策略模式

模式定义:定义一系列算法,将每个算法都封装起来,并且它们可以互换。策略模式是一种对象行为模式。

策略模式通用类图如下:

我们通过一段比较通用的代码来解释怎么使用策略模式:

//定义一个策略接口
public interface Strategy {
    void strategyImplementation();
}
​
//具体的策略实现(可以定义多个具体的策略实现)
public class StrategyA implements Strategy{
    @Override
    public void strategyImplementation() {
        System.out.println("正在执行策略A");
    }
}
​
//封装策略,屏蔽高层模块对策略、算法的直接访问,屏蔽可能存在的策略变化
public class Context {
    private Strategy strategy = null;
​
    public Context(Strategy strategy) {
        this.strategy = strategy;
    }
  
    public void doStrategy() {
        strategy.strategyImplementation();
    }
}


工程实践

通过上文介绍的返奖业务模型,我们可以看到返奖的主流程就是选择不同的返奖策略的过程,每个返奖策略都包括返奖金额计算、更新用户奖金信息、以及结算这三个步骤。 我们可以使用工厂模式生产出不同的策略,同时使用策略模式来进行不同的策略执行。首先确定我们需要生成出n种不同的返奖策略,其编码如下:

//抽象策略
public abstract class RewardStrategy {
    public abstract void reward(long userId);
  
    public void insertRewardAndSettlement(long userId, int reward) {} ; //更新用户信息以及结算
}
//新用户返奖具体策略A
public class newUserRewardStrategyA extends RewardStrategy {
    @Override
    public void reward(long userId) {}  //具体的计算逻辑,...
}
​
//老用户返奖具体策略A
public class OldUserRewardStrategyA extends RewardStrategy {
    @Override
    public void reward(long userId) {}  //具体的计算逻辑,...
}
​
//抽象工厂
public abstract class StrategyFactory<T> {
    abstract RewardStrategy createStrategy(Class<T> c);
}
​
//具体工厂创建具体的策略
public class FactorRewardStrategyFactory extends StrategyFactory {
    @Override
    RewardStrategy createStrategy(Class c) {
        RewardStrategy product = null;
        try {
            product = (RewardStrategy) Class.forName(c.getName()).newInstance();
        } catch (Exception e) {}
        return product;
    }
}

通过工厂模式生产出具体的策略之后,根据我们之前的介绍,很容易就可以想到使用策略模式来执行我们的策略。具体代码如下:

public class RewardContext {
    private RewardStrategy strategy;
​
    public RewardContext(RewardStrategy strategy) {
        this.strategy = strategy;
    }
​
    public void doStrategy(long userId) { 
        int rewardMoney = strategy.reward(userId);
        insertRewardAndSettlement(long userId, int reward) {
          insertReward(userId, rewardMoney);
          settlement(userId);
       }  
    }
}

接下来我们将工厂模式和策略模式结合在一起,就完成了整个返奖的过程:

public class InviteRewardImpl {
    //返奖主流程
    public void sendReward(long userId) {
        FactorRewardStrategyFactory strategyFactory = new FactorRewardStrategyFactory();  //创建工厂
        Invitee invitee = getInviteeByUserId(userId);  //根据用户id查询用户信息
        if (invitee.userType == UserTypeEnum.NEW_USER) {  //新用户返奖策略
            NewUserBasicReward newUserBasicReward = (NewUserBasicReward) strategyFactory.createStrategy(NewUserBasicReward.class);
            RewardContext rewardContext = new RewardContext(newUserBasicReward);
            rewardContext.doStrategy(userId); //执行返奖策略
        }if(invitee.userType == UserTypeEnum.OLD_USER){}  //老用户返奖策略,... 
    }
}

工厂方法模式帮助我们直接产生一个具体的策略对象,策略模式帮助我们保证这些策略对象可以自由地切换而不需要改动其他逻辑,从而达到解耦的目的。通过这两个模式的组合,当我们系统需要增加一种返奖策略时,只需要实现RewardStrategy接口即可,无需考虑其他的改动。当我们需要改变策略时,只要修改策略的类名即可。不仅增强了系统的可扩展性,避免了大量的条件判断,而且从真正意义上达到了高内聚、低耦合的目的。

3.2.3 返奖流程与设计模式实践

业务建模

当受邀人在接受邀请人的邀请并且下单后,返奖后台接收到受邀人的下单记录,此时邀请人也进入返奖流程。首先我们订阅用户订单消息并对订单进行返奖规则校验。例如,是否使用红包下单,是否在红包有效期内下单,订单是否满足一定的优惠金额等等条件。当满足这些条件以后,我们将订单信息放入延迟队列中进行后续处理。经过T+N天之后处理该延迟消息,判断用户是否对该订单进行了退款,如果未退款,对用户进行返奖。若返奖失败,后台还有返奖补偿流程,再次进行返奖。其流程如下图所示:

我们对上述业务流程进行领域建模:

  1. 在接收到订单消息后,用户进入待校验状态;
  2. 在校验后,若校验通过,用户进入预返奖状态,并放入延迟队列。若校验未通过,用户进入不返奖状态,结束流程;
  3. T+N天后,处理延迟消息,若用户未退款,进入待返奖状态。若用户退款,进入失败状态,结束流程;
  4. 执行返奖,若返奖成功,进入完成状态,结束流程。若返奖不成功,进入待补偿状态;
  5. 待补偿状态的用户会由任务定期触发补偿机制,直至返奖成功,进入完成状态,保障流程结束。

可以看到,我们通过建模将返奖流程的多个步骤映射为系统的状态。对于系统状态的表述,DDD中常用到的概念是领域事件,另外也提及过事件溯源的实践方案。当然,在设计模式中,也有一种能够表述系统状态的代码模型,那就是状态模式。在邀请下单系统中,我们的主要流程是返奖。对于返奖,每一个状态要进行的动作和操作都是不同的。因此,使用状态模式,能够帮助我们对系统状态以及状态间的流转进行统一的管理和扩展。

模式:状态模式

模式定义:当一个对象内在状态改变时允许其改变行为,这个对象看起来像改变了其类。

状态模式的通用类图如下图所示:

对比策略模式的类型会发现和状态模式的类图很类似,但实际上有很大的区别,具体体现在concrete class上。策略模式通过Context产生唯一一个ConcreteStrategy作用于代码中,而状态模式则是通过context组织多个ConcreteState形成一个状态转换图来实现业务逻辑。接下来,我们通过一段通用代码来解释怎么使用状态模式:

//定义一个抽象的状态类
public abstract class State {
    Context context;
    public void setContext(Context context) {
        this.context = context;
    }
    public abstract void handle1();
    public abstract void handle2();
}
//定义状态A
public class ConcreteStateA extends State {
    @Override
    public void handle1() {}  //本状态下必须要处理的事情
​
    @Override
    public void handle2() {
        super.context.setCurrentState(Context.contreteStateB);  //切换到状态B        
        super.context.handle2();  //执行状态B的任务
    }
}
//定义状态B
public class ConcreteStateB extends State {
    @Override
    public void handle2() {}  //本状态下必须要处理的事情,...
  
    @Override
    public void handle1() {
        super.context.setCurrentState(Context.contreteStateA);  //切换到状态A
        super.context.handle1();  //执行状态A的任务
    }
}
//定义一个上下文管理环境
public class Context {
    public final static ConcreteStateA contreteStateA = new ConcreteStateA();
    public final static ConcreteStateB contreteStateB = new ConcreteStateB();
​
    private State CurrentState;
    public State getCurrentState() {return CurrentState;}
​
    public void setCurrentState(State currentState) {
        this.CurrentState = currentState;
        this.CurrentState.setContext(this);
    }
​
    public void handle1() {this.CurrentState.handle1();}
    public void handle2() {this.CurrentState.handle2();}
}
//定义client执行
public class client {
    public static void main(String[] args) {
        Context context = new Context();
        context.setCurrentState(new ContreteStateA());
        context.handle1();
        context.handle2();
    }
}

工程实践

通过前文对状态模式的简介,我们可以看到当状态之间的转换在不是非常复杂的情况下,通用的状态模式存在大量的与状态无关的动作从而产生大量的无用代码。在我们的实践中,一个状态的下游不会涉及特别多的状态装换,所以我们简化了状态模式。当前的状态只负责当前状态要处理的事情,状态的流转则由第三方类负责。其实践代码如下:

//返奖状态执行的上下文
public class RewardStateContext {
​
    private RewardState rewardState;
  
    public void setRewardState(RewardState currentState) {this.rewardState = currentState;}
    public RewardState getRewardState() {return rewardState;}
    public void echo(RewardStateContext context, Request request) {
        rewardState.doReward(context, request);
    }
}
​
public abstract class RewardState {
    abstract void doReward(RewardStateContext context, Request request);
}
​
//待校验状态
public class OrderCheckState extends RewardState {
    @Override
    public void doReward(RewardStateContext context, Request request) {
        orderCheck(context, request);  //对进来的订单进行校验,判断是否用券,是否满足优惠条件等等
    }
}
​
//待补偿状态
public class CompensateRewardState extends RewardState {
    @Override
    public void doReward(RewardStateContext context, Request request) {
        compensateReward(context, request);  //返奖失败,需要对用户进行返奖补偿
    }
}
​
//预返奖状态,待返奖状态,成功状态,失败状态(此处逻辑省略)
//..
​
public class InviteRewardServiceImpl {
    public boolean sendRewardForInvtee(long userId, long orderId) {
        Request request = new Request(userId, orderId);
        RewardStateContext rewardContext = new RewardStateContext();
        rewardContext.setRewardState(new OrderCheckState());
        rewardContext.echo(rewardContext, request);  //开始返奖,订单校验
        //此处的if-else逻辑只是为了表达状态的转换过程,并非实际的业务逻辑
        if (rewardContext.isResultFlag()) {  //如果订单校验成功,进入预返奖状态
            rewardContext.setRewardState(new BeforeRewardCheckState());
            rewardContext.echo(rewardContext, request);
        } else {//如果订单校验失败,进入返奖失败流程,...
            rewardContext.setRewardState(new RewardFailedState());
            rewardContext.echo(rewardContext, request);
            return false;
        }
        if (rewardContext.isResultFlag()) {//预返奖检查成功,进入待返奖流程,...
            rewardContext.setRewardState(new SendRewardState());
            rewardContext.echo(rewardContext, request);
        } else {  //如果预返奖检查失败,进入返奖失败流程,...
            rewardContext.setRewardState(new RewardFailedState());
            rewardContext.echo(rewardContext, request);
            return false;
        }
        if (rewardContext.isResultFlag()) {  //返奖成功,进入返奖结束流程,...
            rewardContext.setRewardState(new RewardSuccessState());
            rewardContext.echo(rewardContext, request);
        } else {  //返奖失败,进入返奖补偿阶段,...
            rewardContext.setRewardState(new CompensateRewardState());
            rewardContext.echo(rewardContext, request);
        }
        if (rewardContext.isResultFlag()) {  //补偿成功,进入返奖完成阶段,...
            rewardContext.setRewardState(new RewardSuccessState());
            rewardContext.echo(rewardContext, request);
        } else {  //补偿失败,仍然停留在当前态,直至补偿成功(或多次补偿失败后人工介入处理)
            rewardContext.setRewardState(new CompensateRewardState());
            rewardContext.echo(rewardContext, request);
        }
        return true;
    }
}

状态模式的核心是封装,将状态以及状态转换逻辑封装到类的内部来实现,也很好的体现了“开闭原则”和“单一职责原则”。每一个状态都是一个子类,不管是修改还是增加状态,只需要修改或者增加一个子类即可。在我们的应用场景中,状态数量以及状态转换远比上述例子复杂,通过“状态模式”避免了大量的if-else代码,让我们的逻辑变得更加清晰。同时由于状态模式的良好的封装性以及遵循的设计原则,让我们在复杂的业务场景中,能够游刃有余地管理各个状态。

3.3 点评外卖投放系统中设计模式的实践

3.3.1 业务简介

继续举例,点评App的外卖频道中会预留多个资源位为营销使用,向用户展示一些比较精品美味的外卖食品,为了增加用户点外卖的意向。当用户点击点评首页的“美团外卖”入口时,资源位开始加载,会通过一些规则来筛选出合适的展示Banner。

3.3.2 设计模式实践

业务建模

对于投放业务,就是要在这些资源位中展示符合当前用户的资源。其流程如下图所示:

从流程中我们可以看到,首先运营人员会配置需要展示的资源,以及对资源进行过滤的规则。我们资源的过滤规则相对灵活多变,这里体现为三点:

  1. 过滤规则大部分可重用,但也会有扩展和变更。
  2. 不同资源位的过滤规则和过滤顺序是不同的。
  3. 同一个资源位由于业务所处的不同阶段,过滤规则可能不同。

过滤规则本身是一个个的值对象,我们通过领域服务的方式,操作这些规则值对象完成资源位的过滤逻辑。下图介绍了资源位在进行用户特征相关规则过滤时的过程:

为了实现过滤规则的解耦,对单个规则值对象的修改封闭,并对规则集合组成的过滤链条开放,我们在资源位过滤的领域服务中引入了责任链模式。

模式:责任链模式

模式定义:使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。

责任链模式通用类图如下:

我们通过一段比较通用的代码来解释如何使用责任链模式:

//定义一个抽象的handle
public abstract class Handler {
    private Handler nextHandler;  //指向下一个处理者
    private int level;  //处理者能够处理的级别
​
    public Handler(int level) {
        this.level = level;
    }
​
    public void setNextHandler(Handler handler) {
        this.nextHandler = handler;
    }
​
    // 处理请求传递,注意final,子类不可重写
    public final void handleMessage(Request request) {
        if (level == request.getRequstLevel()) {
            this.echo(request);
        } else {
            if (this.nextHandler != null) {
                this.nextHandler.handleMessage(request);
            } else {
                System.out.println("已经到最尽头了");
            }
        }
    }
    // 抽象方法,子类实现
    public abstract void echo(Request request);
}
​
// 定义一个具体的handleA
public class HandleRuleA extends Handler {
    public HandleRuleA(int level) {
        super(level);
    }
    @Override
    public void echo(Request request) {
        System.out.println("我是处理者1,我正在处理A规则");
    }
}
​
//定义一个具体的handleB
public class HandleRuleB extends Handler {}  //...
​
//客户端实现
class Client {
    public static void main(String[] args) {
        HandleRuleA handleRuleA = new HandleRuleA(1);
        HandleRuleB handleRuleB = new HandleRuleB(2);
        handleRuleA.setNextHandler(handleRuleB);  //这是重点,将handleA和handleB串起来
        handleRuleA.echo(new Request());
    }
}


工程实践

下面通过代码向大家展示如何实现这一套流程:

//定义一个抽象的规则
public abstract class BasicRule<CORE_ITEM, T extends RuleContext<CORE_ITEM>>{
    //有两个方法,evaluate用于判断是否经过规则执行,execute用于执行具体的规则内容。
    public abstract boolean evaluate(T context);
    public abstract void execute(T context) {
}
​
//定义所有的规则具体实现
//规则1:判断服务可用性
public class ServiceAvailableRule extends BasicRule<UserPortrait, UserPortraitRuleContext> {
    @Override
    public boolean evaluate(UserPortraitRuleContext context) {
        TakeawayUserPortraitBasicInfo basicInfo = context.getBasicInfo();
        if (basicInfo.isServiceFail()) {
              return false;
        }
        return true;
    }
  
    @Override
    public void execute(UserPortraitRuleContext context) {}
​
}
//规则2:判断当前用户属性是否符合当前资源位投放的用户属性要求
public class UserGroupRule extends BasicRule<UserPortrait, UserPortraitRuleContext> {
    @Override
    public boolean evaluate(UserPortraitRuleContext context) {}
  
    @Override
    public void execute(UserPortraitRuleContext context) {
        UserPortrait userPortraitPO = context.getData();
        if(userPortraitPO.getUserGroup() == context.getBasicInfo().getUserGroup().code) {
          context.setValid(true);
        } else {
          context.setValid(false);
        }
    }
}
  
//规则3:判断当前用户是否在投放城市,具体逻辑省略
public class CityInfoRule extends BasicRule<UserPortrait, UserPortraitRuleContext> {}
//规则4:根据用户的活跃度进行资源过滤,具体逻辑省略
public class UserPortraitRule extends BasicRule<UserPortrait, UserPortraitRuleContext> {} 
​
//我们通过spring将这些规则串起来组成一个一个请求链
    <bean name="serviceAvailableRule" class="com.dianping.takeaway.ServiceAvailableRule"/>
    <bean name="userGroupValidRule" class="com.dianping.takeaway.UserGroupRule"/>
    <bean name="cityInfoValidRule" class="com.dianping.takeaway.CityInfoRule"/>
    <bean name="userPortraitRule" class="com.dianping.takeaway.UserPortraitRule"/>
      
    <util:list id="userPortraitRuleChain" value-type="com.dianping.takeaway.Rule">
        <ref bean="serviceAvailableRule"/>
        <ref bean="userGroupValidRule"/>
        <ref bean="cityInfoValidRule"/>
        <ref bean="userPortraitRule"/>
    </util:list>
      
//规则执行
public class DefaultRuleEngine{
    @Autowired
    List<BasicRule> userPortraitRuleChain;
​
    public void invokeAll(RuleContext ruleContext) {
        for(Rule rule : userPortraitRuleChain) {
            rule.evaluate(ruleContext)
        }
    }
}

责任链模式最重要的优点就是解耦,将客户端与处理者分开,客户端不需要了解是哪个处理者对事件进行处理,处理者也不需要知道处理的整个流程。在我们的系统中,后台的过滤规则会经常变动,规则和规则之间可能也会存在传递关系,通过责任链模式,我们将规则与规则分开,将规则与规则之间的传递关系通过Spring注入到List中,形成一个链的关系。当增加一个规则时,只需要实现BasicRule接口,然后将新增的规则按照顺序加入Spring中即可。当删除时,只需删除相关规则即可,不需要考虑代码的其他逻辑。从而显著地提高了代码的灵活性,提高了代码的开发效率,同时也保证了系统的稳定性。

四、总结

本文从营销业务出发,介绍了领域模型到代码工程之间的转化,从DDD引出了设计模式,详细介绍了工厂方法模式、策略模式、责任链模式以及状态模式这四种模式在营销业务中的具体实现。除了这四种模式以外,我们的代码工程中还大量使用了代理模式、单例模式、适配器模式等等,例如在我们对DDD防腐层的实现就使用了适配器模式,通过适配器模式屏蔽了业务逻辑与第三方服务的交互。因篇幅原因不再进行过多的阐述。

对于营销业务来说,业务策略多变导致需求多变是我们面临的主要问题。如何应对复杂多变的需求,是我们提炼领域模型和实现代码模型时必须要考虑的内容。DDD以及设计模式提供了一套相对完整的方法论帮助我们完成了领域建模及工程实现。其实,设计模式就像一面镜子,将领域模型映射到代码模型中,切实地提高代码的复用性、可扩展性,也提高了系统的可维护性。

当然,设计模式只是软件开发领域内多年来的经验总结,任何一个或简单或复杂的设计模式都会遵循上述的七大设计原则,只要大家真正理解了七大设计原则,设计模式对我们来说应该就不再是一件难事。但是,使用设计模式也不是要求我们循规蹈矩,只要我们的代码模型设计遵循了上述的七大原则,我们会发现原来我们的设计中就已经使用了某种设计模式。

五、参考资料

六、作者简介

吴亮亮,2017年加入美团外卖,美团外卖营销后台团队开发工程师。

招聘信息

美团外卖上海研发中心长期招聘前端、数据仓库、机器学习/数据挖掘算法工程师,欢迎感兴趣的同学发送简历到:tech@meituan.com(邮件标题注明:美团外卖-上海)

查看原文

赞 1 收藏 1 评论 0

maclxf 赞了文章 · 3月22日

请收好保存,写业务代码中的成长机会

默认文件1584590512532.png

写业务代码有成长机会吗?关于这个问题,答案非常肯定:必须有成长机会。对于大部分公司而言,能够写底层代码或者中间件代码的人总是有限的,写业务代码会面临更高的复杂度。

这里分三个层次来看其中的成长机会。

  • 第 1 个层次,让代码写得不一样。可从代码规范、可读性、可扩展性等角度着手,这也是程序员的基本功。
  • 第 2 个层次,考虑业务问题和技术问题的匹配。可从写业务代码中理解需求,- 并做好分析与设计。被动接收需求和实现接口,确实成长空间不大。
  • 第 3 个层次,总结相关方法体系,成为业务及技术双料专家。

下面通过例子分别针对这 3 个层次进行讲解。

让代码写的不一样

这里看一个例子,常见的代码如下:

这段代码其实可以写成这样:

考虑业务问题和技术问题的匹配

这里举一个烟囱型架构重构的例子。某团队在早期发展阶段对新来的每个业务都会搭建一套系统,业内人士将这种见招拆招、垂直化发展的架构称为「烟囱型架构」。烟囱型架构并非一无是处,在早期业务成败未知的情况下,不过度设计架构,能直接、有效地支持业务。

不过,随着业务的发展,「烟囱」越来越多,对这些「烟囱」的后续维护成为很大的问题,「成长」的烦恼如期而至。

其中存在的问题如下

  • 人手不够,业务响应慢了下来。 以一个 5 人研发团队为例,起初这个团队从 0 到 1 进行建设,5 个人花了 1 个月做出一个简单版的红包系统;几年后该团队增加到 10 个人,但手头要维护 10 个系统,每个人就要维护一个系统。这时又来了两个新业务,该团队派出 3 个人去做这两个新业务,大约要花 4 个月才能完成。这严重不符合前端业务的响应预期。
  • 重复建设。 在同类烟囱系统中有 80% 的功能是类似的,从数据库模型到主要业务逻辑都是复制粘贴加补丁,一不留神就又踩到一个坑。
  • 维护成本高。 面对日常升级包、咨询支持服务,该团队疲惫不堪。

那么,能不能将其中 80% 甚至 90% 的共性问题抽象出来呢?核心领域模型是否可以稳定呢?

在既要支持不断出现的各种业务,又要建设新平台的纠结中权衡之后,该团队首先启动了平台化项目,建设路径是存量业务继续使用烟囱架构,但新业务随着新平台一起接入,然后逐步迁移存量业务,实现烟囱系统的下线。如图所示,优惠券平台对前端业务提供统一的能力输出。

总结下来,平台化架构有如下好处。

  1. 快速支撑、响应业务。
  2. 抽象共性、边界清晰。快速支撑、响应业务是以终为始的出发点。

架构不服务业务,那么再高大上都是空话。技术不是炫技,要服务于商业。对于抽象共性的问题,业务平台化要解决业务的共性问题,比如天猫、淘宝都有各类营销活动,那就抽象出一个营销平台来管理营销活动、营销工具的整个生命周期,并提供给前端业务使用。

总结相关方法体系,成为业务及技术双料专家

举个例子,曾有一位做事非常努力、成长也比较快的小兄弟诉苦说,他之前在做网关程序,做得很细,除了完成了业务还保障了系统没有 Bug,还使文档沉淀、效率提升,外部机构联调合作方比如银行对其反馈也很好,但大家对其印象是善于合作,技术貌似一般。这位小兄弟在最近一年又做了业务平台,觉得自己在分析设计、中间件技术的应用上已经进步很大,甚至对自己的成长也很满意,但其他人还是认为其技术貌似一般。这位小兄弟百思不得其解。

笔者的反馈如下。

  1. 从量变到质变是有一个过程的,自我感觉总是良好的,外部感知可能会晚。如果坚持去做,「迟到的」一定会到。
  2. 大部分人都是固定型思维模式和成长型思维模式的混合模式,如果你身边的人固守对你的原有看法,不是你的错,你需要做的就是不断拓展自己的能力。

再举个例子,小郭在几年前参与了一个接入类业务,当时已经有不少机构接入了这个业务,业务规模不大不小,产品经理换了几茬,研发团队也变更了三次。当大家日复一日地做业务接入、测试、发布时,有人发现这个业务缺少一个业务视图。也就是说,这个业务有对系统资源的检测、对接口调用的监控,但是没有对业务运行情况的监控,看不到地区粒度、子业务粒度的变化情况等,甚至 BD 在签订新业务时都不了解为啥某地区已经没有调用量了。小郭利用业余时间开发了一个业务视图工具,整个团队马上就感受到他的过人之处了。有人讲,大公司不是应该什么都就绪吗?实际情况是,大公司也是经历了业务的高速发展的,可以这样说,大部分公司并不缺少做得更好、做得不一样的机会!

所以,「写业务代码有成长机会吗」这个句式还可以改为「维护老系统如何晋升」「做商户接入如何走向高端」和「项目这么忙如何学习」,我们需要进一步将自己的知识和经验系统化,形成相关的方法体系。

在心态上,笔者提两点建议:一是欣赏自己的成长,二是做个有心人。

感谢您的阅读和关注。

《程序员的三门课》读书笔记,作者:于君泽

主要是大前端技术以及程序员成长精进相关内容,文章首发于同名公众号,如果你想第一时间接收最新文章,那么可以扫码关注。如果对你有一点点帮助,可以点喜欢点赞点收藏,还可以小额打赏作者,以鼓励作者写出更多更好的文章。

查看原文

赞 8 收藏 5 评论 1

maclxf 收藏了文章 · 3月22日

请收好保存,写业务代码中的成长机会

默认文件1584590512532.png

写业务代码有成长机会吗?关于这个问题,答案非常肯定:必须有成长机会。对于大部分公司而言,能够写底层代码或者中间件代码的人总是有限的,写业务代码会面临更高的复杂度。

这里分三个层次来看其中的成长机会。

  • 第 1 个层次,让代码写得不一样。可从代码规范、可读性、可扩展性等角度着手,这也是程序员的基本功。
  • 第 2 个层次,考虑业务问题和技术问题的匹配。可从写业务代码中理解需求,- 并做好分析与设计。被动接收需求和实现接口,确实成长空间不大。
  • 第 3 个层次,总结相关方法体系,成为业务及技术双料专家。

下面通过例子分别针对这 3 个层次进行讲解。

让代码写的不一样

这里看一个例子,常见的代码如下:

这段代码其实可以写成这样:

考虑业务问题和技术问题的匹配

这里举一个烟囱型架构重构的例子。某团队在早期发展阶段对新来的每个业务都会搭建一套系统,业内人士将这种见招拆招、垂直化发展的架构称为「烟囱型架构」。烟囱型架构并非一无是处,在早期业务成败未知的情况下,不过度设计架构,能直接、有效地支持业务。

不过,随着业务的发展,「烟囱」越来越多,对这些「烟囱」的后续维护成为很大的问题,「成长」的烦恼如期而至。

其中存在的问题如下

  • 人手不够,业务响应慢了下来。 以一个 5 人研发团队为例,起初这个团队从 0 到 1 进行建设,5 个人花了 1 个月做出一个简单版的红包系统;几年后该团队增加到 10 个人,但手头要维护 10 个系统,每个人就要维护一个系统。这时又来了两个新业务,该团队派出 3 个人去做这两个新业务,大约要花 4 个月才能完成。这严重不符合前端业务的响应预期。
  • 重复建设。 在同类烟囱系统中有 80% 的功能是类似的,从数据库模型到主要业务逻辑都是复制粘贴加补丁,一不留神就又踩到一个坑。
  • 维护成本高。 面对日常升级包、咨询支持服务,该团队疲惫不堪。

那么,能不能将其中 80% 甚至 90% 的共性问题抽象出来呢?核心领域模型是否可以稳定呢?

在既要支持不断出现的各种业务,又要建设新平台的纠结中权衡之后,该团队首先启动了平台化项目,建设路径是存量业务继续使用烟囱架构,但新业务随着新平台一起接入,然后逐步迁移存量业务,实现烟囱系统的下线。如图所示,优惠券平台对前端业务提供统一的能力输出。

总结下来,平台化架构有如下好处。

  1. 快速支撑、响应业务。
  2. 抽象共性、边界清晰。快速支撑、响应业务是以终为始的出发点。

架构不服务业务,那么再高大上都是空话。技术不是炫技,要服务于商业。对于抽象共性的问题,业务平台化要解决业务的共性问题,比如天猫、淘宝都有各类营销活动,那就抽象出一个营销平台来管理营销活动、营销工具的整个生命周期,并提供给前端业务使用。

总结相关方法体系,成为业务及技术双料专家

举个例子,曾有一位做事非常努力、成长也比较快的小兄弟诉苦说,他之前在做网关程序,做得很细,除了完成了业务还保障了系统没有 Bug,还使文档沉淀、效率提升,外部机构联调合作方比如银行对其反馈也很好,但大家对其印象是善于合作,技术貌似一般。这位小兄弟在最近一年又做了业务平台,觉得自己在分析设计、中间件技术的应用上已经进步很大,甚至对自己的成长也很满意,但其他人还是认为其技术貌似一般。这位小兄弟百思不得其解。

笔者的反馈如下。

  1. 从量变到质变是有一个过程的,自我感觉总是良好的,外部感知可能会晚。如果坚持去做,「迟到的」一定会到。
  2. 大部分人都是固定型思维模式和成长型思维模式的混合模式,如果你身边的人固守对你的原有看法,不是你的错,你需要做的就是不断拓展自己的能力。

再举个例子,小郭在几年前参与了一个接入类业务,当时已经有不少机构接入了这个业务,业务规模不大不小,产品经理换了几茬,研发团队也变更了三次。当大家日复一日地做业务接入、测试、发布时,有人发现这个业务缺少一个业务视图。也就是说,这个业务有对系统资源的检测、对接口调用的监控,但是没有对业务运行情况的监控,看不到地区粒度、子业务粒度的变化情况等,甚至 BD 在签订新业务时都不了解为啥某地区已经没有调用量了。小郭利用业余时间开发了一个业务视图工具,整个团队马上就感受到他的过人之处了。有人讲,大公司不是应该什么都就绪吗?实际情况是,大公司也是经历了业务的高速发展的,可以这样说,大部分公司并不缺少做得更好、做得不一样的机会!

所以,「写业务代码有成长机会吗」这个句式还可以改为「维护老系统如何晋升」「做商户接入如何走向高端」和「项目这么忙如何学习」,我们需要进一步将自己的知识和经验系统化,形成相关的方法体系。

在心态上,笔者提两点建议:一是欣赏自己的成长,二是做个有心人。

感谢您的阅读和关注。

《程序员的三门课》读书笔记,作者:于君泽

主要是大前端技术以及程序员成长精进相关内容,文章首发于同名公众号,如果你想第一时间接收最新文章,那么可以扫码关注。如果对你有一点点帮助,可以点喜欢点赞点收藏,还可以小额打赏作者,以鼓励作者写出更多更好的文章。

查看原文

认证与成就

  • 获得 6 次点赞
  • 获得 24 枚徽章 获得 0 枚金徽章, 获得 3 枚银徽章, 获得 21 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-05-12
个人主页被 639 人浏览