Git Product home page Git Product logo

blog's Introduction

blog

Blog about programming and product design

blog's People

Contributors

frankcai4real avatar

Stargazers

 avatar

Watchers

 avatar  avatar

blog's Issues

一次预发布搭建引发的redis故障-shutdown源码分析

故障描述

前情提要

我们需要部署一台预发布机器,预发布机器和正式环境机器是在同一个局域网内,因为预发布机器需要和正式环境保持一致,故运维直接把正式机的redis.conf直接拷贝到预发布机器,然后使用systemd进行重启。

systemctl restart redis

该命令执行后,直接影响到正式环境的redis服务,redis被关闭了。

原因

查看systemd具体会执行什么脚本

systemdctl status redis
结果如下:
● redis.service - LSB: start and stop Redis server
   Loaded: loaded (/etc/rc.d/init.d/redis; bad; vendor preset: disabled)
   Active: active (running) since Tue 2020-05-19 02:12:41 UTC; 1 weeks 0 days ago
     Docs: man:systemd-sysv-generator(8)
  Process: 28308 ExecStart=/etc/rc.d/init.d/redis start (code=exited, status=0/SUCCESS)
   Memory: 4.4M
   CGroup: /system.slice/redis.service
           └─28318 /usr/bin/redis-server 127.0.0.1:6379
打开/etc/rc.d/init.d/redis脚本可以看到shutdown会执行的脚本:
shut="/usr/libexec/redis-shutdown"

打开/usr/libexec/redis-shutdown可以看到:

#!/bin/bash
#
# Wrapper to close properly redis and sentinel
test x"$REDIS_DEBUG" != x && set -x

REDIS_CLI=/usr/bin/redis-cli

# Retrieve service name
SERVICE_NAME="$1"
if [ -z "$SERVICE_NAME" ]; then
   SERVICE_NAME=redis
fi

# Get the proper config file based on service name
CONFIG_FILE="/etc/$SERVICE_NAME.conf"

# Use awk to retrieve host, port from config file
HOST=`awk '/^[[:blank:]]*bind/ { print $2 }' $CONFIG_FILE | tail -n1`
PORT=`awk '/^[[:blank:]]*port/ { print $2 }' $CONFIG_FILE | tail -n1`
PASS=`awk '/^[[:blank:]]*requirepass/ { print $2 }' $CONFIG_FILE | tail -n1`
SOCK=`awk '/^[[:blank:]]*unixsocket\s/ { print $2 }' $CONFIG_FILE | tail -n1`

# Just in case, use default host, port
HOST=${HOST:-127.0.0.1}
if [ "$SERVICE_NAME" = redis ]; then
    PORT=${PORT:-6379}
else
    PORT=${PORT:-26739}
fi

# Setup additional parameters
# e.g password-protected redis instances
[ -z "$PASS"  ] || ADDITIONAL_PARAMS="-a $PASS"

# shutdown the service properly
if [ -e "$SOCK" ] ; then
        $REDIS_CLI -s $SOCK $ADDITIONAL_PARAMS shutdown
else
        $REDIS_CLI -h $HOST -p $PORT $ADDITIONAL_PARAMS shutdown
fi

脚本会去拿配置文件bind的ip去连接redis服务,因为配置是直接从正式环境拿过来的,第一个ip正是正式环境reids的ip,然后发送shutdown命令,于是正式环境shutdown。
发送指令后,脚本会去检查redis进程是否还存在,如果存在则发送kill命令,因此本地reids也同时关闭。

为啥会踩坑

  1. 预发布机器刚好和正式机在同一个局域网内,于是通过内网ip是可以访问redis的
  2. redis.conf中bind的配置第一个ip竟然不是127.0.0.1而是内网ip,如果是127.0.0.1那么也不会访问到正式机机器
  3. 是直接拷贝正式环境的reids.conf所以密码也是一致的
  4. redis的shutdow是通过redis-cli连接到服务端发送关闭命令的redis-cli -h $host -p $port shutdown(如果失败则会在本地发送kill进程命令)。

吐槽点: 为什么redis的shutdown是通过客户端连接向server发送shutdown命令? 理论上我是一个普通用户,我不应该用kill,但是通过客户端连接的方式也不太对,我一个服务,每个连接的客户端都可以发送shutdown命令,那岂不很危险。这些命令应该要允许禁调。
通过改变名字来禁止:
rename-command CONFIG CONFIG_b9fc8327c4dee7
rename-command SHUTDOWN SHUTDOWN_b9fc8327c4dee7
rename-command FLUSHDB "" #禁用此命令
rename-command FLUSHALL "" #禁用此命令

如何避免

如果需要填写redis.conf中bind的配置第一个请尽量填写127.0.0.1

systemd

在上面的脚本中(/usr/libexec/redis-shutdown)我们看到它在发送关闭命令后会去检测进程是否还存在。那么如果我每个服务都要写一套这样的逻辑,不就很麻烦,为什么没人把这种东西抽离出来?有的,就是它,systemd。systemd的介绍可以看如下:
http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-commands.html
http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-part-two.html

redis的systemd脚本

当使用systemd去操作redis,systemd会去对应的目录寻找redis.service脚本寻找对应需要执行的脚本。
ExecStop=/usr/libexec/redis-shutdown

#!/bin/bash
#
# Wrapper to close properly redis and sentinel
test x"$REDIS_DEBUG" != x && set -x

REDIS_CLI=/usr/bin/redis-cli

# Retrieve service name
SERVICE_NAME="$1"
if [ -z "$SERVICE_NAME" ]; then
   SERVICE_NAME=redis
fi

# Get the proper config file based on service name
CONFIG_FILE="/etc/$SERVICE_NAME.conf"

# Use awk to retrieve host, port from config file
HOST=`awk '/^[[:blank:]]*bind/ { print $2 }' $CONFIG_FILE | tail -n1`
PORT=`awk '/^[[:blank:]]*port/ { print $2 }' $CONFIG_FILE | tail -n1`
PASS=`awk '/^[[:blank:]]*requirepass/ { print $2 }' $CONFIG_FILE | tail -n1`
SOCK=`awk '/^[[:blank:]]*unixsocket\s/ { print $2 }' $CONFIG_FILE | tail -n1`

# Just in case, use default host, port
HOST=${HOST:-127.0.0.1}
if [ "$SERVICE_NAME" = redis ]; then
    PORT=${PORT:-6379}
else
    PORT=${PORT:-26739}
fi

# Setup additional parameters
# e.g password-protected redis instances
[ -z "$PASS"  ] || ADDITIONAL_PARAMS="-a $PASS"

# shutdown the service properly
if [ -e "$SOCK" ] ; then
        $REDIS_CLI -s $SOCK $ADDITIONAL_PARAMS shutdown
else
        $REDIS_CLI -h $HOST -p $PORT $ADDITIONAL_PARAMS shutdown
fi

我们可以看到这个shutdown脚本和上面的(service的方式)差不多,唯一不同的是这里没有kill进程的逻辑,其实是因为systemd帮我们做了这一步了,不需要每个服务都要去写kill进程的逻辑。
我们可以看看systemd对应的源码(文件地址:/src/core/service.c):

static void service_enter_stop(Service *s, ServiceResult f) {
        int r;

        assert(s);

        if (s->result == SERVICE_SUCCESS)
                s->result = f;

        service_unwatch_control_pid(s);
        (void) unit_enqueue_rewatch_pids(UNIT(s));

        s->control_command = s->exec_command[SERVICE_EXEC_STOP];//这里会去执行stop命令对应的脚本
        if (s->control_command) {
                s->control_command_id = SERVICE_EXEC_STOP;

                r = service_spawn(s,
                                  s->control_command,
                                  s->timeout_stop_usec,
                                  EXEC_APPLY_SANDBOXING|EXEC_APPLY_CHROOT|EXEC_IS_CONTROL|EXEC_SETENV_RESULT|EXEC_CONTROL_CGROUP,
                                  &s->control_pid);
                if (r < 0)//检测服务进程是否还存在,存在的话则关闭失败则goto fail
                        goto fail;

                service_set_state(s, SERVICE_STOP);
        } else
                service_enter_signal(s, SERVICE_STOP_SIGTERM, SERVICE_SUCCESS);

        return;

fail://通过脚本关闭服务失败,则通过发kill信号到进程
        log_unit_warning_errno(UNIT(s), r, "Failed to run 'stop' task: %m");
        service_enter_signal(s, SERVICE_STOP_SIGTERM, SERVICE_FAILURE_RESOURCES);
}

rpc选型

分享 & 讨论

1.rpc是什么

  1. 1981年Bruce Jay Nelson发表论文《Implementing Remote Procedure Calls》,是rpc概念的发明者。
    image
    http://web.eecs.umich.edu/~mosharaf/Readings/RPC.pdf

  2. 发明rpc的目的

    1. clean and simple semantics,干净、简单的语义
    2. efficiency,高效
    3. generality,通用
      image
  3. RPC(Remote Procedure Call)远程过程调用。在分布式环境中,程序员无需了解远程交互的细节,不需要关心调用的是本地程序还是远程程序。

  4. RPC是IPC的一种形式,IPC可能在一台或多台计算机上运行。

  5. RPC的核心模块
    image

//sample: 13年写的模块之间的远程调用(“rpc”雏形)
private function invoke($invokeParam)
    {
        $invokeInfo = serialize($invokeParam);
        $url = $this->url;
        $urlInfo = parse_url($url);
        $ssl = array_key_exists('scheme', $urlInfo) ? ($urlInfo['scheme'] == 'https') : false;
        $host = $urlInfo['host'];
        $port = array_key_exists('port', $urlInfo) ? $urlInfo['port'] : ($ssl ? 443 : 80);
        if ($this->proxy) {
            $proxyInfo = parse_url($this->proxy);
            $host = $proxyInfo['host'];
            $port = $proxyInfo['port'];
        }
        $query = array_key_exists('query', $urlInfo) ? $urlInfo['query'] : false;
        $requstHeaders = array(
            'Host: ' . $urlInfo['host'],
            'Content-Type: text/plain',
            'Content-Length: ' . strlen($invokeInfo)
        );
        if (is_callable('gzdecode')) {
            $requstHeaders[] = 'Accept-Encoding: compress, gzip';
        }
        $requestURI = (array_key_exists('path', $urlInfo) ? $urlInfo['path'] : '/') . ($query ? ('?' . $query) : '');
        $socket = fsockopen(($ssl ? 'ssl://' : '') . $host, $port, $errno, $errstr, 10);
        if ($errno == 0) {
            $request = "POST {$requestURI} HTTP/1.1\r\n";
            foreach ($requstHeaders as $header) {
                $request .= $header . "\r\n";
            }
            $request .= "\r\n";
            if ($invokeInfo) {
                $request .= $invokeInfo;
            }
            fwrite($socket, $request);

            $contentLength = 0;
            $response = '';
            $chunked = false;
            $zip = false;
            $line = trim(fgets($socket));
            if ($line) {
                list($protocol, $responseCode, $responseText) = explode(" ", $line);
                while (($line = trim(fgets($socket))) != "") {
                    if (strstr($line, "Content-Length:")) {
                        list($cl, $contentLength) = explode(" ", $line);
                    }
                    if (strstr($line, "Content-Type:")) {
                        $responseContentType = $line;
                    }
                    if (strstr($line, "Transfer-Encoding")) {
                        $chunked = strstr($line, "chunked");
                    }
                    if (strstr($line, "Content-Encoding")) {
                        $zip = strstr($line, "gzip");
                    }
                }
                if ($contentLength > 0) {
                    $response = stream_get_contents($socket, intval($contentLength));
                } else if ($chunked) {
                    $length = hexdec(fgets($socket));
                    while ($length > 0) {
                        $response .= stream_get_contents($socket, $length);
                        fgets($socket); // skip the \r\n of data block end
                        $length = hexdec(fgets($socket));
                    }
                }
                fclose($socket);
            }
        }

        if ($response) {
            return $zip ? unserialize(gzdecode($response)) : unserialize($response);
        } else {
            return false;
        }
    }

2.开源rpc的对比

star数 协议(默认的、主推的) 序列化方式 支持语言 作者 缺点 优点
gRPC 30k h2、gRPC Web Protocol Buffer,支持扩展其他序列化方式 多语言 google,17年加入CNCF基金会 1.官方没有提供PHP服务端 2. http协议相对更底层的私有协议不够高效 IDL、社区活跃更受欢迎、文档和example更齐全、生态更好(nginx、thrift等都会对grpc支持)
Thrift 8.2k 私有协议,Tsocket、TFramedTransport、TFileTransport、TMemory Transport、TZlibTransport Binary、Compact(使用zigzag、varint压缩编码)、json 多语言 fb开发,贡献给ASF IDL、支持的语言更丰富、时间长更成熟(vip osp在使用)
Tars 9k 私有协议 私有序列化方式https://github.com/TarsCloud/TarsTup 多语言 腾讯,捐赠给Linux Foundation IDL、带有服务治理
Motan 6k 私有协议,Motan协议 多语言 新浪微博
Dubbo 35k 私有协议,Dubbo协议 开始只有java,现已支持 Go, Node.js, Python, PHP, Erlang 阿里巴巴,捐赠给ASF
hprose 2k http,fpm模式、私有协议 开始只有PHP,现在支持多语言

2.1.性能对比

https://colobu.com/2020/01/21/benchmark-2019-spring-of-popular-rpc-frameworks/

3.gRPC

3.1.支持的协议

  1. gRPC over HTTP2(主要)
  2. gRPC web
    1. 提供了JavaScript客户端,可以让web应用直接访问gRPC服务进行通信,不需要经过http服务器

image
image

3.1.1.HTTP2

为什么gRPC会选择h2
  1. h2的前身就是google的SPDY,google本身对它的理解更深,并且h2已经经过了实践证明
  2. h2对于推广更有利,基本上流行的编程语言对http都有成熟的支持
  3. h2支持stream和流控
  4. h2对于webserver、proxy有很容易的支持
  5. h2天然支持ssl,如果是私有协议这需要自己去包,去解决http遇到过的所有问题
  6. 私有协议定制更强、更快,但是垄断性质存在,不够开放,生态做起来相对比较难
h2的特点
  1. h2的使用统计,https://w3techs.com/technologies/details/ce-http2
  2. 二进制协议,除了官方定义的十几种帧,可以定义额外的帧
  3. 多工。在一个tcp连接里,可以同时发送多个请求或回应,而不需要一一回应,避免“队头阻塞”
  4. 数据流。同一个tcp连接里面连续的包可能食欲不同的response
  5. 头信息压缩。除了压缩外,还维护一张头信息表,这样不需要每次请求都附带,浪费宽带
  6. server push。
    image

3.2.序列化方式-Protocol Buffers

  1. 与开发语言、平台无关
  2. 二进制传输格式
  3. 上下文和数据分离
  4. 使用varint编码,减少对空间的占用
    image
{“name”: "test", "age": 18}, {“name”: "test", "age": 19}
---->
{
	string name = 1;
	int32 age = 2;
}

3.2.1.var int编码

假如32位的整型存11(十进制),那么将会是00000000000000000000000000001011,那么前面的0都是浪费。
image

  1. varint的**就是,var+int,通过标志位来判断是否还有数据
  2. 如果都是大数字把32位都占满了,那么反而空间占用更多了,但是实际场景中小数字使用率远远大于大数字。
  3. code: github.com/golang/[email protected]/proto/buffer.go

3.3.通信方式

  1. Unary RPCs。这种模式最为传统,即客户端发起一次请求,服务端响应一个数据。
  2. Server streaming RPCs。这种模式是客户端发起一次请求,服务端返回一段连续的数据流。典型的例子就是客户端向服务端发送一个股票代码,服务端就把该股票的实时数据源源不断地返回给客户端。
  3. Client streaming RPCs。和上面的相反。典型的例子是,物联网终端向服务器上报数据
  4. Bidirectional RPCs。互相发送数据流,实时交互。典型的例子是聊天机器人。

image

3.4.为什么不支持PHP

  1. gRPC提供的代码有server的部分,但是只是用来实验,不建议投入生产环境

image

  1. PHP流行的是通过webserver+phpfpm这种方式
  2. webserver、fastcgi管理器不去做,不借助swoole、roadrunner的话是个单进程服务,有并发问题
解决方案
  1. 基于swoole的hyperf、mixPHP框架封装支持gRPC server端
  2. 基于RoadRunner的spiral/php-grpc https://github.com/spiral/php-grpc
  3. 自己去写(参考hyperf的实现)

3.5.选择比努力重要

  1. Thrift(07年)比gRPC(15年)更早更成熟,但是目前grpc更受欢迎
  2. 选择公有协议、语言中立、平台中立
  3. grpc选择http2那么它的性能肯定不会是最顶尖的,但是对于rpc来说中庸的qps可以接受,通用和兼容性才是最重要的事情。
  4. https://grpc.io/blog/principles/

4.go使用grpc

安装Protocol Buffer,https://grpc.io/docs/protoc-installation/

  1. 修改proto文件
    image
  2. 重新生成gRPC代码,protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative helloworld/helloworld.proto
  3. 相关接口已经自动生成,接下来实现接口

https://grpc.io/docs/languages/go/quickstart/
https://grpc.io/docs/languages/go/basics/

代码分层

背景

接手了一个电商的erp系统,业务逻辑复杂,为了提高代码的可读性、可维护性,应用分层很有必要。分层可以让我们专注上层业务,还可以让我们对代码进行分类。

生活中的其他分层

  1. OSI的7层网络模型
  2. TCP/IP协议四层模型
  3. MVC分层

一个好的应用分层需要具备

  1. 方便后续代码进行维护扩展
  2. 分层的效果需要让整个团队接受
  3. 各个层职边界清晰

现在的目录结构

erp  WEB部署目录
├─application           应用目录
│  ├─common             公共模块目录
│  │  ├─command         命令行定义目录
│  │  ├─controller      控制器目录
│  │  ├─exception       异常目录
│  │  ├─model           mysql目录
│  │  └─ ...            更多类库目录
│  │
│  ├─module_name        模块目录
│  │  ├─common.php      模块函数文件
│  │  ├─controller      控制器目录
│  │  ├─model           模型目录
│  │  ├─view            视图目录
│  │  └─ ...            更多类库目录
│  │
│  ├─command.php        命令行定义文件
│  ├─common.php         公共函数文件
│  └─tags.php           应用行为扩展定义文件

代码分层优化

参考阿里的代码分层,具体可以查看《阿里巴巴java开发手册》。

image

erp WEB部署目录
├─application 			应用目录
│ ├─common 				公共模块目录
│ │ ├─command 			命令行定义目录
│ │ ├─controller 		控制器目录
│ │ ├─exception 		异常目录
│ │ ├─model 			DAO层目录
│ │ ├─service			Service目录
│ │ ├─manager			Manager目录
│ │ └─ ... 				更多类库目录
│ │
│ ├─module_name 		模块目录
│ │ ├─common.php 		模块函数文件
│ │ ├─controller 		控制器目录
│ │ ├─model 			模型目录
│ │ ├─view 				视图目录
│ │ ├─service			Service目录
│ │ ├─manager			Manager目录
│ │ └─ ... 				更多类库目录
│ │
│ ├─command.php 		命令行定义文件
│ ├─common.php 			公共函数文件
│ └─tags.php 			应用行为扩展定义文件

分层规范

Controller层
规范:
  1. 命名规范:保持不变
  2. 不要在controller写业务
  3. 同层级不能互相调用
职责:
  1. 请求输入与输出的处理。其中包括:参数校验、BFF对返回数据的特殊处理。
  2. 转发与组装service。
Service层
规范:
  1. 命名规范:service结尾,如:XxxService
  2. 推荐一个controller对应一个service
  3. 不要在service远程调度,下沉到Manager层去处理
  4. 同层级不能互相调用
职责:
  1. 相对具体的业务逻辑服务层
  2. 与DAO层进行交互
Manager层
规范:
  1. 命名规范:XxxManager
  2. 考虑复用
  3. 同层级不能互相调用
职责:
  1. 对service层通用能力的下沉,如缓存方案、中间件通用处理
  2. 与DAO层交互,对多个DAO的组合复用
  3. 对第三方平台封装的层,预处理返回结果及转化异常信息
DAO层(也就是目前的Model)相对具体的业务逻辑服务层
规范:
  1. 命名规范:保持不变
职责:
  1. 数据访问层,与底层Mysql、Oracle、Hbase等进行数据交互

分层也有缺点

  1. 增加代码的复杂度
  2. 如果每个层次单独部署的话可能会增加网络消耗

优化的关键就是对不同组件的责任进行划分

虽然只有四层结构,但是在结构里面可以继续划分。例如:

  1. 验证器
  2. 中间件
  3. exception

善用TP本身提供的能力

  1. 依赖注入(IOC控制反转)
  2. Facade

讨论

  1. 对分层有什么建议,以及规范的建议
  2. rest风格
  3. migration、数据库字典
  4. A service 需要调用B service,怎么办?
    • 抽离到manager
    • 在controller对service进行组装
  5. 特殊情况:错误时候需要data返回给前端,异常机制可以支持吗
    • 异常可以改造支持,但是成本比较大
    • 这种特殊直接在controller进行特殊处理

类使用的配置应该写在哪里

1.前文

很多时候我们的类需要一些配置数据,例如:充值渠道对接的类、第三方登陆对接的类需要拿到渠道商提供的验证url、商户ID、加解密所需要密钥等等。重构项目时候发现了很多不一样的写法,这些写法间有什么优缺点呢?

2.写法

2.1.直接写在类常量

image

2.1.1.优点
  1. code review非常直观,类所需要的数据都在类文件里面
2.1.2.缺点
  1. PHP7前常量只支持标量类型(PHP7后还支持复合类型中的数组)
  2. 严格规范来说,外部的配置数据不应该设置为类属性,因为它不是属于类的一部分,只是类会使用到的数据
  3. 对于守护进程或者静态语言,修改这些配置就是要修改到类文件,也就是说我们需要重启或者重新编译

2.2.直接写在类变量然后使用构造函数初始化

image

2.2.1.优点
  1. code review非常直观,类所需要的数据都在类文件里面
  2. 相对类常量来说,支持的数据类型更加丰富以及灵活
2.2.2.缺点
  1. 变量可能会在使用过程中被改变
  2. 严格规范来说,外部的配置数据不应该设置为类属性,因为它不是属于类的一部分,只是类会使用到的数据
  3. 对于守护进程或者静态语言,修改这些配置就是要修改到类文件,也就是说我们需要重启或者重新编译
  4. 构造函数每次初始化类都会执行一次,而配置不一定是每个类方法都会使用到

2.3.放到外部的配置目录

image

2.3.1.优点
  1. 结构灵活、相对常量类型支持丰富
  2. 对配置进行脱敏处理比较方便
  3. 对于守护进程或者静态语言,修改这些配置就是要修改到类文件,可以不需要重启或者重新编译
  4. 方便支持多份配置,代码结构清晰
  5. 还可以优化成把配置放到缓存里面
2.3.2.缺点
  1. code review相对上面两种来说不够直观

3.硬编码(hard code)

wiki的解释:

硬编码(hard code或hard coding)是指在软件实现上,将输出或输入的相关参数(例如:路径、输出的形式或格式)直接以常量的方式撰写在源代码中,而非在运行期间由外界指定的设置、资源、数据或格式做出适当回应。一般被认定是反模式或不完美的实现,因为软件受到输入数据或输出格式的改变就必须修改源代码,对客户而言,改变源代码之外的小设置也许还比较容易。
但硬编码的状况也并非完全只有缺陷,因某些封装需要或软件本身的保护措施,有时是必要的手段。除此之外,有时候因应某些特殊的需求,制作成简单的应用程序,应用程序可能只会运行一次,或者永远只应付一种需求,利用硬编码来缩短开发的时间也是一种不错的决策。
其实写在类的属性中,从类的角度来看,类已经把这些数据抽离出来类属性了,不算hard code。但是在整个系统的角度来说,这也属于一种hard code,需要更改配置的时候就需要更改源代码。

4.总结

从以上角度来看,使用配置文件的方式比较优的。

我们会看到每一种方案都有优缺点,PHP是一门比较灵活的语言,因此大家写法会很多。

pear、pecl、composer

前言

对于PHP项目开发来说,现在composer已经足够流行了,所以我们都比较了解,但是pear、pecl,你未必知道它是什么。其实他们都是PHP开发演化的产物,优胜劣汰,谁更适合这个环境谁就能生存下来,这是这是自然规律。

1.概念

1.1.pear

PEAR是PHP Extension and Application Repository的缩写,即php扩展和应用仓库。
PEAR将php程序开发过程中常用的功能编写成类库,涵盖了页面呈现、数据库访问、文件操作、数据结构、缓存操作、网络协议、webservice等许多方面,用户可以通过下载这些类库并适当的做一些定制以实现自己需要的功能。避免重复造车轮,PEAR的出现大大提高了php程序的开发效率和开发质量。pear package以phar、tar或zip发布。
后面还有pear2,是一代的pear代码仓库。
地址: https://pear.php.net/index.php

1.2.pecl

PECL是PHP Extension Community Library的缩写,即PHP扩展库。
PECL可以看作PEAR的一个组成部分,提供了与PEAR类似的功能。不同的是PEAR的所有扩展都是用纯粹的PHP代码编写的,用户在下载到PEAR扩展以后可以直接将扩展的代码包含到自己的PHP文件中使用。而PECL是使用C语言开发的,通常用于补充一些用PHP难以完成的底层功能,往往需要重新编译或者在配置文件中设置后才能在代码中使用。
通俗的表述是,PEAR是PHP的上层扩展,PECL是php的底层扩展。它们都是为特定的应用提供现成的函数或者类。
地址: http://pecl.php.net/

1.3.composer

composer是php的包管理工具,优点在于仅需要提供一个composer.json文件,申明需要用到的第三方库,一个简单的命令就能将其依赖全部装好,也方便项目的部署和发布。还提供了自动加载的支持。(PSR-0规范)
地址: https://getcomposer.org/

2.演化进程和区别

2.1.发布时间轴

pear(1999)-> pecl(2004)-> pear2(2009)-> composer(2012)

2.2.区别

composer和pear功能是一样的,但是composer更方便好用,pear差不多被淘汰了。如果用扩展的就pecl,上层包的就用composer。

REST设计风格

REST设计风格

1.背景

  1. Roy Thomas Fielding在2000年的博士论文中提出rest架构风格。
  2. 作者参与了http/1.0的设计,并且是apache http server的共同创始人。认识到需要为万维网如何工作提供一个模型,这个模型就是rest。rest架构旨在提高组件的独立性和伸缩性和置换性。
  3. rest不是规范不是协议,是一种架构风格。

2.概念

REST是REpresentational State Transfer的首字母缩写。它是分布式超媒体系统的架构风格(an Internet-scale distributed hypermedia system)。

2.1.论文中的描述

The name "Representational State Transfer" is intended to evoke an image of how a well-designed Web application behaves: a network of web pages(a virtual state-machine), where the user progresses through the application by selecting links(state transitions), resulting in the next page(representing the next state of the application) being transferred to the user and rendered for their use.

The World Wide Web has succeeded in large part because its software architecture has been designed to meet the needs of an Internet-scale distributed hypermedia system. The Web has been iteratively developed over the past ten years through a series of modifications to the standards that define its architecture. In order to identify those aspects of the Web that needed improvement and avoid undesirable modifications, a model for the modern Web architecture was needed to guide its design, definition, and deployment

论文最后,作者这样写到:rest架构试图为现代web架构提供一个指导原则,它试图最小化网络延迟,同时最大化组件的独立性和可伸缩性,以满足分布式超媒体系统对于可伸缩网络的需要。

REST is a coordinated set of architectural constraints that attempts to minimize latency and network communication while at the same time maximizing the independence and scalability of component implementations. This is achieved by placing constraints on connector semantics where other styles have focused on component semantics. REST enables the caching and reuse of interactions, dynamic substitutability of components, and processing of actions by intermediaries, thereby meeting the needs of an Internet-scale distributed hypermedia system.)

2.2.具体的理解(约束4:统一接口的要求)

在wiki上有一段关于Figurative art(具体艺术)的定义:

Figurative art, sometimes written as figurativism, describes artwork—particularly paintings and sculptures—that is clearly derived from real object sources, and are therefore by definition representational。

具象艺术,有时被写成具象主义,它所描述的艺术品--尤其是绘画和雕塑--是由真实对象派生而来的,他是通过表征定义的。
由此可知,表征指的就是一种描述方式,它准确定义了真实存在的事物,在具象艺术里,我们通过绘画和雕塑来定义真实的物体。这种描述就是一种表征,它可以被用来定义真实存在的事物,是一种“可重复的表象”,因为其被记录了下来。
在计算机领域“Representational”表述或表征的对象是一种资源,这里的资源具体一点可以指图片、视频、数据、库表字段等等。那么这种表述或表征就是定义这些资源的方式,说到这里恍然大悟,具体一点,就是json、xml等这些描述资源的东西。representational是个大概念,它包括一切的这些json、xml的子集。

  1. 资源(resource): REST的名称“表现层状态转化”中,省略了主语。“表现层”其实指的是“资源”(Resources)的“表现层”。所谓“资源”,就是网络上的一个实体,或者说网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的存在。你可以用一个URI(统一资源定位符)指向它,每种资源对应一个特定的URI。
    1. 资源必须是名词,URI应该只有名词,不应该有动词(当然,有时候我们会把动词是当做名词来用)。
    2. 如果遇到一些场景是crud所抽象不了的,例如用户登录,如何去匹配crud? 可以先把你的操作对象或者行为抽象为资源。例如登录这个场景,我们可以把用户在服务器的会话信息抽象为一个资源。这样的话登录就是在远程服务器增加一个会话资源,登出就是 删除一个会话资源。
    3. 如果某些动作是协议动词(通常是http的method)表示不了的,你就应该把动作做成一种资源。比如网上汇款,transfer改成名词transaction,资源不能是动词,但是可以是一种服务。
    4. 复数url。既然URL是名词,那么应该使用复数还是单数? 这没有一个统一的规定,但是常见的操作是操作一个集合,为了统一起见,建议都使用复数URL。比如GET /articles/2要好于GET /article/2
    5. 避免多级URL。常见的情况是,资源需要多级分类,因此很容易写出很多级的URL,应该适当使用参数代替。
  2. 表现层(Representational): “资源”是一种信息实体,它可以有多种外在表现形式。我们把“资源”具体呈现出来的形式,叫做它的“表现层”(Representational)。URI只代表资源的实体,不代表它的形式。严格地说,有些网址最后的“.html”后缀名是不必要的,因为这个后缀名表示格式,属于“表现层”范畴,而URI应该只代表“资源”的位置。例如http中,它的具体表现形式应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段才是对“表现层”的描述。(客户端和服务器之间,传递的就是资源的某种表现层)
  3. 状态转化(State Transfer): 访问一个网站,就代表客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。如果客户端想要操作服务器,必须通过某种手段,让服务器发生“状态转化”(State Transfer)。而这种转化是建立在表现层之上的,所以就是“表现层状态转化”。例如HTTP协议中:四个比较常用的操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源、POST用来新建资源、PUT用来更新资源、DELETE用来删除资源。(客户端通过HTTP动词,对服务器资源进行操作,实现“表现层状态转化”)
    1. http动词有5个:
      1. GET(SELECT):从服务器取出资源
      2. POST(CREATE):在服务器新建一个资源
      3. PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)
      4. PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)
      5. DELETE(DELETE):在服务器删除资源
    2. 还有2个不常用的http动词:
      1. HEAD:获取资源的元数据
      2. OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。
  4. 回想作者对于"Representational State Transfer"的描述:旨在提醒人们基于网络的应用最好的设计就是:“在一个web页面中,用户点击一个链接,然后为用户返回下一个页面”。这个概念是万维网的本质,万维网之所以成功就在于通过link(链接)将文档链接。那么试想是不是我们对于资源(图片、文本、json、xml...)的获取,也应该与link(链接)的初衷更加契合,这样会使得我们系统的架构设计更加符合网络应用的可伸缩的需求。在REST中这个link(链接)就是rest api的uri地址,这样的设计使得互联网上的所有资源更像万维网中的link(链接),你只要点击它就会获得新的资源,更加契合万维网的本质。

3.六个体系结构约束(原则)

3.1.客户端-服务器体系结构(client-server)

允许双方独立发展。数据模块和显示模块分开(前后端分离),提高可移植性、提高伸缩性。

3.2.无状态(stateless)

客户端负责维护上下文,服务端不负责。请求本身包含处理该请求所需要的状态,并且服务端不存储与会话相关的任何内容。每次请求都是独立的,服务端不会帮助客户端保存上下文(e.g., 用户的会话信息使用jwt会比维护session信息更优, 客户端不能告诉服务端需要下一页而是精确的告诉服务端第几页)。

3.3.缓存(cacheable)

要求将对请求的响应中的数据隐式或显式标记为可缓存或不可缓存。如果响应是可缓存的,则客户端缓存有权重用该响应数据以用于以后的等效请求。

3.4.统一接口(uniform interface)

要求接口的通用性,一旦开发人员熟悉你的某个api,他就应该能够对其他api采用类似的方法。无论设备或应用类型如何,都可以采用统一的方式和给定的服务端进行交互,并且接口满足一下4个约束(以下通过http协议的场景来介绍):

  1. identification of resources(标识资源),通过uri唯一标识资源
  2. manipulation of resources through representations,操作资源的方式通过http post、get...(一个典型的rest服务不需要额外的文档对如何操作资源进行说明)
  3. self-descriptive messages(自描述信息)客户端请求需要告诉接受什么样的格式,是否缓存等,能够提供自身如何被处理的足够信息
  4. hypermedia as the engine of application state(超媒体是应用表述的核心。作者表示很关键,但是大众却没有重视HATEOAS)。通过结果就可以得到下一步操作所需要的信息。(一个典型的rest服务不需要额外的文档标示通过哪些url访问特定类型的资源,而是通过服务端返回的响应来标示到底能在该资源上执行什么样的操作)

3.5.分层设计(layered system)

通过约束组件行为来使体系结构由分层组成,这样每个组件都不能“看到”超出与它们交互的直接层。e.g., 客户端访问服务器时候,并不知道它连接的事终端服务器还是沿途的负责转发的服务器,后端服务器可能会根据不同功能划分成多层。

3.6.按需编码(code on demand(optional))

rest允许通过小程序和脚本的形式下载和执行代码来扩展客户端功能。通过减少预先实现所需的功能数量来简化客户端。

4.示例与比较

4.1.满足第1, 2约束

我们现在大部分的架构都是负责REST架构的,客户端(IOS、Android、Angular...)与数据业务(Business Logic、Service)分离,这符合client-server。不像旧时代都挤在一块没有分离。

4.2.满足第4约束

4.2.1.example1

[POST]/users 新增用户
[PUT]/users/id 修改用户
[DELETE]/users/id 删除用户
[GET]/users 查找用户
问:我平时都是get、post走天下的,也用的好好的呀,新增用户为什么不能/addUser,删除用户为什么不能/deleteUser,感觉也很清晰呀?
答:url的定义是:统一资源定位符。也就是说url是用来表示资源在互联网上的位置的,所以url不应该包含动词,只能包含名词。对资源的操作应该体现在http method上面。你可以用deleteUser也可以用removeUser等等,具体含义只有设计api的人才能说清楚了。而使用DELETE这个方法,看到这个api就知道提供的是删除这个资源的方法,这就叫做语义化。

4.2.2.example2

在jane的网站有一张小汽车的图片,地址是http://jane.com/img/car.jpg 现在想设计一个api,对这张图的删除。
[DELETE]http://jane.com/img/car
问:为什么没加资源的后缀.jpg?
答:严格地说,有些网址最后的".html"后缀名是不必要的,因为这个后缀名表示格式,术语"变现层"范畴,而url应该只代表"资源"的位置。它的具体表现形式,应该在http请求头的信息中用Accept和Content-Type字段指定,这两个字段才是对"表现层"的描述。所以这个例子里,我们可以通过在http header里指定content-type为image/jpeg来申明这个资源是一张jpg格式图片。

4.2.3.其他:

参考github提供的rest api:https://docs.github.com/cn/rest

4.2.4.特殊情况

  1. api设计不可能一帆风顺,总有一些场景是crud所抽象不了的,例如用户登录,如果去匹配crud,这里的建议是,先把你的操作对象或者行为抽象为资源,然后就简单了,无非就是对这个资源的crud。我们可以把用户在远程服务器的会话信息抽象为一个资源,这样的话登录就是增加一个会话资源,退出就是删除一个会话资源。所以可以设计成:[POST]/login [DELETE]/logout
  2. 如果是发短息呢?似乎更难。如果某些动作是http动词表示不了的,你就应该把动作做成一种资源,比如网上汇款,从账户1向账户2汇款,正确的写法是把动词transfer改成名词transaction,资源不能是动词,但是可以是一种服务。如果你把发短信理解成一种服务,api可以设计为:[POST] /smsService body:{"mobile":"111111111", "text":"hello world"}
  3. 如果url太长,应该通过参数来过滤。过滤信息:
    1. ?limit=10,指定返回记录的数量
    2. ?offset=10,指定返回记录的开始位置
    3. ?page=2&per_page=100,指定第几页,以及每页的记录数
    4. ?soryby=name&order=asc,指定返回结果按照哪个属性排序,以及排序顺序
    5. ?animal_type_id=1,指定筛选条件

4.3.rest和restful的关系

rest是一种架构设计风格。restful是rest的形容词(就像helpful、useful),restful api指的是满足rest原则(6个约束)的接口。

4.4.rest与http、uri的关系

  1. rest和http属于不同层面的东西,不应该进行比较。。但是由于rest打算使web更加简化和标准化,他主张更严格的使用rest原则,这就是人们试图开始把http和rest比较的原因。
  2. 作者并没有在论文中提到任何实现指令(例如用什么协议),不管是什么协议,只要遵循rest的6个原则,那么接口就是restful
  3. 作者在论文第六节有提到,www万维网就是rest的最好实践,http和uri就是rest的实践。也就是说rest独立于任何基础协议,不一定是http,但是http是实现rest的最常见的协议。
  4. 关系总结:rest使用http作为各组件的交流协议,使用uri作为资源定位,数据的组织形式可以采用流行的格式:json、xml等。

4.5.优缺点

4.5.1.优点

  1. 客户端和服务端的解耦:rest提供更好的抽象性,具有抽象级别的系统能够封装其实现细节,以更好的标示和维持它的属性。这使得api足够灵活,可以随着时间的推移而发展,同时保持稳定的系统。
  2. 可发现性:客户端和服务端之间的通信描述了所有内容,因此不需要外部文档即可了解如何与rest api进行交互。
  3. 缓存友好:rest重用了许多http自带的概念,在http层面缓存数据非常方便。(例如,我对get方法直接缓存就好了)
  4. 使用rest能带来的额外的好处,你可以很方便的权限控制。因为POST、PUT、DELETE、GET等都是标准的http方法,你可以很轻松的在nginx这样的7层代理或者防火墙上设置策略,禁止某些资源的修改及删除操作。而这很显然是自定义的url所达不到的。
  5. 虽然rest本身受web技术的影响很深,但是理论上rest架构风格并不是绑定在http上,只不过目前http是唯一与rest相关的实例。

4.5.2.缺点

  1. 没有标准的rest结构:没有具体的正确的方法,如何对资源进行建模取决于不同情况。所以rest在理论上简单,但在实践中困难
  2. 庞大的负载:rest会返回大量丰富的元数据。但是带宽容量并非总是足够的。这也是facebook在2012年提出GraphQL架构风格的关键驱动因素。
  3. 响应过度和响应不足。rest的响应包含的数据会过多或不足,通常会导致客户端需要发送多几个请求,或者新做一个接口合并这些数据。(1. 这也是GraphQL要解决的问题 2.我们后端经常要求客户端发送多个请求,其实在这种模式下这个工作要么前端做要么后端做的,其实尽量后端做吧,在controller做好封装就行,这也可以保证controller以下的函数的独立性又能保证前端省点工作。后来我们这样controller这样的工作被称为BFF,Backend For Frontend)

5.HATEOAS(非HATEOAS的系统不能称为restful)

HATEOAS是Hypermedia As The Engine Of Application State的缩写,从字面理解是“超媒体即是应用状态引擎”。

  1. hypermedia(超媒体)是包括超文本的,超文本特有的优势是拥有超链接(hyperlink)。如果我们把超链接引入到多媒体当中去,那就得到了超媒体,因此关键角色还是超链接。
  2. 使用超媒体作为应用引擎状态,意思是,应用引擎的状态变更由客户端访问不同的超媒体资源驱动。
  3. 其原则就是客户端与服务端的交互完全由超媒体动态提供,客户端无需事先了解如何与数据或者服务器交互。(相反的一些rpc服务或者一些软件都是需要事先了解接口定义或者特定的交互语法)
  4. 它是构建成熟rest服务的核心。它的重要性在于打破了客户端和服务器之间严格的契约,使得客户端可以更加智能和自适应。
  5. 从Richardson提出的成熟度模型的标准看来,使用HATEOAS是成熟度最高的rest架构风格,也是推荐的做法。

rest的设计者Roy Thomas Fielding在博客强调,非HATEOAS的系统不能称为restful(HATEOAS不是选项,如果没有实现则不是在做rest)

  1. rest api不能定义固定的资源名称或者层次关系
  2. 使用rest api应该只需要知道初始uri(书签)和一系列针对目标用户的标准媒体类型

5.1.例子

通过实现HATEOAS,每个资源能够描述针对自己的操作资源,动态的控制客户端,即便更改了url也不会破坏客户端。

GET /account/12345 HTTP/1.1
Host: somebank.org
Accept: application/xml
...

//将会返回
HTTP/1.1 200 OK
Content-Type: application/xml
Content-Length: ...
<?xml version="1.0"?>
<account>
  <account_number>12345</account_number>
  <balance currency="usd">100.00</balance>
   <link rel="deposit" href="https://somebank.org/account/12345/deposit">
   <link rel="withdraw" href="https://somebank.org/account/12345/withdraw">
   <link rel="transfer" href="https://somebank.org/account/12345/transfer">
   <link rel="close" href="https://somebank.org/account/12345/close">
</account>

//返回的body不仅包含了账号信息、账户编号:12345,账户余额100同时还有四个可执行的链接deposit, withdraw, transfer, close

//一段时间后再次查询用户信息时返回
HTTP/1.1 200 OK
Content-Type: appliction/xml
Content-Length: ...
<?xml versino="1.0"?>
<account>
  <account_number>12345</account_number>
  <balance currency="usd">-25.00</balance>
  <link rel="deposit" href="https://somebank.org/account/12345/deposit">
</account>

//这时用户账户余额产生了赤字,可操作链接只剩下一个,其余三个在赤字情况下无法执行。

5.2.好处

让api变得可读性更高,实现客户端和服务端的部分解耦。
对于不使用HATEOAS的REST服务,客户端和服务器的实现之间是紧密耦合的。客户端需要根据服务端提供的相关文档来了解所暴露的资源和对应的操作。当服务器发生变化时,例如修改了资源的uri,客户端也需要进行相应的修改。
而使用HATEOAS的rest服务中,都可以智能地发现可执行的操作,都需要动态的。

  1. 客户端不再需要对不同的资源uri进行硬编码

5.2.1.HATEOAS也是需要文档的

  1. 有些开发者说有了HATEOAS就不用写文档了,这是过于高估HATEOAS的作用了。(如果只是懒得写文档,可以用swagger之类的工具,自动根据注释去生成)
  2. 并不是用来HATEOAS就可以不用写文档了,HATEOAS中只是呈现了资源的关系和操作描述,但是不包括商务逻辑的语义。
  3. 因此他主要解决的是客户端自动适应服务端的变化,而不是客户端在没有文档的情况下能理解整个业务逻辑。

5.3.HATEOAS这么重要,为什么大众都没有提及

在博客的评论中,作者有回应这个问题:主要是当时论文还不是最完善的,描述HATEOAS的不多,但是HATEOAS才是最重要的。

To some extent, people get REST wrong because I failed to include enough detail on media type design within my dissertation. That’s because I ran out of time, not because I thought it was any less important than the other aspects of REST. Likewise, I suspect a lot of people get it wrong because they read only the Wikipedia entry on the subject, which is not based on authoritative sources.
However, I think most people just make the mistake that it should be simple to design simple things. In reality, the effort required to design something is inversely proportional to the simplicity of the result. As architectural styles go, REST is very simple.
REST is software design on the scale of decades: every detail is intended to promote software longevity and independent evolution. Many of the constraints are directly opposed to short-term efficiency. Unfortunately, people are fairly good at short-term design, and usually awful at long-term design. Most don’t think they need to design past the current release. There are more than a few software methodologies that portray any long-term thinking as wrong-headed, ivory tower design (which it can be if it isn’t motivated by real requirements).
And, of course, lately there has been a lot of “me too” activity around REST, as is the nature of any software buzzword.

5.4.现实情况

现实情况是很少见有人使用,正如有个朋友说:HATEOAS很少人用,让他成为一个概念吧。但是我们可以看到github提供的接口有rest风格的,都是有遵循到HATEOAS原则的。很多人忽略了HATEOAS的重要性,但是这却是作者认为最重要的部分。

图片

6.理查森成熟度模型

rest不是一个协议或者规范,只是一种风格的指导**,因此每个人对rest的理解都不尽相同,如何评判自己开发的接口是否restful?2008年,Leonard Richardson在QConTalk中,根据当时实现restful的uri、http、Hypermedia三个约束建立了一个堆叠模型,并将这三个约束的才用程度分成了4个等级,用以评估web服务涉及的好坏与否,而Martin Fowler在2010年进一步诠释,称为Richardson成熟模型。

6.1.概念

  1. Leonard Richardson分析了一百种不同的web服务设计,并根据它们与rest的兼容程度分成了四类,这种rest服务划分模型用于识别其成熟度级别称为Richardson成熟度模型。
  2. Richardson使用三个因素来决定服务的成熟度,即资源(resource、uri)、http动词(http verbs)、hateoas(超媒体)。服务越多采用这些技术,越成熟。
  3. 可以看出,我们大部分都还是在一级和二级。

图片

图片

零级

  1. 此级别根本不是rest api
  2. 不使用任何uri、http method、HATEOAS功能,基本上就是使用http作为远程交互中的隧道机制。
  3. 这些服务具有单个uri并使用单个http method(通常是post)。通俗说,就是只有单个地址,然后通过body参数来获取想要的资源。
  4. 这种做法相当于把http这个应用层协议降级为传输层协议来用。
  5. SOAP、XML-RPM都属于这一级别,仅仅是来回传送POX(Plain Old XML)。

一级

  1. 这是rest api的起始级别
  2. 引入了资源的概念,每个资源有对应的标识符和表达。不像零级那样将所有请求发送到单一的uri

二级

  1. 在一级的基础上,引入了http动词,充分利用了http作为应用程序层协议的全部潜力

三级

  1. 在上一级的基础上,引入了HATEOAS。
  2. 作者认为只有用来HATEOAS才能算是rest,因此,第三级成熟度以外的都不算rest。
  3. 你会发现很多系统宣称自己按照rest构建的api不过只是达到了1、2级的成熟度。

6.2.注意事项

  1. 我们不能盲从,盲目地根据RichardsonMaturityModel去限制我们的开发。rest的设计者创建这个模型,主要是为了帮助开发者更好理解rest,而不是为了让你盲从进行对自己的限制。但是我们知道,当你项目开始复杂的时候,限制是有用的。这个模型也是让开发者更好的理解rest,更好了解自己开发的rest的程度,并不是一味的盲从
  2. 虽然RichardsonMaturityModel是思考rest各种元素时很好的方法,但它不能作为区别rest级别的定义。Roy Fielding已经清晰地做了定义,只有第三级才算是rest。
  3. 该模型与通用的设计技术的关系:1. 第一级通过分治法(divide and conquer)解决复杂性问题,它将较大的服务端点分解成多个资源(resource) 2.第二级引入一组标准的动词,这样我们可以用相似的方法来处理类似的问题,这消除了不必要的不确定性。3. 第三级引入可查找性,提供了使这一协议体现自描述性的手段。结果就是,这样的模型有助于我们思考要提供的http服务,也有助于我们约束使用者的期望。
  4. 当然这个模型也有很多争议讨论:1.我们不应该用这个模型,因为根据roy的规格规定,只有第三级是rest(反驳:从实现的角度,没办法直接跳过第一级就到第三级,所以这个模型还是有作用)。2. 这种判断毫无意义,就好比一个应用选择了RDBMS而非nosql,在没有弄清作出这一选择所基于的原因之前就贸然批评它一样。如果你一味为了满足“rest的优点”,但是真正重要的是否实现了呢?(反驳:这种规定怎么是无意义呢?你认为重要的东西,为什么就不能根据规定评判呢。再反驳:不存在放之四海皆准的好坏标准,意思是可有评判标准,但是应该根据提高质量的属性而不是只围绕着rest限制。最后达成一致:我明白你的观点,我们不应该以“限制”为核心,但是在确定了质量属性之后,在实现中选择正确的限制来支持这些属性是有意义的)

7.REFERENCES

https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
https://www.ics.uci.edu/~fielding/
https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
https://www.infoq.cn/article/2011/05/measuring-rest
https://zhuanlan.zhihu.com/p/37980590
http://www.ruanyifeng.com/blog/2014/05/restful_api.html
https://martinfowler.com/articles/richardsonMaturityModel.html
https://www.crummy.com/writing/speaking/2008-QCon/act3.html

URL编码问题的延伸

遇到问题

问题描述

一个用户,每次请求厂商发货api都是失败,补单却是成功。
正常请求的使用的方法是:

$params = json_encode($params);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);

补单请求使用的方法是:

$client = new Client();
$result = $client->request('POST', $notifyUrl, ['json'=>$parameters])->getBody()->getContents();

原因

正常请求:
这个用户的角色名称带有=号,对url来说是保留字符。
字符串'"r_name": "===NAME==="'会被解析成:
参数名: '"r_name": "=', 参数内容: '==NAME==="'
因此导致服务器接收方解析url有问题。
补单请求:
使用的是第三方包guzzle库,地址:https://github.com/guzzle/guzzle/releases
封装好的类会经过url编码:rawurlencode、urlencode

如何解决

  • 方法1:
    使用php://input。
    这个可以访问请求的原始数据的只读流,不依赖于特定的php.ini的指令,给内存压力更小,而且没有经过解析填充到特定的变量。
  • 方法2:
    传输之前经过url编码。
    对特殊字符经过url编码后,特殊字符就会被转义掉,不影响正常的解析。
    如果使用http_build_query函数,会自动进行了一次url编码。
  • 方法3:
    业务上规避特殊字符的字段。
    一般情况下,我们做接口时候就需要考虑到涉及的字段是否有风险,例如有特殊字符会有什么影响,其实角色名完全可以不传,传角色id即可。

服务器如何接收请求数据

Content-Type决定了后端(nginx、服务端(php、go等等)、框架)的解析方式

Content-Type 浏览器策略 PHP解析body后支持的接收方式
application/x-www-form-urlencoded 默认模式,在发送到服务器之前,所有字符都会进行编码(空格转换为 "+" 加号,特殊符号转换为 ASCII HEX 值)。 三种都支持
multipart/form-data 不对字符编码,在使用包含文件上传控件的表单时,必须使用该值 $_ POST
raw(例如: application/json、text/plain等) 空格转换为"+"加号,但不对特殊字符编码 php://input、$HTTP_RAW_POST_DATA

HTTP协议是建立在TCP/IP协议之上的应用层规范,它把HTTP请求分为三个部分:请求行、请求头、消息主体。协议规定POST提交的数据必须放在消息体(entity-body)中,但协议并没有规定数据使用什么编码方式。服务端通常是根据请求头中Content-Type来获知请求中的消息主体是用何种方式编码的,再对消息主体进行解析。

$_POST

$_ POST是获取表单POSt过来数据(body)的最常用的方法。
只有当请求头中Content-Type为application/x-www-form-urlencoded或multipart/form-data时,PHP才会将POST数据填充到全局变量 $_ POST数组中。

$HTTP_RAW_POST_DATA

原生POST数据。
比如下面的key-value对:
name: Jonathan Doe
age: 23
formula: a + b == 13%!
会被编码成(也就是$HTTP_RAW_POST_DATA的值):name=Jonathan+Doe&age=23&formula=a+%2B+b+%3D%3D+13%25%21
所以$HTTP_RAW_POST_DATA就是PHP的一个预定义的变量,用来获取原始的POST数据。
之后PHP会解析这些原始的POST数据,并且格式化数组,填充到$_ POST中。
Array
(
[name] => Jonathan Doe
[age] => 23
[formula] => a + b == 13%!
)

注意:
1.PHP7已经取消了$HTTP_RAW_POST_DATA,请使用php://input
2.$HTTP_RAW_POST_DATA需要在php.ini中开启always_populate_raw_post_data = On
3.$HTTP_RAW_POST_DATA不支持enctype="multipart/form-data" 方式传递的数据,这种情况下,我们要用 $ _ POST 获取字段的内容,$_ FILES 来获取上传的文件信息。

php://input

php://,是访问各个输入/输出流(I/O streams)
php://input是可以访问请求的原始数据的只读流。POST请求的情况下,最好使用php://input来代替$HTTP_RAW_POST_DATA, 因为它不依赖于特定的php.ini指令。而且这样的情况下$HTTP_RAW_POST_DATA默认没有填充,比激活always_populate_raw_post_data潜在需要更少的内存。
注意:enctype="multipart/form-data"的时候php://input是无效的。

为什么需要原始的POST数据?
因为很多时候接收到的不是网页POST过来的数据,有格式规范,很可能其他方式POST过来的其他格式的数据,可能这些内容无法解析成$_POST数组,这个时候我们就需要对原始的POST数据进行处理。例如:没有经过url编码的数据,可能会有些格式上的问题,会导致解析成数组有问题。例如json、soap、text/xml,PHP不能够识别,就需要直接拿原始数据。

参考文档:
https://www.php.net/manual/zh/wrappers.php.php
https://blog.wpjam.com/m/post-http_raw_post_data-php-input/
https://blog.csdn.net/lamp_yang_3533/article/details/52384199

url编码

通常如果一样东西需要编码,说明这东西不适合传输。原因很多种,如size过大、包含隐私数据。对于url来说,之所要url编码是因为url中有些字符会引起歧义。
URL只能使用英文、阿拉伯数字和某些标点符号,不能使用其他文字和符号。这是网络标准(以前,RFC1738, 现在RFC3986)。
这意味着如果url中有汉字,就必须编码后使用。但是麻烦的是,RFC规范没有规定具体的编码方法,而是交给应用程序(浏览器)自己决定。这就导致了url编码成为了一个混乱的领域。
例如:网址路径的编码,用的是utf-8编码、查询字符串的编码用的是操作系统的默认编码等等,不同浏览器用的策略又不一样。
怎么办?
js先对url编码,再向服务器提交,不要给浏览器插手的机会。因为js的输出总是一致的,就保证了服务器得到的数据格式是统一的。
参考文档:
http://www.ruanyifeng.com/blog/2010/02/url_encoding.html

哪些字符需要编码

RFC3986文档规定,url中只允许包含英文字母、数字、-_ .~4个特殊字符以及所有保留字符(RFC3986中指定了以下字符为保留字符(英文字符):     ! * ' ( ) ; : @ & = + $ , / ? # [ ])。RFC3986文档对url的编码问题做出了详细的建议,指出了哪些字符需要被编码才不会引起url语义的转变,以及对为什么这些字符需要编码做出了相应的解释。

uri网络标准

一开始网络标准是RFC1738,现在更新为RFC3986。这只是网络标准,指明哪些是合法哪些是不合法,但是对于不合法的数据RFC没有指明使用什么具体的编码方法(可能utf-8可能其他),这导致url编码成为一个混乱的领域。

url编码的三个概念

网络标准:uri网络标准,规定了什么是规范什么是合法的数据,RFC3986、RFC1738
编码类型:如何对表单进行编码,Content-Type:application/x-www-form-urlencoded、multipart/form-data、text/plain
具体编码方案:utf-8、GB2312等等。会稍有改动,它是十六进制。需要在前面加上%。比如"\ ",它的ascii码是92,92的十六进制是5c,所以"\ "的url编码是%5c。比如"迷"对应的utf-8编码是\xe8\xbf\xb7,那么它的url编码是%E8%BF%B7。如何解码?先去掉%,然后再进行utf-8解码(不一定utf-8,具体什么编码就什么解码)

参考文档:
https://www.php.net/urlencode/
https://www.php.net/manual/zh/function.rawurlencode.php
https://www.cnblogs.com/panchanggui/p/9436348.html
http://www.faqs.org/rfcs/rfc3986.html
http://www.ruanyifeng.com/blog/2010/02/url_encoding.html

PHP中的url编码

rawurlencode

按照RFC3986对url进行编码。
返回字符串中除了-_ .之外的所有非字母数字字符都会被替换成百分号(%)后跟两位十六进制数。这是在RFC3986中描述的编码,是为了保护原义字符以免其被解释为特殊的url界定符,同时保护url格式以免被传输媒体(像一些邮件系统)使用字符转换时弄乱。
注意:php5.3.0之前,rawurlencode根据RFC1738来编码波浪线(~),后面不需要了

urlencode

返回字符串中除了-_ .之外的所有非字母数字字符都将被替换成百分号(%)后跟两位十六进制,空格则编码为加号(+)。由于历史原因,此编码在将空格编码为加号(+),与rawurlencode(RFC3986编码)不同。
和application/x-www-form-urlencoded、text/plain的类型编码方式一样,空格的编码为加号(+)

rawurlencode、urlencode区别

空格编码差异,分别是+(与post表单保持一样,也是空格编码为+)和%20(遵循RFC,%后面加十六进制数,RFC3986认为+是保留字符)。
所以用rawurlencode更加符合规范,推荐使用rawurlencode。只是一些历史遗留原因,urlencode才会使用。
post请求的时候,application/x-www-form-urlencoded、text/plain的类型编码,也是空格为+的,所以是不是我们需要用urldecode呢?其实一般我们不会接收原始数据,而是接收解析后的数据$_ POST,所以PHP已经处理这些数据(可能用urldecode),我们就不用关心了。

参考文档
https://stackoverflow.com/questions/996139/urlencode-vs-rawurlencode
https://www.jianshu.com/p/99c09270ad52

多次url编码

有时候一些框架会帮助我们做了url编码,或者一些类的封装就已经自动做了url编码,所以这个时候要注意,不然编码多次但是只是解码一次,则会得到不符合预期的结果。

uri、url、urn的区别

概念

uri是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源。web上可用的每种资源:html文档、图像、视频片段、程序等都由一个通用资源标识符进行定位。
url是uniform resource locator,统一资源定位器。它是一种具体的uri,即url可以用来标识一个资源,而且还指明了如何locate这个资源。
urn是uniform resource name,统一资源命名,是通过名字来标识资源,也是一种具体的uri,比如mailto:[email protected]

三者的关系和区别

url、urn是uri的子集。uri是以一种抽象的,高层次概念定义统一资源标识,而url和urn则是具体的资源标识的方式。url和urn都是一种uri。
web上地址的基本形式是uri,它代表统一资源标识符。有两种形式:
url是目前uri最普遍形式,无处不在的url。
urn是url的一种更新形式,统一资源名称不依赖于位置。
微信截图_20200130201149

区分url

如何区分uri中是否url,就看除了标识资源可用的位置外,是否描述了访问该资源的主要机制。所有uri中,不管其是否为url,需遵循形式scheme:[//authority][/path][?query][#fragment]
每部分描述如下:
scheme:对于url,是访问资源的协议名称,对与其他uri,是分配标识符的规范的名称
authority:可选的组成用户授权信息部分,主机及端口(可选)
path:用于在scheme和authority内标识资源
query:与路径一起的附加数据用于标识资源。对于url是查询字符串
fragment:资源特定部分的可选标识符
为了方便地标识特定的uri是否是url,我们可以检查它的scheme,每个url都必须从以下scheme开始:ftp,http,https,gopher,mailto,news,nntp,telnet,wais,file,prospero。如果不是以此开头,则不是url。
也就是说,任何东西,只要能够唯一标识出来,都可以说这个标识是uri。如果这个标识是一个可获取到上述对象的路径,那么同时它也可以是一个url。
例子:
ftp://ftp.is.co.za/rfc/rfc1808.txt (also a URL because of theprotocol)
http://www.ietf.org/rfc/rfc2396.txt (also a URL because of the protocol)
ldap://[2001:db8::7]/c=GB?objectClass?one (also a URL because of the protocol)
mailto:[email protected] (also a URL because of the protocol)
news:comp.infosystems.www.servers.unix (also a URL because of the protocol)
tel:+1-816-555-1212
telnet://192.0.2.16:80/ (also a URL because of the protocol)
urn:oasis:names:specification:docbook:dtd:xml:4.1.2
这些全都是URI, 其中有些是URL. 哪些? 就是那些提供了访问机制的.

参考文档:
https://segmentfault.com/a/1190000006081973
https://blog.csdn.net/neweastsun/article/details/81057868
https://zhuanlan.zhihu.com/p/32613313?utm_source=zhihu&utm_medium=referral&utm_campaign=293074141&utm_term=url
https://www.zhihu.com/question/19557151

上下滚动还是左右翻页

为什么微信读书APP默认是左右翻页而不是上下滚动?

  1. 和微博、朋友圈(短资讯)这种不同,读书是一个比较沉浸式的体验,左右滑动效率不如上下滚动,用户浏览速度变慢,利于沉浸式体验,专注在此刻不变的内容中,提升阅读体验。
  2. 左右滑动和看实体书交互类似

新出的微信读书PC页面版本为啥是默认上下滚动?

两个场景不一样。大家都习惯了pc页面的上下滚动,如果出现左右翻页的,用户不习惯觉得体验很差。人们根深蒂固的使用习惯。

IOC是什么

0.乱

  1. 依赖注入
  2. 依赖倒置
  3. 控制反转、IOC容器
  4. 我们接触这么多概念,它们好像都在说一个意思,到底它们有什么区别呢?

1.依赖倒置原则(Dependency Inversion Principle, DIP)

要了解控制反转(Inversion of Control),我觉得有必要先了解软件设计的一个重要**:依赖倒置原则(Dependency Inversion Principle)

  1. 是Object Mentor公司总裁TobertC.Martin在1996年在发表的
  2. 依赖倒置原则的原始定义是:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象(High level modules shouldnot depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details. Details should depend upon abstractions)核心的**就是,面向接口编程,不要面向实现编程。
  3. 依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合
  4. 由于在软件设计中,细节具有多变性,而抽象则相对稳定,因此以抽象为基础搭建起来的架构要比细节为基础搭建起来的架构要稳定得多。这里的抽象指的是接口或者抽象类,而细节是指具体的实现类。
  5. 使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给它们的实现类去完成。
  6. SOLID原则的D就是,Dependency inversion

1.1.作用

  1. 降低类间的耦合性
  2. 提高系统的稳定性
  3. 减少并行开发引起的风险
  4. 提高代码的可读性和可维护性

1.2.实现方法

  1. 每个类尽量提供接口或抽象类,或者两者都具备
  2. 变量的声明类型尽量是接口或者是抽象类
  3. 任何类都不应该从具体类派生
  4. 使用继承时尽量遵循里氏替换原则

1.3.举例子:依赖倒置原则中高层模块不依赖底层模块

假设我们设计一辆汽车:先设计轮子,然后根据轮子大小设计底盘,接着根据底盘设计车身,最后根据车身设计好整个汽车。这里就出现了一个"依赖"关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子。
v2-c68248bb5d9b4d64d22600571e996446_720w

这样的设计看起来没问题,但是可维护性却很低。假设设计完工之后,上司却突然说根据市场需求的变动,要我们把车子的轮子设计都改大一码。这下就麻烦了:因为我们是根据轮子的尺寸设计的底盘,轮子的尺寸一改,底盘的设计就得修改,同样车身是根据底盘设计的,那么车身也要修改,同理汽车也要修改,整个设计几乎都得改。
现在换一种思路。我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子。这时候,依赖关系就倒置过来了:轮子依赖底盘,底盘依赖车身,车身依赖汽车。

v2-e64bf72c5c04412f626b21753aa9e1a1_720w

这时候,上司再说要改动轮子的设计,我们只需要改动轮子的设计,而不需要动底盘、车身、汽车的设计。
这就是依赖倒置原则---把原来的高层建筑依赖底层建筑"倒置"过来,变成底层建筑依赖高层建筑。高层建筑决定需要什么,底层去实现这样的需求,但是高层并不用管底层是怎么实现的。这样就不会出现前面的"牵一发动全身"的情况。
为了理解这几个概念,我们还是用上面汽车的例子。只不过这次换成代码。我们先定义四个class,车、车身、底盘、轮胎。
v2-8ec294de7d0f9013788e3fb5c76069ef_720w
v2-64e8b19eeb70d9cf87c27fe4c5c0fc81_720w
v2-c920a0540ce0651003a5326f6ef9891d_720w
v2-99ad2cd809fcb86dd791ff7f65fb1779_720w

http://c.biancheng.net/view/1326.html
https://en.wikipedia.org/wiki/Dependency_inversion_principle

1.4.好莱坞原则(Hollywood principle)

  1. Don't call me, I'll call you
  2. 核心,用通知代替轮询
  3. 他没有一个具体的设计模式,就是一个**。和DIP是很像的,描述了上层和下层之间的关系,为了解决类之间的紧耦合,但是两者还是有区别的。
    https://zhuanlan.zhihu.com/p/295209351

2.控制反转(Inversion of Control, IOC)

  1. 控制反转(Inversion of Control)就是基于依赖倒置原则的一种代码设计的思路,不是什么技术,而是一种**。具体采用的方法一般是DI、DL
  2. ioc意味着我们设计好的对象交给容器控制,而不是传统的需要时在内部构造直接控制。高耦合变成了松散耦合
  3. ioc对编程带来的最大改变不是从代码上,而是从**上,发生了“主从换位”的变化。应用程序原本是老大,要什么资源都是主动出击,但是ioc**中,应用程序就变成被动的,被动地等待ioc容器来创建并注入它所需要的资源。
  4. ioc很好地提现了 DIP原则和好莱坞原则,即由ioc容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。
  5. IOC是把对象的控制权交给框架或容器,容器中存储了众多我们需要的对象,然后我们就无需再手动的在代码中创建对象。需要什么对象就直接告诉容器我们需要什么对象,容器会把对象根据一定的方式注入到我们的代码。注入的过程就被称为DI。有时候需要动态的指定我们需要什么对象,这个时候要让容器在众多对象中去寻找,容器寻找需要对象的过程,称为DL(Dependency Lookup, 依赖查找)。
  6. 按照上面的理解,那么IOC包含了DI与DL,并且多了对象注册的过程。IOC是一种设计模式,一种概念,主要包含的内容如下:对象的生命周期的管理;依赖的解析与注入;依赖的查询;应用的配置

2.1.IOC的优点

IOC容器给我们提供的最大便利之处就是更容易实现可插拔,可替换的组件。这也是接口驱动开发所带来的优势,根据接口可以提供更多灵活的子类实现,增强代码的健壮性和稳定性。

IOC管理的组件一般是实现了某些接口的类,这些组件又会使用其他的实现某些接口的组件,它们都不需要知道接口的具体实现,因为这个,组件间的替换才会如此容易。容器的任务就是帮我们创建组件的具体事例,并且把管理它们的依赖关系,把需要的具体依赖传给组件。

  1. 减少代码的耦合,可以使应用更加模块化
  2. 增加代码的复用率
  3. 资源更加统一管理
  4. 维护代码更方便,一般只需要修改一下配置文件就ok了
  5. 提升了程序的测试性

应用无需关心组件,只需要从容器中拿。
v2-555b2be7d76e78511a6d6fed3304927f_720w

2.2.控制反转容器

  1. 其实上面的例子中,对车类进行初始化的那段代码发生的地方,就是控制反转容器。
    v2-c845802f9187953ed576e0555f76da42_720w

IOC Container优点

  1. 显然你也应该观察到,因为采用了依赖注入,在初始化的过程中就不可避免的会写大量的new。这里IOC容器就解决了这个问题。这个容器可以自动对你的代码进行初始化,你只需要维护一个Configuration(可以是xml也可以是一段代码),而不用每次初始化一辆车都要亲手去写那一大段初始化的代码。
  2. 我们在创建实例的时候不需要了解其中的细节。在上面的例子中,我们自己手动创建一个车instance时候,是从底层往上层new的。
    这个过程中,我们需要了解整个Car/Framework/Bottom/Tire类构造函数是怎么定义的,才能一步一步new/注入,而IOC Container在进行这个工作的时候是反过来的,它先从最上层开始往下找依赖关系,到达最底层之后再往上一步一步new(有点像深度优先遍历)。这里IOC Container可以直接隐藏具体的创建实例的细节,在我们来看它就像一个工厂。

3.PHP中的IOC

  1. laravel、tp,反射
  2. https://github.com/laravel/framework/tree/8.x/src/Illuminate/Container

4.Go中的DI

手动DI

  1. https://www.openmymind.net/Dependency-Injection-In-Go/
  2. DI framework不是必要的,DI只是手段
  3. Type aliases or interface

DI framework - wire

  1. 是Go Cloud团队开发的,通过自动生成代码的方式在编译期完成依赖注入
  2. 早期就有uber的dig和facebook的inject,他们都是通过反射机制实现运行时依赖注入。为什么Go Cloud团队还要重造一遍轮子?因为在他们看来上面的类库都不符合Go的哲学:clear is better than clever,Reflection is never clear -- Rob Pike
  3. 作为一个代码生成工具,Wire可以生成Go源码并在编译期完成依赖注入。它不需要反射机制或Service Locators。后面会看到,Wire生成的代码与手写无异。这种方式带来一些列好处:
    1. 方便debug,若有依赖缺失编译时会报错
    2. 因为不需要Service Locators,所以对命名没有特殊要求 https://martinfowler.com/articles/injection.html
    3. 避免依赖膨胀,生成的代码只包含被依赖的代码,而运行时依赖注入则无法做到这一点
    4. 依赖关系静态存于源码中,便于工具分析与可视化

5.区别与联系

  1. 依赖倒置原则是六大设计原则中的一种
  2. IOC是遵循了依赖倒置原则的一个**
  3. 依赖注入、依赖查找是IOC的实现方式
  4. DI、DL可以看做是IOC的一种实现方式。IOC是一种**,而DI、DL是一种设计模式,是一种实现IOC的模式
  5. IOC和DI其实是同一个概念的不同角度的描述,由于控制反转概念比较模糊,所以2004年大师级人物Martin Flower又给出了一个新的名字:依赖注入。相对ioc而言,依赖注入明确描述了被注入对象依赖ioc容器配置依赖对象
    https://martinfowler.com/articles/injection.html#InversionOfControl
    Screenshot from 2021-09-25 17-53-55

6.思考

  1. 我们控制反转不是一直都是上层与下层的反转,而是核心与非核心的反转

    1. 例如DDD的基础层与其他层交互,就用控制反转,意思是,上层控制基础层,而不是上层的活动被基础层控制。但是DDD的领域层,它与应用来说,它是底层,但是领域层是核心层,所以不需要控制反转

    Screenshot from 2021-09-25 18-02-55

  2. 为什么需要依赖倒置?

    1. 就像DDD一样,业务决定技术实现,而不是相反。

what is good package name

1.前文

  1. linter提示包名的设置,不要有下滑线,那么怎样才是好的包名呢

2.怎样才是好的包名

  1. 只包含小写字母和数字。
  2. 不要重复的含义,例如bufio.Reader,而不是bufio.BufReader;或者walletbiz.New而不是walletbiz.NewWalletBiz:The importer of a package will use the name to refer to its contents, so exported names in the package can use that fact to avoid repetition
  3. 好的包名名称是短的、清晰的、没有下划线、没有驼峰,他们是简单的名词. "package name should be good: short, concise, evocative。By convention, packages are given lower case, single-word names; there should be no need for underscores or mixedCaps."
    1. 例如:time、list、http
    2. 其他语言经常用下划线和驼峰来命名包,但是在go不这样做
  4. 使用缩写。但是如果你的缩写是模凌两可或者读起来不清晰,那么就不要用缩写
    1. strconv(string conversion)
    2. syscall(system call)
    3. fmt(formatted I/O)
  5. Don’t steal good names from the user
    1. 避免包名是客户端经常用的名字。
    2. 例如buffered I/O包名是bufio,而不是buf,因为用你包的人肯定会经常用buf这个名字

3.怎么命名好包里面的内容

  1. 包里面的内容和包名是一对的,因为使用这个包内容,包和包的内容是在一起的,包名是前缀
  2. 避免重复。例如http的server,直接http.Server就行,而不是http.HttpServer。
  3. 简单的方法名。当包里的方法返回的是pkg.Pkg(or *pkg.Pkg),那么可以省去方法名带有的类型名
    1. 我们可能会有方法GetUser()返回User类型,但是如果你本来就是在user包,那么就直接user.Get()就好,不需要user.GetUser()
    2. 但是如果是user包的GetGrade()返回Grade类型,那么就还是要带类型的
    3. 例如time.Now()返回time.Time, time.Parse()返回time.Time, context.WithTimeout()返回context.Context, usurp.FromContext返回IP
  4. 如果不是返回pkg.Pkg而是返回pkg.T,那么方法名应该带有T
    1. time.ParseDuration()返回的是time.Duration, time.Since()返回的是time.Duration, time.NewTicker返回的是*time.Ticker, time.NewTimer返回的是*time.Timer
  5. New()返回pkg.Pkg,是提供给用户使用的标准
  6. 类型在不同的包可以有不同的名字,因为用户可以通过包名来清晰地分辨
    1. 例如jpeg.Reader, buffo.Reader, csv.Reader

4.包路径

  1. 包路径是导入的时候才会用到,路径的最后一个名字就是包名
  2. 不同的目录下可以有相同的包名,就像不同的包可以有相同的类型或方法名字一样
    1. 例如runtime/pprof提供数据分析,而net/http/pprof也是提供数据分析,但是只是通过http的方式呈现
    2. 如果一个文件里面同时使用这两个包,可以rename
    3. 但是如果这两个包是会经常在一起使用的,那么还是要包名区分的。例如,accountrepo,accountsvc,这两者都会被同一个文件用到的,如果都命名account,那么就难以使用了

5.不好的包名

  1. meaningless:util, common, misc,
    1. 使用者不知道这些包里面有什么东西,提供什么功能,难以使用。
    2. 维护者也难以保持包的聚焦。久而久之,难以维护。
  2. 不要把所有api放在一个包
    1. 就像和无意义的包一样,会变得越来越大,使用者不知道怎么使用
  3. 避免不必要的包名冲突
    1. 例如,accountrepo,accountsvc,这两者都会被同一个文件用到的,如果都命名account,那么就难以使用了

references

  1. https://go.dev/blog/package-names
  2. https://stackoverflow.com/questions/63446528/package-name-containing-hyphen
  3. https://go.dev/doc/effective_go#package-names

Git分支工作流

1.Git分支开发规范

  1. Git没有一个强制的规范,每个团队都可以使用适合自己的。但是有一些比较流行的流程
  2. git-flow出现的最早,适用于版本控制的发布。github flow在git flow的基础上做了一些优化,适用于持续版本的发布。而gitlab flow出现的时间最晚,所以综合了前面两种工作流的优点。
  3. 以上三种工作流程,有一个共同点:都采用功能驱动式开发(Feature-driven development,简称FDD)。它指的是,需求是开发的起点,先有需求再有功能分支(feature branch)或者补丁分支(hotfix branch)。完成开发后,该分支就合并到主分支,然后被删除。

2.git-flow

  1. 是Vincent Driessen 2010年提出,属于强流程性。
  2. 最早诞生、并得到广泛采用的一种工作流程
  3. git flow的分支结构很特别,按功能来说可以分为5种分支。从生命时间上,又可以分别归类为长期分支和暂时分支(或者更贴切的描述为,主要分支和协助分支)。是比较流行的一种方式。有两个稳定的分支:master、develop,还有三个临时分支:feature、release、hotfix,一旦完成开发,它们就会被合并进develop和master,然后被删除。
  4. 比较多人用,还推出了一个工具辅助脚本来帮助强制流程(https://github.com/nvie/gitflow)。

2.1.分支命名

master/main

  1. main为主分支,用于部署正式环境的分支,要保持稳定
  2. 一般由develop以及hotfix分支合并,任何时间都不能直接修改代码
  3. 为了避免种族歧视,2021年开始,github、gitlab等开始把默认分支的命名从master改为main

develop

  1. 为开发分支
  2. 开发新功能时,feature分支都是基于develop分支下创建

feature

  1. 分支命名:feature/xxxx
  2. 一般的话,加人名也更加直观,并且分支还可以直到归属方。{feature, hotfix}/{开发者名称, integration, delivery}/{xxx}{date}

release分支

  1. release为预上线分支,发布提测阶段,以release分支代码为基准提测
  2. 当有一组feature开发完成,首先会合并到develop分支,进入提测时,会创建release分支。如果测试过程中存在bug需要修复,则直接由开发者在release分支修复并提交。当测试完成之后,合并release分支到master和develop分支,此时master为最新代码,用作上线。

hotfix分支

  1. 分支命名:hotfix/xxx,它的命名规则和feature类似
  2. 线上出现紧急问题时,需要及时修复,以master分支为基线,创建hotfix分支,修复完成后,需要合并到master分支和develop分支
    Screenshot from 2021-03-22 20-13-20

2.2.问题

这个模式是基于“版本发布”的,目标是一段时间后产出一个新版本。但是,很多网站项目是“持续发布”的,代码一有变动就部署一次。这时,master分支和develop分支的差别不大,没必要维护两个长期分支。
git flow的优点是清晰可控,缺点是相对复杂,需要同时维护两个长期分支。大多数工具都将master当做默认分支,可是开发是在develop分支进行,这导致经常要切换分支,非常烦人。
假如我一个feature提交到develop了,也在release了,但是发现有问题。需要修改。这个时候,另外一个feature提交到develop需要上线了,但是原来的feature还没修改完,这时候就影响上线了,所以需要回滚,这是非常麻烦的。(其实这并不麻烦,可以很多办法,回滚或者cherry-pick都可以,无论什么flow都会有这种问题,只能说应该在提交合并之前就要做好充分测试,减少这样的场景发生)

https://nvie.com/posts/a-successful-git-branching-model/
https://www.ruanyifeng.com/blog/2012/07/git.html
https://jeffkreeftmeijer.com/git-flow/

3.github flow

  1. 是git flow的简化版,专门配合“持续发布”
  2. 是github使用的工作流模型,由scott chacon在2011年8月31日发布。

3.1.为什么会有github flow

  1. 因为git flow对于大部分团队来说过于复杂,并且没有gui图形界面,只能用命令行
  2. git-flow过程主要围绕版本进行设计,但是很多情况下我们没有版本这个概念,因为我们每天都部署到生产中很多次

3.2.介绍

Screenshot from 2021-03-22 20-41-42

  1. 对比git flow,github flow可以说是非常简单,只有一个主分支master,其他团队成员通过pull request来合并到master(过程中可以一起评审和讨论你的代码,对话的过程中,也可以不断地提交代码)。因此需要团队成员技术比较强,不然就容易出事了
  2. 比较有特色的是pull request,后来gitlab flow就是受这个启发有了merge request

3.3.评价

github flow最大的优点就是简单,对于“持续发布”的产品,是最合适的流程
问题:master分支的更新与产品的发布是一致的,也就是master是最新的分支,默认就是当前的线上代码。可是有些时候并非如此,我们可能不需要马上发布,可能是有版本控制,也可能是我的公司是有发布窗口的。
因此,只有master是不够用的,还需要另建一个production分支跟踪线上版本。
http://scottchacon.com/2011/08/31/github-flow.html

4.Gitlab Flow

  1. Gitlab Flow是GitLab的CEO Sytse Sijbrandij在2014年9月29正式发布。因为比前面两种晚发布,所以有非常大的优势集各家之长,是git flow和github flow的综合。它吸取了两者的优点,既有适应不同开发环境的弹性,又有单一分支的简单和便利。
  2. Gitlab Flow既支持Git Flow的分支策略,也有Github Flow的Pull Request(Merge Request)和issue tracking
    针对Github里面只有一个Master分支的情况,从需要发布的环境的角度出发,添加了pre-production和production分支都对应不同的环境,这个分支策略比较适用服务端,测试环境、预发布环境,正式环境,一个环境一个分支。

4.1.上游优先

gitlab flow最大原则叫做,上游优先,既只存在一个主分支master,它是所有其他分支的“上游”。只有上游分支采纳的代码变化,才能应用到其他分支

4.2.持续发布

Screenshot from 2021-03-23 10-32-28

对于持续发布的项目,它建议在master分支之外,再建立不同环境的分支。比如,“开发环境”分支是master,“预发布环境”分支是pre-production,“生产环境”分支是production
开发分支是预发布分支的上游,预发布分支又是生产分支的上游,代码的变化必须由上游向下游发展。比如,生产环境出现了bug,这时就要新建一个功能分支,先把它合并到master,确认没有问题,再cherry-pick到pre-production,这一步也没有问题,才进入production。只有紧急情况,才允许跳过上游,直接合并到下游分支

4.3.版本发布

Screenshot from 2021-03-23 10-36-22

对于“版本发布”项目,建议的做法是每一个稳定版本,都要从master分支拉出一个分支,比如2-3-stable、2-4-stable
以后只有修补bug,才允许将代码合并到这些分支,并且此时要更新小版本号

4.4.gitlab flow的一些原则

每个人都从master分支开始工作,目标也是master分支
在master分支修正错误,其次再到发布分支
http://dockone.io/article/2350
https://about.gitlab.com/topics/version-control/what-are-gitlab-flow-best-practices/

4.5.一些问题

4.5.1.gitlab flow中pre-production和master是否应该是临时分支

如果只有production(就是我们印象中的master)是稳定的分支,其他都是临时的话,可以保证每次都是干净的,但是缺点是,每次都是临时,不方便我每个人的提交,也不方便我做测试。
没有特别说明是临时还是长期的,但是临时并没有什么好处。所以原则上,master、pre-production、production都应该是永久分支,只有自己的feature分支才是临时分支
官方没有说临时还是永久的问题,但是从图片可以看出,这三个分支都是永久分支,不是临时拉出来的。
永久的比较好,这样不需要每次都要建新分支,增加工作量和沟通成本。他们都是同一个分支出来只是存在不同环境,而且遵循上游优先的原则,理论上会最终一致的。

为什么飞书所有消息都排列在左侧

1.前文

  1. 第一次用飞书/lark的时候,发现会话消息统一都在左侧的。它是支持设置回左右模式,但是默认设置是统一在左侧(通过颜色深浅来区分自己发的消息)。
  2. slack也是左对齐
  3. 产品这样设计的原因是什么?
    image

2.官方回答

image
https://helpdesk.feishu.cn/hc/zh-CN/articles/360045255773

3.原因

  1. 全部居左可以减少眼球运动,降低疲劳感
  2. 有一个报告说,人们大多不自觉地使用F字母形状的模式来阅读页面。所以,居左的设计对用户来说,读信息会更快。

4.F模型浏览

  1. 《2014 Web UI模式》中所述,CNN和纽约时报都使用了F型
  2. 该阅读模式由NNGroup的眼动追踪研究而被广普及,在这个研究中记录了超过200位用户浏览网页时,发现用户的主要阅读行为在许多不同的网站和任务重相当一致。
  3. 首先横向浏览读取内容,通常跨越网页内容布局的上部。这个初始元素构成了字母F的第一横
  4. 接下来向下浏览,在整个页面稍下一排横向浏览,该浏览轨迹通常覆盖比先前浏览更短的趣语。这个元素形成了字母F的第二横
  5. 最后,以垂直移动的方式浏览网页页面的左侧,有些情况下,这是一个缓慢而系统的浏览,在eyetracking热点图上显示为实心条纹。有些情况下,用户浏览得比较快,构成一个spottier热点图。
    image
    image

https://www.nngroup.com/articles/f-shaped-pattern-reading-web-content/
https://thenextweb.com/news/how-to-design-websites-that-mirror-how-our-eyes-work
https://youle.zhipin.com/questions/2ec8bdd8d884aaf1tnB43Nu9.html
https://wen.woshipm.com/question/detail/8646es.html
https://www.zhihu.com/question/344660865/answer/815159293
http://www.woshipm.com/pd/430151.html
http://www.woshipm.com/ucd/631142.html

重构

"The scarcest resource is not oil, metals, clean air, capital, labor, or technology. It is our willingness to listen to each other and learn from each other and to seek the truth rather than seek to be right."
— Donella Meadows

“最稀缺的资源不是石油、金属、清洁的空气、资本、劳动力或技术。而是我们愿意相互倾听、相互学习,寻求真理而不是追求正确。”
— 多内拉·梅多斯

题外话

1.为什么会有Gopher Meetup (Q1)

1.1 为什么故意叫meetup

  1. 中文翻译是:碰头、相聚、会面
  2. 大概想要表达的意思是,轻松自由,像和朋友瞎聊天一下,讨论技术、生活、哲学、八卦
  3. 反正就是不要这么严肃,越能Diss越好(想象一下,你怎么和你亲密的朋友讨论技术/NBA)
  4. 阿老师说:我以为这周是我,有些紧张。(我们希望是轻松自由,可以犯错,认真总结就行,不需要有包袱)

1.2. 但是也有一些原则

  1. 只能和Golang相关,沾点边就行了(吐槽Golang、Golang圈的一些八卦、自己的学习经历、自己遇到的问题、最好是听完就能用),其它业务分享、技术、规范,都是非常重要的,比Golang更加重要,但是,不是在这里,我们需要另外约分享/交流
  2. 推荐使用markdown而不是ppt(ppt有它的使用场景,not here),因为有些知识我在分享会上没能吸收到,我私下时间再根据文档去学习;我们不是为show自己,不是要制作多少惊喜,不是要多漂亮的界面,要的是交流**,要的是自由地交谈。

1.3. 没人来听了怎么办

  1. 只要全职Gopher程序员,还是乐意参加,交流能学到东西,那么这个meetup,属于一手的Gopher Meetup文化就一直举办下去(会议室里3个人及以上就继续走下去,如果只有两个人,那么就宣布停摆...)
  2. 举办这个meetup不是为了让别人听,或者显得自己不一样,这个meetup是为了交流,三人行必有我师。所以我们不是为了让别人听,我们是交流。
  3. 越小越能充分互相学习

重构

1.交流过去的经历(Q2)

2.复盘我们这一次

Screenshot from 2021-12-02 18-17-04
Screenshot from 2021-12-02 18-22-07

3.我的一次“成功”的“重构”经历

  1. 2015年,当时团队所有人,3个月左右,小黑屋
  2. 运维、测试、前端,全上阵
  3. 3个月,我们重构成我们想要的样子,非常“完美”,闪闪发亮的代码。引以为傲的一次重构经历
  4. 上线那一刻,还是出问题了,无论做了多少测试、多少备用方案,最后还是出问题了,因为整个上线太大了,忽略了某个细节。充值模块出问题,故障约30分钟。
  5. 当时,技术经理面对巨大的压力,有两个选择,回滚还是马上查bug解决,回滚是最安全的方式,但是也意味着,我们上线失败了,业务、更高的领导层可能会不同意我们再次上线。我们选择的是后者,解决了bug,吃了一个s级故障,同时我们完成了最大的一次重构。
  6. 这次“重构”,帮助我们以后节省了非常多的服务器成本、人力维护成本
  7. 最后我们这个项目,在技术中心赢得很多荣誉,很多人因此得到晋升,我们也是往微服务的方向走了一大步
  8. 这好像是一次值得拿出来炫耀的经历,但是现在不了,这是一次失败的经历。

3.1.为什么说它是失败

  1. 我们能承担这种故障吗?我们选择不回滚,因为回滚后,我们的手尾更长,同时也宣布我们的项目就算失败了,可能不会有第二次机会让你上线(因为CEO不会继续冒险)。这是非常不健康的上线方式
  2. 我们能向业务争取到这么多时间,让我们去重构吗?在很多地方都是不现实的,能争取一点点,降低需求量就很不错了,完全停止新需求去做重构,在现在几乎不可能。
  3. 在不断增加新功能,应对各种变化,在几年后的某一天,就会有新来的同事说:这太糟糕了,维护不了了,重写它吧...

4.什么是重构

  1. 重构技术的两位最早倡导者是Ward Cunningham和Kent Beck。他们很早就把重构作为软件开发过程的一块基石,并且在自己的开发过程中运用它。

  2. 好代码的检验标准就是人们是否能够轻而易举地修改它![Screenshot from 2021-12-01 22-35-32](/home/frank/Pictures/Screenshot from 2021-12-01 22-35-32.png)

  3. 所谓重构(refactoring)是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。重构是一种千锤百炼形成的有条不絮的程序整理方法,可以最大限度地减少整理过程中引入错误的概率。本质上说,重构就是在代码写好之后改进它的设计。

    1. 重构的关键在于运用大量微小且保持软件行为的步骤,一步步达成大规模的修改。每个单独的重构要么很小,要么由若干个步骤组合而成。因此,在重构的过程中,我的代码很少进入不可工作的状态,即便重构没有完成,我也可以在任何时刻停下来。
    2. 上面可能和大家想的不一样。在软件开发的大部分历史时期,大部分人都应该先设计而后编码:首先得有一个良好的设计,然后才能开始编码。但是,随着时间流逝,人们不断修改代码,于是根据原先设计所得的系统,整体结构逐渐衰弱。代码质量慢慢沉沦。所以,你永远没有一个完美的设计,所以等不来一个可以让我们轻松十年的架构,它不存在。
    3. "重构"正好与此相反。哪怕手上有一个糟糕的设计,甚至是一堆混乱的代码,我们也可以借由重构将它加工成设计良好的代码。
    4. 重构的每个步骤都很简单,甚至显得有些过于简单:只需要把某个字段从一个类移到另一个类,把某些代码从一个函数拉出来构成另一个函数,或是在继承体系中把某些代码推上推下就行了。但是聚沙成塔,这些小小的修改累积起来就可以根本改善设计质量,这和一般常见的"软件会慢慢腐烂"的观点恰恰相反。
    5. 有了重构以后,工作的平衡点开始发生变化。设计不是在一开始完成的,而是在整个开发过程中逐渐浮现出来。在系统构筑过程中,不断改进设计。这个"构筑-设计"的反复互动,可以让一个程序在开发过程中持续保持良好的设计。
    6. 如果有人说他们的代码在重构过程中有一两天时间不可用,基本上可以确定,他们在做的不是重构。
    7. 刚接触重构的人看我用很多小步骤完成似乎可以一大步就能做完的事,可能会觉得这样很低效。但小步前进能让我走得更快,因此这些小步骤能完美地彼此组合,而且,更关键的是,整个过程我不会花任何时间来调试。
    8. 重构和性能优化有很多相似之处:两者都需要修改代码,并且两者都不会改变程序的整体功能。两者的差别在于目的:重构是为了让代码"更容易理解,更易于修改"。这可能使程序运行得更快,也可能使程序运行得更慢。在性能优化时,我只关心让程序运行得更快,最终得到的代码有可能更难理解和维护,对此要有心理准备。
  4. 名词:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本

  5. 动词:使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构

  6. 过去我们都用"重构"来描述各种形式的代码清理,但是上面的定义,只是一种特定的方式。你可以用重写、结构调整(restructing)来泛指对各种形式的代码清理

5.重构的目的

  1. 它不是银弹,不过它的确很有价值,尽管不是一颗银弹,也能算是一把"银钳子",可以帮你始终良好地控制自己的代码。

5.1.改进软件的设计

  1. 如果没有重构,程序的内部设计(或者叫架构)会逐渐腐败变质。我们只为短期目的(完成这个需求)而修改代码时,他们经常没有完全理解架构的整体设计,于是代码逐渐失去自己的结构。程序员越来越难以阅读源码来理解原来的设计。代码结构的流失有积累效应。越难看出代码所代表的设计意图,就越难保护其设计,于是设计就腐败得越快。 -----> 经常性的重构有助于代码维持自己该有的形态。
  2. 设计欠佳的代码往往需要更多代码,很多重复的代码,只是上下文有点不同 ------> 我们就是要消除重复,这样更容易理解。消除重复代码,我就可以确定所有事物和行为在代码中只表述一次, 这正是优秀设计的根本。

5.2.使软件更容易理解

  1. 写代码本质就是表达业务逻辑,但是别忘了,代码还是需要被同事(或者未来的你自己)看的
  2. 写代码的时候,一般不会想到未来要看这段代码的开发者,所以我们需要刻意让代码更容易理解 ------> 重构就是在代码正常运行情况下,让代码变得更好看,我更清晰表达出代码的逻辑

5.3.帮助找bug

  1. 重构的过程中,会梳理好逻辑,更有效地写出健壮的代码

5.4.提高接需求的速度

  1. 这是最重要的一点,这也是违反大家直觉的一点。what?重构还提高速度?要改代码后才能继续开始写需求,但我现在来一个ctrl+c/v,改改参数就可以完成任务了,重构明显是降低我的开发速度!

Screenshot from 2021-12-01 23-04-06

  1. 经常听到这样的故事:一开始进展很快,但如今想要添加一个新功能需要的时间就要长得多。他们需要花越来越多的时间去考虑如何把新功能塞进现有的系统,因为牵连的东西太多了,产品提出新增/修改一个小逻辑,会听到:影响很大的没那么简单,或者说,这块不是我写的比较复杂,我要花比别人更多的时间。
  2. 不断蹦出的bug修复起来也越来越慢。代码看起来就像不断加补丁,需要考古工作才能弄明白整个系统是如何工作的。这份负担不断拖慢新增功能的速度,到最后程序员恨不得从头开始重写整个系统。
  3. 而有些团队的境遇则截然不同,他们添加新功能的速度越来越快,因为他们能利用已有的功能,基于已有的功能快速构建新功能。
  4. 两种团队的区别就在于软件的内部质量。内部良好,更容易添加、修改,即使bug也很容易发现和解决。
  5. Martin Fowler称之为:设计耐久性假说。通过投入精力改善内部设计,增加软件的耐久性,从而可以更长时间地保持开发的快速。
  6. 20年前行业的陈规认为,良好的设计必须在开始编程之前完成,因为一旦开始编写代码,设计就会逐渐腐败(甚至现在也会有很多人这样认为)。重构改变了这个图景,现在我们可以改善已有代码的设计,因此我们可以先做一个设计,然后不断改善它,哪怕程序本身的功能也在不断地发生着变化。由于预先做好良好设计非常困难,想要既体面又快速地开发功能,重构必不可少。
    Screenshot from 2021-12-02 00-30-30

6.啥时候重构

  1. Martin Fowler:每时每刻都在重构,我编程的每个小时,都会发生重构

  2. Don Roberts的三次法则:第一次做某件事就去做,第二次做类似的事就会反感了,第三次再做就应该重构了。(也就是DRY原则,Don't repeat yourslef)

  3. 一开始开发的人就是这样是最好的设计,后面就不合适

6.1.预备性重构

  1. 添加功能之前。重构对于新功能的加入更加容易。
  2. Jessica Kerr:这就好像我要往东去100公里。我不会往东一头把车开进树林,而是先往北开20公里上高速,然后再往东开100公里,后者的速度比前者要快3倍。如果有人催你赶快去那儿,有时你需要说:等等,我要先看看地图,找出最快的路径。这就是预备性重构。
  3. 修复bug时的情况也是一样,在寻找问题根因时,我可能会发现:如果把3段一模一样的都会导致错误的代码合并到一处,问题修复起来会容易很多。或者说把新数据的逻辑和查询逻辑分开,会更容易避免造成错误的逻辑纠缠。用重构改善这些情况。

Screenshot from 2021-12-02 00-51-27

6.2.帮助理解的重构:使代码更易懂

  1. 一旦需要思考代码在做什么?就问自己:能不能重构这段代码,令其一目了然?修改一下函数名、变量名,代码复用,这些都会有立竿见影的效果,这些都是重构的时机
  2. 记性不好,记不住细节。

6.3.捡垃圾式的重构

  1. 每次经过代码,修改或者新增,都让代码比原来更好
  2. 这里有一个取舍:任务紧急不想花时间重构,那么久先记录这个重构的位置,等完成任务后回来重构它

6.4.有计划 vs 见机行事、长期重构 vs 短期谁靠近谁重构

  1. 上面的重构时机,并不会专门安排一段时间、独立的团队去重构,都是和功能增加/修改的编程工作密不可分的,在完成工作的时候顺便去做的事情,自然发生的事情。(当然并不是说有计划地规划重构是错的,但是这种计划应该要变少,大量的重构应该是不起眼的、见机行事的)
  2. 重构不是与功能编程割裂的行为(非常容易误解)
  3. 坏味道的代码需要重构,但是漂亮的代码也需要重构,昨天完全合理的权衡,到今天就不合适了。
  4. 优秀的程序员知道:添加功能最快的方法往往是先修改现有的代码,使新功能容易被加入。
  5. 重构应该和新功能紧密交织,重构脱离了上下文,就看不出重构的价值。

7.怎么对pmo、产品经理说

  1. 不要告诉他们
  2. 他们的职责是让新功能完成更快些,对我们来说,最快的方式就是先重构,所以我就重构

8.啥时候不应该重构

  1. 一堆凌乱的代码,如果你以后也不需要修改它,不需要和它打交道,那就不重构
  2. 一堆凌乱的代码,隐藏在api之下,我不需要理解它的工作原理,那就不重构
  3. 重写比重构更容易,那就不重构(这个需要严谨判断)

9.重构的挑战

9.1.延缓新功能开发

  1. 正如上面说过了,很多人认为花在重构的时间是在拖慢新功能的开发进度,这可能是导致人们没有充分重构的最大阻力所在。
  2. 重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值。
  3. 有一种情况确实需要权衡的,有一个大规模的重构很有必要进行,而马上要添加的功能非常小,这时我会愿意先把新功能加上,然后再做这次大规模重构。
  4. 有些人试图用"整洁的代码""良好的工程实践"之类道德理由来论证重构的必要性,我认为这是个陷阱。重构的意义不在于把代码库打磨得闪闪发光,而是纯粹经济角度出发考量的。我们之所以重构,因为它能让我们更快--添加功能更快,修复bug更快。一定要随时记住这一点,与别人交流时也要不断强调这一点。重构应该总是由经济利益驱动。

9.2.分支冲突

  1. 很可能我在重构的时候,其他同事的功能开发就已经用到这个被重构的函数。功能开发周期越久,到时候集成到master就越困难。
  2. 持续集成(Continuous Integration, CI),持续集成到master,这就很考验你的代码拆分能力,要保持master不被影响。
  3. 测试,自动化测试体系需要建立起来。

9.3.遗留的代码

  1. 没有单元测试、也很难看懂逻辑 ----> 建议写单元测试;每次触碰一块代码时,尝试把它变得好一点点

10.重构、架构、YAGNI

  1. 我们一直在说演进式架构。
  2. "在编码之前先完成架构"这种做法最大的问题在于,它假设了软件的需求可以预先充分理解。但经验现实,这个假设很多时候是不切实际的。只有真正使用了软件、看到了软件对工作的影响,人们才会想明白自己想要什么。
  3. 所以我们需要再code review做得更好,要有更多"守望者",而不是期望一开始就有一个完美的架构,能很轻松地写代码、最少的代码量,并且又健壮,并且又智能地不出bug。
  4. 重构,就帮我们做到这些。与其猜测未来需要哪些灵活性、需要什么机制来提供灵活性,我更愿意只根据当前的需求来构造软件。
    1. 如果一种灵活性机制不会增加复杂度(比如添加几个命名良好的小函数),我可以很开心地引入它,但如果一种灵活性会增加软件复杂度,就必须先证明自己值得被引入。
    2. 如果不同的调用者不会传入不同的参数值,那么就不要添加这个参数。当真的需要添加这个参数时,重构也很容易。
    3. 要判断是否应该未未来的变化添加灵活性,我会评估"如果以后再重构有多困难",只有当未来重构会很困难时,我才考虑现在就添加灵活性机制。
  5. 以上这种设计方法叫做:简单设计、增量式设计、YAGNI。并不是说不做架构性思考,而是应该将架构、设计、开发过程三者融为一种工作方式,这种工作方式需要有重构作为基础。
  6. YAGNI,意思是You Ain't Gonna Need It
    1. Always implement things when you actually need them, never when you just foresee that you need them
      Screenshot from 2021-12-02 16-37-16

11.重构与软件开发过程

  1. 重构一开始是作为极限编程(XP)的一部分被人们接受,XP本身就融合了:持续集成、自测体系(这三者之间有很强的协同效应)。现在每个公司都说自己敏捷开发的**,但是真正敏捷的项目,团队成员必须在重构上有能力、有热情。
  2. code review,团队更有凝聚力,帮助这个"重构"更好地完成

12.自动化重构

  1. IDE的工具,refactor、查找替换等等
  2. 静态语言中,很多重构手法更加安全。

https://item.jd.com/12584498.html
https://en.wikipedia.org/wiki/Martin_Fowler_(software_engineer)

mysql-为什么字段不要用null

前言
曾经遇到两个与mysql字段为null导致的问题,这都是团队现实中遇到的。
1. 一个字段允许null,程序A写进数据库为空字符串,程序B判断字段是否为空是通过is null,所以对“空”有不一样的判断,导致故障。
2. 一个字段允许null,在一次统计该字段某范围的数量时,比预期的少了很多,后面发现是字段为null,范围条件的作用不起效。
null在以上的业务字段其实并没有发挥特殊的作用,完全可以使用其他默认值代替,这样也就可以避免以上的问题出现。

空字符串empty string和null的区别
empty string 就是'',对于字符串类型的默认值我们一般会default ''或者default null。
使用null你可以区分写入的时候是没有写入还是写入了空字符串,如果使用empty string就区分不了了(但是我们一般不用区分。)

为什么这么多人在用NULL?
1. null是创建数据表时默认的,所以不知情或者不够了解或者怕麻烦的程序员不会注意到这一点
2. 有些程序员以为not null需要更多的空间
3. 更加糟糕的是,业务上根本没有null,default也不是null,但是还是允许null。

官方文档说明

NULL columns require additional space in the rowto record whether their values are NULL. For MyISAM tables, each NULL columntakes one bit extra, rounded up to the nearest byte.

《高性能mysql第二版》的观点:

Mysql难以优化引用可空列查询,它会使索引、索引统计和值更加复杂。可空列需要更多的存储空间,还需要mysql内部进行特殊处理。可空列被索引后,每条记录都需要一个额外的字节,还能导致MyISAM中固定大小的索引变成可变大小的索引。

不使用NULL的理由
为了性能
1. 如果字段中有null则索引不生效?
这是谣言。null列是可以用到索引的,不管是单列索引还是联合索引。如果where条件中出现is null、is not null、!=这些条件仍然可以使用索引,本质上都是优化器去计算一下对应的二级索引数量占所有记录数量的比值而已。
2. null值到非null的更新无法做到原地更新,更容易发生索引分裂,从而影响性能。但是注意:把null列改为not null带来的性能提示很小,除非确定它带来了问题,否则不要把它当成优先的优化措施,最重要的是使用的列的类型的是适当性。

节省空间(索引长度)
字段为null值的索引会需要额外的空间来存储,即每行1字节的大小。对于相同数据的表,字段中有null值得表比not null的大。
image
image
key_len的计算规则和三个因素有关:数据类型、字符编码、是否为NULL
key_len 62 = 203(utf8 3字节) + 2(存储varchar变长字符长度2字节,定长字段无需额外的字节)
key_len 83 = 20
4(utf8mb4 4字节) + 1(是否为null的标识)+2(存储varchar变长字符长度2字节,定长字段无需额外的字节)
所以索引字段最好不要用null,会使索引、索引统计和值更加复杂,并且需要额外一个字节的存储空间。

与我们期望不相符
1. 当你用用>、<、!=这些的时候注意字段有没有特殊内容,例如null,因为他是没办法比较,所以大于还是小于,都匹配不上。
2. NOT IN子查询在有null值得情况下返回永远为空结果,查询容易出错。例如,
select name from table1 where name not in(select name from table2 where id != 1)
1. count(), max(), min()是忽略null的,而count()是包含null值得,那么count(col)和count()结果是不一样的。
2. 两个字符串拼接,如果包含null值,则返回结果为null。例如,
select concat(name_not_null, name_null) from table; -- out: null
select concat(name_not_null, name_not_null2) from table; -- out:xxxx

对开发的影响
1. 将来将这个表的数据转为php程序的数据时,整数列有NULL会转为0吗,字符列会转为''吗?或者浮点型会转为0.0之类的吗,所以这个是不确定的。所以建议:在创建表时,每个字段都不要设置NULL。而应该为NOT NULL,然后使用default指定默认值。
2. null值是不相等的,对于业务表述可能会有影响。不能='null' 只能is null,不利于开发的编写。当然该null的时候还是要null,但是很多业务场景其实用不到的,获取其他办法有更好的实现的。可以这样说,所有使用null值的情况,都可以通过一个有意义的值来表示,这样有利于代码的可读性和维护性,并能从约束上增强业务数据的规范性。比如0, ''空字符串等,如果是datetime类型,可以设置为'1970-01-01 00:00:00'这样的特殊值。

权限管理系统设计

1.权限管理系统的三要素

  1. 账号、角色、权限。
  2. 以前我们是只有账号和权限直接关联,这是最简单,但是不适用,会发现比较繁琐

角色

  1. 角色是搭建在账号和权限之间的一道桥梁。系统中会有很多权限,如果每建一个账号都要配置一遍权限,那么就非常繁琐。
  2. 角色把需要的权限收归于其中,然后再根据账号的需要来配置角色。通常会根据不同的部门,职位,工作内容等对角色进行设置。
  3. 角色这一概念的引入极大地增加了权限管理系统配置的灵活性和便捷性。创建账号时,可以将不同的角色配置在同一个账号上,也可以给不同的账号配置相同的角色。创建角色时,可以根据角色的差异赋予不同的权限。
  4. 角色不仅仅是桥梁,更多的是权限的集合、也有人的集合

权限

  1. 权限可以分三大类:数据权限、操作权限、页面权限。
  2. 权限可以是资源的集合,也可以就是资源,就看你怎么安排
  3. 数据权限,控制账号可看到的数据范围。例如不同区域只会看到不同区域的数据。也可能是多少个字段。
  4. 页面权限,控制账号可以看到的页面,通常系统都会有这一层权限控制。但是比较粗
  5. 操作权限。细到每一个接口了
  6. 所以权限表应该加一个权限类型的字段
  7. 我们这里的权限不是某个接口,而是某个接口的集合,也就是有接口表、权限表、接口-权限关联表
    Screenshot from 2021-07-29 16-24-40

2.RBAC模型

  1. RBAC(Role-Based Access Control)意思是基于角色的权限控制,有别于传统模型中直接把权限赋予账号
  2. RBAC模型根据设计需要,可分为RBAC0、RBAC1、RBAC2、RBAC3四种类型。

Screenshot from 2021-07-29 16-34-34

3.RBAC0模型的三种扩展

Screenshot from 2021-07-29 16-36-58

3.1.RBAC1模型(角色分级模型)

  1. 该模型在RBAC0模型的基础下,引入了角色间的集成关系。将角色划分层级,每个层级拥有的权限不同,每个层级的下级角色只能按照系统的设置,继承上级角色的部分权限。以此来对角色的关系进行精细化管理。
  2. 例如,风控经理、风控主管、风控专员可分为三个不同层级。风控主管拥有风控经理的部分权限,而风控专员拥有风控主管的部分权限。

3.2.RBAC2模型(角色限制模型)

  1. 该模型也是以RBAC0模型为基础,引入了角色间的限制条件,共有4种限制条件
  2. 角色互斥:在系统的互斥角色集合中存在互相制约的角色,这些角色不能分配给同一个账号。例如,风控系统中的尽调角色和审核角色即为互斥角色,不能配置在同一个账号上
  3. 基数限制。某个角色被分配给账号的数量有限,不允许超过数量限制的账号拥有该角色。例如,专门为公司某个职位的高管建立的角色(CEO)
  4. 先决条件限制。即账号想要获得高层级的角色,则需要先拥有低层级的角色。例如,先拥有风控主管的权限,才能拥有风控经理的权限
  5. 运行限制。允许一个账号拥有两个角色的权限,但是运行时只能激活其中一个

3.3.RBAC3(统一模型)

  1. 同时包含RBAC1和RBAC2的特性,既有角色层级划分,也有各种限制。

4.用户组

  1. 角色就是权限的集合、人的集合的桥梁。但其实更多是权限的集合,你可以理解为权限的集合
  2. 那么用户是不是也应该有用户的集合呢?的确是的,当用户基数比较大,角色类型过多时,为了方便管理可以引入"用户组"
  3. 例如,用户部门来分组。直接给分组关联角色
  4. 通俗的讲,大体分为,权限、权限集合、人、人的集合

经验

  1. 我准备设计的就是按照这个规范,但是也会引入用户组(人的集合)
  2. 应该算是RBAC1模型。角色会有分级。另外用户组也会分级。角色分级是控制权限(包括三种权限类型),但是用户组也会分层级但是基本上是和角色对应,这里不会做权限的分级,因为角色已经分好级了,用户组的存在意义主要是方便同时管理多个人,然后关联不一样的角色,因为在角色这里已经分级了。注意:用户组、角色组,其中一个权限分级就好,大部分会在角色分级权限,用户组不参与分配权限,但是也有人相反,确定的是二选一。(注意,这里的角色组并不是说真的需要分组,有层级关系即可,因为角色本来就是权限的集合,不需要再集合了)
  3. 同时我会把用户-用户组映射表、用户-角色映射表、用户组-角色映射表、角色-权限映射表,直接单独表这样更方便查询。但是用户或用户组不会直接与权限关联,都需要角色这个桥梁。所以一共有:用户表、用户组表、用户-用户组关联表、角色表、用户-角色关联表、用户组-角色关联表、权限表、权限-角色关联表、接口表、接口-权限关联表(操作权限)、页面表、页面-权限关联表(页面权限)、数据表、数据-权限关联表(数据权限,包括文件、菜单、字段);总结:人、人的集合、角色、角色的分级、权限(接口的集合、页面的集合、数据的集合)、接口、页面、数据
  4. 用户组按照部门去分,角色按照职位去分
  5. 操作权限会按照rest风格设置,资源+方法
  6. 下图是一个比较全面的RBAC1模型(这就是是我们想要的,但是角色这里会变成层级关系而不是组,这样更加简单)

Screenshot from 2021-07-29 19-13-59

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.