Yomut

Yomut 查看完整档案

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

个人动态

Yomut 关注了用户 · 10月25日

CrazyCodes @crazycodes

https://github.com/CrazyCodes... 我的博客
_
| |__ __ _
| '_ | | | |/ _` |
| |_) | |_| | (_| |
|_.__/ \__,_|\__, |

         |___/   感谢生命可以让我成为一名程序员

                         CrazyCodes To Author

关注 4673

Yomut 赞了文章 · 2月20日

重构——条件逻辑判断

1.案例分析

如何去除If,else,switch条件判断

对于具有一定复杂逻辑的代码实现,避免不了出现if,else,switch等逻辑判断。当逻辑分支越来越多的时候,大大地加大了阅读的难度。这种情况,我们该如何处理呢?

2.switch 与if else谁快

对同一个变量的不同值作条件判断时,可以用switch语句与if语句,哪个语句执行效率更高呢,答案是switch语句,尤其是判断的分支越多越明显。(具体测试的代码,小伙伴可以试一下)
    public static void main(String[] args) {
        testIF("12");
        testSwitch("12");
    }
    public static void testIF(String arg) {
        long t1 = System.nanoTime();
        if ("1".equals(arg)) {
            System.out.println(arg);
        } else if ("2".equals(arg)) {
            System.out.println(arg);
        } else if ("3".equals(arg)) {
            System.out.println(arg);
        } else if ("4".equals(arg)) {
            System.out.println(arg);
        } else if ("5".equals(arg)) {
            System.out.println(arg);
        } else if ("6".equals(arg)) {
            System.out.println(arg);
        } else if ("7".equals(arg)) {
            System.out.println(arg);
        } else if ("8".equals(arg)) {
            System.out.println(arg);
        } else if ("9".equals(arg)) {
            System.out.println(arg);
        } else if ("10".equals(arg)) {
            System.out.println(arg);
        } else if ("11".equals(arg)) {
            System.out.println(arg);
        } else if ("12".equals(arg)) {
            System.out.println(arg);
        } else if ("13".equals(arg)) {
            System.out.println(arg);
        } else if ("14".equals(arg)) {
            System.out.println(arg);
        } else {
            System.out.println(arg);
        }
        long t2 = System.nanoTime();
        System.out.println("test if : " + (t2 - t1));
    }
    public static void testSwitch(String arg) {
        long t1 = System.nanoTime();
        switch (arg) {
            case "1":
                System.out.println(arg);
                break;
            case "2":
                System.out.println(arg);
                break;
            case "3":
                System.out.println(arg);
                break;
            case "4":
                System.out.println(arg);
                break;
            case "5":
                System.out.println(arg);
                break;
            case "6":
                System.out.println(arg);
                break;
            case "7":
                System.out.println(arg);
                break;
            case "8":
                System.out.println(arg);
                break;
            case "9":
                System.out.println(arg);
                break;
            case "10":
                System.out.println(arg);
                break;
            case "11":
                System.out.println(arg);
                break;
            case "12":
                System.out.println(arg);
                break;
            case "13":
                System.out.println(arg);
                break;
            case "14":
                System.out.println(arg);
                break;
            default:
                System.out.println(arg);
                break;
        }
        long t2 = System.nanoTime();
        System.out.println("test switch: " + (t2 - t1));
    }
最终现实结果
12
test if : 482713
12
test switch: 24870

3.逻辑分支多为什么看起来费劲呢?

复杂!复杂!代码圈复杂度高!
什么是代码圈复杂度?

圈复杂度

1)概念:
  • 用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,根据经验,程序的可能错误和高的圈复杂度有着很大关系。
1)计算公式:
  • 计算公式为:V(G)=e-n+2。其中,e表示控制流图中边的数量,n表示控制流图中节点的数量。 其实,圈复杂度的计算还有更直观的方法,因为圈复杂度所反映的是“判定条件”的数量,所以圈复杂度实际上就是等于判定节点的数量再加上1,也即控制流图的区域数,对应的计算公式为:V(G)=区域数=判定节点数+1
  • 对于多分支的CASE结构或IF-ELSE结构,统计判定节点的个数时需要特别注意一点,要求必须统计全部实际的判定节点数,也即每个 ELSEIF语句,以及每个CASE语句,都应该算为一个判定节点。判定节点在模块的控制流图中很容易被识别出来,所以,针对程序的控制流图计算圈复杂度 V(G)时,最好还是采用第一个公式,也即V(G)=e-n+2;而针对模块的控制流图时,可以直接统计判定节点数,这样更为简单。

4.如何重构这样的代码呢?

1) NULL Object空对象模式

  • 描述: 当你在处理可能会出现null的对象时,可能要产生相对乏味的代码来做相应的处理,使用空对象模式可以接受null,并返回相应的信息。
  • 代码示例:
interface ILog {
     void log();
}
class FileLog implements ILog {
    public void log() {
    }
}
class ConsoleLog implements ILog {
    public void log() {
    }
}

class NullObjectLog implements ILog {
    public void log() {
    }
}
public class LogFactory {
    static ILog Create(String str) {
        ILog log = new NullObjectLog();
        if ("file".equals(str))
            log = new FileLog();
        if ("console".equals(str))
            log = new ConsoleLog();
        return log;
    }
}

2) 表驱动法(Table-Driven Approach)

  • 描述:表驱动法是一种设计模式,可用来代替复杂的if、else逻辑判断。
观察下面的一维数组的形式可以发现,定义了2个变量。
例如:int a[12],a[x]=y;
相当于函数 y=f(x) (在此例子中,x和y为均为int类型),于是就变成了我们平时熟悉的普通的c函数。而函数一般通过数学表达式和逻辑判断的形式得出结果,而这样的结果一般的来说有数学规律,例如像n!就很适合于使用函数实现。
而表驱动法的函数关系是人为定义的,如果采用函数,一般会出现很多的if、else判断。所以表驱动法适合于去实现“人造逻辑”的函数。
例子1:假设你要编写一个计算医疗保险费用的程序,其中保险费用是随着性别、年龄、婚姻状况 和是否吸烟而变化的。(这是代码大全书上的例子,原版是Pasca版本,方便阅阅读改成Java办)。这时候,第一反应可能就是一大堆的if-else语句,如下:
enum SexStatus {
    Female, Male
}
enum MaritalStatus {
    Single, Married
}
enum SmokingStatus {
    NonSmoking, Smoking
}
public double ComputeInsuranceCharge(SexStatus sexStatus, MaritalStatus maritalStatus, SmokingStatus smokingStatus, int age) {
    double rate = 1;
    if (sexStatus.equals(SexStatus.Female)) {
        if (maritalStatus.equals(MaritalStatus.Single)) {
            if (smokingStatus.equals(SmokingStatus.NonSmoking)) {
                if (age < 18) {
                    rate = 40.00;
                } else if (age == 18) {
                    rate = 42.50;
                } else if (age == 19) {
                    rate = 45.00;
                }
                ...
                else if (age > 65) {
                    rate = 150.00;
                }
            } else if (smokingStatus == SmokingStatus.Smoking) {
                if (age < 18) {
                    rate = 44.00;
                } else if (age == 18) {
                    rate = 47.00;
                } else if (age == 19) {
                    rate = 50.00;
                }
                ...
                else if (age > 65) {
                    rate = 200.00;
                }
            }
        } else if (maritalStatus == MaritalStatus.Married) {
            //......
        }
    }
    return rate;
}

但是仔细看一下代码,其实保险费率和性别、婚姻、是否抽烟、年龄这个几个因素有一定的关系,尤其年龄的变化区间是相当大,按照上述的写法,可想而知,代码的复杂会达到什么样子的程度。
这时候,肯定有人会想不需要对每个年龄进行判断,而且将保险费用放入年龄数组中,这样将极大地改进上述的代码。不过,如果把保险费用放入所有影响因素的数组而不仅仅是年龄数组的话,将会使程序更简单,类似于可以设计一个费率表格,来降低代码的复杂度呢。

  • 定义好费率表格之后, 你就需要确定如何把数据放进去。你可以用从文件中读入费率表格数据。
  • 当你建立好数据之后, 便做好了计算保险费用的一切工作。现在就可以用下面这个简单的语句来代替前面那个复杂的逻辑结构了。
Table<Integer, RateFactor, Double> rateTable = HashBasedTable.create();
enum RateFactor {
    MALE_SINGLE_NONSMOKING,
    MALE_SINGLE_SMOKING,
    MALE_MARRIED_NONSMOKING,
    MALE_MARRIED_SMOKING,
    FEMALE_SINGLE_NONSMOKING,
    FEMALE_SINGLE_SMOKING,
    FEMALE_MARRIED_NONSMOKING,
    FEMALE_MARRIED_SMOKING,
}
public double ComputeInsuranceCharge(RateFactor rateFactor, int age) {
    int ageFactor;
    if (age < 18) {
        ageFactor = 0;
    } else if (age > 65) {
        ageFactor = 65 - 17;
    } else {
        ageFactor = age - 17;
    }
    return rateTable.get(ageFactor, rateFactor);
}
例子2:前端语言可以采用表驱动法简化逻辑吗?

原先代码:

switch (something) {  
  case 1:  
    doX();  
  break;  
  case 2:  
    doY();  
  break;  
  case 3:  
    doN();  
  break;  
  // And so on...  
}  

重构后代码:

 var cases = {   
   1: doX,   
   2: doY,   
   3: doN  
 };  
 if (cases[something]) {   
      cases[something]();  
 }  

关于表驱动法,还是其他灵活的使用方法,可以参考《代码大全》

3) 继承子类的多态

使用继承子类的多态,它们使用对象的间接性有效地摆脱了传统的状态判断。
使用继承子类多态的方式,通常对于某个具体对象,它的状态是不可改变的(在对象的生存周期中)。

  • 原先代码:
public class Method {
    private int type;
    public static final int POST = 0;
    public static final int GET = 1;
    public static final int PUT = 2;
    public static final int DELETE = 3;
    public Method(int type) {
        this.type = type;
    }
    public String getMethod() throws RuntimeException {
        switch (type) {
            case POST:
                return "这是 POST 方法";
            case GET:
                return "这是 GET 方法";
            case PUT:
                return "这是 PUT 方法";
            case DELETE:
                return "这是 DELETE 方法";
            default:
                throw new RuntimeException("方法类型调用出错");
        }
    }
}
  • 重构方法: 现在使用四个子类分别代表四种类型的方法。这样就可以使用多态将各个方法的具体逻辑分置到子类中去了

4) 使用state模式

如果希望对象在生存周期内,可以变化自己的状态,则可以选择state模式。
重构方法:这里抽象状态为一个接口MethodType,四种不同的状态实现该接口。

查看原文

赞 3 收藏 0 评论 0

Yomut 关注了用户 · 2019-11-29

TIGERB @tigerb

// Trying to be the person you want to be.

// 时刻夯实基础
// 时刻对新技术保持热忱

// 个人博客 http://TIGERB.cn
// 轻量级PHP框架EasyPHP 作者 http://easy-php.tigerb.cn
// 电商设计手册|SkrShop 作者 https://github.com/skr-shop/m...

// 新的目标成为一名优秀的 Gopher

关注 2050

Yomut 关注了专栏 · 2019-11-06

好好学java

欢迎关注微信公众号【程序员的技术圈子】,分享历时三个月总结的【Java 面试学习指南】,已经拿到了大厂offer,整理成了一本电子书。免费分享大家,在公众号【程序员的技术圈子】,回复【Java面试】即可获取

关注 3686

Yomut 赞了文章 · 2019-09-29

Nginx+PHP-FPM优化技巧总结

Nginx+PHP-FPM优化技巧总结

这里是从网上找到的一片文章,认真的实践了一遍,有很多值得参考的地方可以学习,由于之前的文章排版非常混乱,所以本人一边学习一边加重写整理此文,所有版权归原作者所有

Unix域Socket通信

之前简单介绍过Unix Domain Socket这种通信方式,参见:Nginx+PHP-FPM的域Socket配置方法
UnixSocket因为不走网络,的确可以提高Nginxphp-fpm通信的性能,但在高并发时会不稳定。

Nginx会频繁报错:

connect() to unix:/dev/shm/php-fcgi.sock failed (11: Resource temporarily unavailable) while connecting to upstream

可以通过下面两种方式提高稳定性:

1.调高nginxphp-fpm中的backlog
配置方法为:在nginx配置文件中这个域名的server下,在listen 80后面添加default backlog=1024
同时配置php-fpm.conf中的listen.backlog1024,默认为128
2.增加sock文件和php-fpm实例数再新建一个sock文件,在Nginx中通过upstream模块将请求负载均衡到两个sock文件
背后的两套php-fpm实例上。

php-fpm参数调优

2.1进程数

# php-fpm初始/空闲/最大worker进程数
 pm.max_children = 300
 pm.start_servers = 20
 pm.min_spare_servers = 5
 pm.max_spare_servers = 35

2.2最大处理请求数

最大处理请求数是指一个php-fpmworker进程在处理多少个请求后就终止掉,master进程会重新respawn一个新的。
这个配置的主要目的是避免php解释器或程序引用的第三方库造成的内存泄露

pm.max_requests = 10240

2.3最长执行时间

最大执行时间在php.iniphp-fpm.conf里都可以配置,配置项分别为max_execution_timerequest_terminate_timeout
其作用及其影响参见:Nginx中502和504错误详解

php-fpm的高CPU使用率排查方法

1. CPU使用率监控方法

top命令:
直接执行top命令后,输入1就可以看到各个核心的CPU使用率。而且通过top -d 0.1可以缩短采样时间。
下面的sar貌似最短只能是1秒

sar命令:

# sar和iostat命令的安装:
 sysstat.x86_64 : The sar and iostat system monitoring commands
 yum install -y sysstat.x86_64
 
# 执行sar -P ALL 1 100。-P ALL表示监控所有核心,1表示每1秒采集,100表示采集100次。
# 输出结果如下:
CPU     %user     %nice   %system   %iowait    %steal     %idle
all     85.54      0.00      5.69      0.00      0.00      8.76
  0     74.75      0.00     25.25      0.00      0.00      0.00
  1     98.00      0.00      2.00      0.00      0.00      0.00
  2     89.22      0.00      3.92      0.00      0.00      6.86
  3     91.00      0.00      2.00      0.00      0.00      7.00
  4     75.00      0.00      9.00      0.00      0.00     16.00
  5     94.95      0.00      5.05      0.00      0.00      0.00
  6     95.00      0.00      4.00      0.00      0.00      1.00
  7     87.88      0.00      4.04      0.00      0.00      8.08
  8     93.94      0.00      3.03      0.00      0.00      3.03
  9     88.00      0.00      3.00      0.00      0.00      9.00
 10     89.11      0.00      2.97      0.00      0.00      7.92
 11     82.35      0.00      3.92      0.00      0.00     13.73
 12     73.27      0.00      7.92      0.00      0.00     18.81
 13     81.44      0.00      4.12      0.00      0.00     14.43
 14     77.23      0.00      6.93      0.00      0.00     15.84
 15     78.79      0.00      4.04      0.00      0.00     17.17
 

2. 开启慢日志

配置输出php-fpm慢日志,阀值为2秒:

request_slowlog_timeout = 2
slowlog = log/$pool.log.slow

利用sort/uniq命令分析汇总php-fpm慢日志:

[root@boole log] grep -v "^$" www.log.slow.tmp | cut -d " " -f 3,2 | sort | uniq -c | sort -k1,1nr | head -n 50
   5181 run() /www/test.net/framework/web/filters/CFilter.php:41
   5156 filter() /www/test.net/framework/web/filters/CFilterChain.php:131
   2670 = /www/test.net/index.php
   2636 run() /www/test.net/application/controllers/survey/index.php:665
   2630 action() /www/test.net/application/controllers/survey/index.php:18
   2625 run() /www/test.net/framework/web/actions/CAction.php:75
   2605 runWithParams() /www/test.net/framework/web/CController.php:309
   2604 runAction() /www/test.net/framework/web/filters/CFilterChain.php:134
   2538 run() /www/test.net/framework/web/CController.php:292
   2484 runActionWithFilters() /www/test.net/framework/web/CController.php:266
   2251 run() /www/test.net/framework/web/CWebApplication.php:276
   1799 translate() /www/test.net/application/libraries/Limesurvey_lang.php:118
   1786 load_tables() /www/test.net/application/third_party/php-gettext/gettext.php:254
   1447 runController() /www/test.net/framework/web/CWebApplication.php:135
 
# 参数解释:
     sort:  对单词进行排序
     uniq -c:  显示唯一的行,并在每行行首加上本行在文件中出现的次数
     sort -k1,1nr:  按照第一个字段,数值排序,且为逆序
     head -10:  取前10行数据

3. 用strace跟踪进程

1.利用nohupstrace转为后台执行,直到attach上的php-fpm进程死掉为止:

nohup strace -T -p 13167 > 13167-strace.log &
# 参数说明:
-c 统计每一系统调用的所执行的时间,次数和出错的次数等.
-d 输出strace关于标准错误的调试信息.
-f 跟踪由fork调用所产生的子进程.
-o filename,则所有进程的跟踪结果输出到相应的filename
-F 尝试跟踪vfork调用.在-f时,vfork不被跟踪.
-h 输出简要的帮助信息.
-i 输出系统调用的入口指针.
-q 禁止输出关于脱离的消息.
-r 打印出相对时间关于,,每一个系统调用.
-t 在输出中的每一行前加上时间信息.
-tt 在输出中的每一行前加上时间信息,微秒级.
-ttt 微秒级输出,以秒了表示时间.
-T 显示每一调用所耗的时间.
-v 输出所有的系统调用.一些调用关于环境变量,状态,输入输出等调用由于使用频繁,默认不输出.
-V 输出strace的版本信息.
-x 以十六进制形式输出非标准字符串
-xx 所有字符串以十六进制形式输出.
-a column
设置返回值的输出位置.默认为40.
-e execve 只记录 execve 这类系统调用
-p 主进程号

2.也可以用利用-c参数让strace帮助汇总,非常方便非常强大!

[root@b28-12 log]# strace -cp 9907
Process 9907 attached - interrupt to quit
Process 9907 detached
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
56.61    0.016612           5      3121           read
11.11    0.003259           1      2517       715 stat
  8.04    0.002358           7       349           brk
  6.02    0.001767           1      1315           poll
  4.28    0.001255           6       228           recvfrom
  2.71    0.000796           1       671           open
  2.54    0.000745           0      2453           fcntl
  2.37    0.000696           1      1141           write
  1.69    0.000497           1       593        13 access
  1.37    0.000403           0      1816           lseek
  0.89    0.000262           1       451        22 sendto
  0.56    0.000163           1       276       208 lstat
  0.49    0.000145           0       384           getcwd
  0.31    0.000090           0      1222           fstat
  0.28    0.000082           0       173           munmap
  0.26    0.000077           0       174           mmap
  0.24    0.000069           2        41           socket
  0.23    0.000068           0       725           close
  0.00    0.000000           0        13           rt_sigaction
  0.00    0.000000           0        13           rt_sigprocmask
  0.00    0.000000           0         1           rt_sigreturn
  0.00    0.000000           0        78           setitimer
  0.00    0.000000           0        26        26 connect
  0.00    0.000000           0        15         2 accept
  0.00    0.000000           0        39           recvmsg
  0.00    0.000000           0        26           shutdown
  0.00    0.000000           0        13           bind
  0.00    0.000000           0        13           getsockname
  0.00    0.000000           0        65           setsockopt
  0.00    0.000000           0        13           getsockopt
  0.00    0.000000           0         8           getdents
  0.00    0.000000           0        26           chdir
  0.00    0.000000           0         1           futex
------ ----------- ----------- --------- --------- ----------------
100.00    0.029344                 18000       986 total

4.加速PHP解释执行

如果自己的程序的确没有问题,只是执行了太多操作,没法再做优化了。则考虑使用APCxcache等PHP加速器来减少CPU解释php文件的耗时。
这些PHP加速器在php文件第一次解释时会生成中间代码opcode,所以之后的执行会快很多,并且减少了一些CPU的运算。下面以xcache为例,
看下如何安装和配置。

安装xcache命令如下,./configure的参数好多不知道是做什么用的,官网上也没说明,所以只开启--enable-xcache了:

 tar zxvf xcache-3.0.3.tar.gz
     /usr/local/php/bin/phpize
     ./configure --with-php-config=/usr/local/php/bin/php-config --enable-xcache
     make
     make install

php.ini中配置如下,最重要的是标红的两个参数,一般推荐xcache.size根据php文件多少来定,xcache.countCPU核心数相同:

[xcache.admin]
xcache.admin.enable_auth = Off
xcache.admin.user = "xcache"
xcache.admin.pass = ""
 
[xcache]
xcache.shm_scheme ="mmap"
xcache.size=1024M
xcache.count =16
xcache.slots =8K
xcache.ttl=0
xcache.gc_interval =0
xcache.var_size=16M
xcache.var_count =1
xcache.var_slots =8K
xcache.var_ttl=0
xcache.var_maxttl=0
xcache.var_gc_interval =300
xcache.test =Off
xcache.readonly_protection = Off
;xcache.readonly_protection = On
xcache.mmap_path ="/dev/zero"
;xcache.mmap_path ="/tmp/xcache"
xcache.coredump_directory =""
xcache.cacher =On
xcache.stat=On
xcache.optimizer =Off
 
[xcache.coverager]
;;xcache.coverager =On
;;xcache.coveragedump_directory =""

常见问题是启动php-fpm时会报错:

Cannot open or create file set by xcache.mmap_path, check the path permission or check xcache.size/var_size against system limitation

这是因为/tmp/xcache是一个文件,而不能创建成目录。

重启php-fpm服务后,用top命令观察会发现每个worker进程的VIRT(包含了swap区)都是xcache.size大小,但REQ变得很小了。
使用上面的配置在使CPU使用率的峰值时间变短了,但峰值时还是所有核心都会达到90%以上,不知道是不是哪里没有配置对。
另外高并发时,/dev/zero这种配置方式经常会导致Nginx 502错误。/tmp/xcache和开启readonly_protection则很稳定。

php程序性能监控

常用的方法就是开启xdebug的性能监控功能,将xdebug输出结果通过WinCacheGrind软件分析。
xdebug的安装和配合IDE调试的方法参见:Vim+XDebug调试PHP

php.ini中配置的这几项是输出性能信息的:

xdebug.auto_trace = on
xdebug.auto_profile = on
xdebug.collect_params = on
xdebug.collect_return = on
xdebug.profiler_enable = on
xdebug.trace_output_dir = "/tmp"
xdebug.profiler_output_dir ="/tmp"

这样XDebug会输出所有执行php函数的性能数据,但产生的文件也会比较大。可以关闭一些选项如collect_params、collect_return,
来减少输出的数据量。或者关闭自动输出,通过在想要监控的函数首尾调用xdebug函数来监控指定的函数。

输出的文件名类似cachegrind.out.1277560600trace.3495983249.txt,可以拿到Windows平台下用WinCacheGrind进行图形化分析。
WinCacheGrind使用方法网上有很多介绍,这里就不详细说明了,WinCacheGrind for github

clipboard.png

结束语

以上都是近期做php程序优化工作总结出的一些优化方法,针对每个地方的配置请详细阅读官方文档进行修改,并不一定要以本文为依据,本文档只阐述方法

警告:本文所有版权归博客园toxic所有,本人只是自己学习对以前的文章进行动手实验和借助segmentfault强大的markdown编辑器重新排版,以便好的文章能够让自己更多的开发者学习和收益,此才是分享文章的真正意义!
参考文献: Nging+PHP-FPM优化总结

查看原文

赞 16 收藏 22 评论 0

Yomut 关注了用户 · 2019-07-18

huashiou @huashiou

本人在大数据领域具有多年的开发经验,对常用大数据技术都有所了解,在架构设计、高并发、分布式等方面具有一定经验。喜欢学习新技术,乐于分享,欢迎大家关注本博客。

关注 748

Yomut 关注了用户 · 2019-07-15

xiyanghui @xiyanghui

18年程序员,一个懂技术,爱技术的CEO

关注 17

Yomut 关注了专栏 · 2019-07-15

四魂之域

在学习和工作中成长。

关注 990

Yomut 赞了文章 · 2019-07-15

如何理解 Laravel 和 ThinkPHP 5 中的服务容器与注入?

从文档说起

很多人一开始看到官方的文档,无论是 Laravel 还是 ThinkPHP ,看完都是一头雾水,不求甚解。甚至都是直接跳过去,不看,反正我也不一样用得到这么高端的东西,如果在短时间内有这个念头很正常,尤其是习惯了 ThinkPHP 3 的使用者,相对引入的理念比较前沿,如果你在长时间内都不去考虑去理解,那就要看你自己的职业规划了。
接下来就来一起看一下,细细追品。

从 Laravel 开始

从 Laravel 的文档中看到有 bindsingleton 以及 instance ,这三个常用方法,接下来就一一解答。

实际应用

假设我们有这样一个场景,当我们用户在进行注册时,我们需要向用户手机发送一条短信验证码,然后当用户收到验证码后在注册表单提交时还需要验证验证码是否正确。

这个需求看起来非常容易实现,对吧?

当我们拿到短信平台的开发文档后,我们只需要写出两个方法。sendcheck 分别用来发送验证码和校验验证码,下面就在不用容器的情况下来写一下伪代码。

  • MeiSms.php
<?php


namespace App\Tools;


class MeiSms
{
    public function send($phone)
    {
        $code = mt_rand(1e3, 1e4 - 1);
        // TODO ... 通过接口发送
        // 存放验证码到 Redis
        $cacheManager = cache();
        // 设定 5 分钟失效
        $cacheManager->set('sms:' . $phone, $code, 5 * 60);
        return true;
    }

    public function check($phone, $code)
    {
        $cacheManager = cache();
        return $cacheManager->get('sms:' . $phone) === $code;
    }
}

很容易,不是吗?

然后在控制器中 new 一个 MeiSms 的实例,直接调用 sendcheck 就可以分别发送和检查验证码了。

但是,如果运营突然反馈说,之前给的短信平台不可靠,发送短信不稳定,用户经常收不到。

这时候我们就需要换一个接口,常见的方式就是 我们再写一个对象, 然后 又可能这个代码是别人写的让你来接收,你觉得 send 或者 check 这个方法名不够规范,然后你就给改了,然后顺带把原来的注册那边一并改了,然后代码就突突上线,跑起来了。

然后没过多久,运营又觉得这个平台的短信太贵了,另外又找到了一家既便宜又稳定的一家,然后你又重复了上面的事情,这次,方法这些你都觉得很完美,不用改动。

只是需要写一写方法体,然后在调用的地方改一些new 时的类名。

当然,这只是一个小例子,开发过程中我们可能还会遇到比这复杂的多的改动,又或者,运营又想让你换回之前的版本?emm。

在这里,如果有了解过简单工厂模式的朋友,可能会想到我可以使用简单工厂模式来搞定这个啊。

function factory($name)
{
    $modules = [
        'sms' => new MeiSms(),
    ];
    if (!isset($modules[$name])) {
        throw new \Exception('对象不存在。');
    }
    return $modules[$name];
}

在需要的地方直接调用 factory('sms') 这样就能拿到一个 发送短信的对象,当需求改了后,我直接改造一下工厂就好了,不是也简单了很多。

但是,到这里你会发现一些问题,工厂生产出来的对象没有类型提示,而且我们在工厂内没办法限制类必须要实现哪些方法(当然你可以把工厂搞的更复杂,加上接口校验),但是到头来你会发现,这里我们最初要做的事儿越来越远,而且,越来越复杂,不是吗?而且,工厂也不是那么的易用。

服务容器

这里就要看回我们的 服务容器 ,首先我们先看看控制器的文档中关于依赖注入部分的说明,这也是很多人最开始了解到 依赖注入 的地方。

  • 构造函数注入
Laravel 服务容器 解析所有的控制器。因此,你可以在控制器的构造函数中使用类型提示可能需要的依赖项。依赖声明会被自动解析并注入到控制器实例
  • 方法注入
处理构造函数注入,你还可以在控制器方法中输入类型提示依赖项。方法注入最常见的用例是在控制器方法中注入 Illuminate\Http\Request 的实例

当我们每次创建一个控制器方法,都会主动填写第一个参数,即 Request $request 你是否有注意过那个 Request 的参数呢?是不是很神奇呢?

为什么我什么都没有做,我就可以使用它, 而且,着并不限于 Laravel 内置的对象,我们自己写的对象也是可以的,而且在使用 IDE 开发时 我们还可以方便的使用类型提示,这些工作,就是 服务容器 帮我们做的。

当容器解析到这个方法时,当方法存在,就会用反射,来解析这个方法中需要的参数以及参数的类型。
ReflectionFunctionAbstract::getParameters,然后在容器中查找我们是否 bind 有这个类型,如果没有,就继续使用容器去创建这个类(因为这个类的构造方法中可能还会依赖其他的类)直到所依赖的类实例化完成,并且,把实例存到容器。

到这里你是不是觉得服务容器没有卵用?就是帮我们递归实例化类而已。那你就太年轻了,既然上面说到了我们要用 服务容器 来解决 简单工厂 所解决的问题,难倒我还会骗你不成?哈哈

这里就要开始说第一个了 bind

bind 方法

首先 我们来看一下 Laravel 中 bind 方法的实现。

    /**
     * Register a binding with the container.
     *
     * @param  string  $abstract
     * @param  \Closure|string|null  $concrete
     * @param  bool  $shared
     * @return void
     */
    public function bind($abstract, $concrete = null, $shared = false)
    {
        // If no concrete type was given, we will simply set the concrete type to the
        // abstract type. After that, the concrete type to be registered as shared
        // without being forced to state their classes in both of the parameters.
        $this->dropStaleInstances($abstract);

        if (is_null($concrete)) {
            $concrete = $abstract;
        }

        // If the factory is not a Closure, it means it is just a class name which is
        // bound into this container to the abstract type and we will just wrap it
        // up inside its own Closure to give us more convenience when extending.
        if (! $concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->bindings[$abstract] = compact('concrete', 'shared');

        // If the abstract type was already resolved in this container we'll fire the
        // rebound listener so that any objects which have already gotten resolved
        // can have their copy of the object updated via the listener callbacks.
        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }
    /**
     * Drop all of the stale instances and aliases.
     *
     * @param  string  $abstract
     * @return void
     */
    protected function dropStaleInstances($abstract)
    {
        unset($this->instances[$abstract], $this->aliases[$abstract]);
    }

    /**
     * Get the Closure to be used when building a type.
     *
     * @param  string  $abstract
     * @param  string  $concrete
     * @return \Closure
     */
    protected function getClosure($abstract, $concrete)
    {
        return function ($container, $parameters = []) use ($abstract, $concrete) {
            if ($abstract == $concrete) {
                return $container->build($concrete);
            }

            return $container->make($concrete, $parameters);
        };
    }

在一开始就调用了 $this->dropStaleInstances($abstract); 追踪源码我们看到,他直接删除了 第一个参数对应的已经存在的实例和别名。

然后接着往下,当 $concrete 不是一个 Closure (匿名方法) 时,他会去做一些包装处理成一个匿名方法,最后存入了 bindings 这个属性,键为 $abstract,值是一个数组,其中 concrete 是包装后的方法,然后调用了容器的 make 。。
当运行到这个位置

if ($this->resolved($abstract)) {
    $this->rebound($abstract);
}

会先去判断这个是否已经解析过了,进行更新已经存放在容器中的副本。

这就是 bind 所干的事儿,描述简单点儿,就是给一个类、类实例、匿名方法提供了一个别名绑定到了容器中去。

当我们使用 resolve 传入刚刚的别名时就能解析拿到我们之前绑定的实例。

赶紧来试试?

我们先打开 bootstrap/app.php,可以看到一开始,就创建了一个 Application 的实例,我们就试着在 $app 被 return 前面给绑定一下。

  • bootstrap/app.php:54
$app->bind('hello', \App\Tools\MeiSms::class);
return $app;
  • routes/web.php:23
Route::any('hello', function ()
{
    $resolve = resolve('hello');
    var_dump(get_class($resolve));
});

打开浏览器看一下

clipboard.png

不错吧,但是到了这里,我们只是做到了和简单工厂差不多的事情,接下来我们改造一下我们的短信类。

首先,我们约定一个接口,短信验证必须要发送短信和验证短信验证码两个方法,分别为 send($phone)check($phone,$code) 方法。

  • Sms.php
<?php


namespace App\Contracts\Interfaces;


interface Sms
{
    /**
     * @param string $phone 手机号
     * @return bool 是否发送成功
     */
    public function send(string $phone): bool;

    /**
     * @param string $phone 手机号
     * @param string $code 用户填写的验证码
     * @return bool 是否验证通过
     */
    public function check(string $phone, string $code): bool;
}

然后,我们用把之前的 MeiSms 类实现实现这个接口。

  • MeiSms.php
<?php


namespace App\Tools;


use App\Contracts\Interfaces\Sms;

class MeiSms implements Sms
{
    public function send(string $phone): bool
    {
        $code = mt_rand(1e3, 1e4 - 1);
        // TODO ... 通过接口发送
        // 存放验证码到 Redis
        $cacheManager = cache();
        // 设定 60 分钟失效
        $cacheManager->set('sms:' . $phone, $code, 5 * 60);
        return true;
    }

    public function check(string $phone, string $code): bool
    {
        $cacheManager = cache();
        return $cacheManager->get('sms:' . $phone) === $code;
    }
}

现在,我们再去 bootstrap/app.php 中注册,这次就跟以前的有点儿不一样了。

$app->bind(\App\Contracts\Interfaces\Sms::class, \App\Tools\MeiSms::class);
return $app;

可以看到,我们的第一个参数传递的时 Sms 的接口,要绑定上去的时 MeiSms 类,接着我们改造一下路由。

  • web.php
Route::any('hello', function (\App\Contracts\Interfaces\Sms $sms)
{
    var_dump(get_class($sms));
});
  • 结果

clipboard.png

你是不是拿刚刚的截图骗我?上面明明限定的是 \App\Contracts\Interfaces\Sms 怎么 打印出来的是 \App\Tools\MeiSms ,代码居然没有报错?

别惊讶,首先 \App\Tools\MeiSms 已经实现了 \App\Contracts\Interfaces\Sms 接口,所以在接口限定类型这是合法。

而因为这个方法调用时通过,容器进行调用的,容器会调用内部的 make 方法 进行一系列的依赖注入处理,当获取到方法需要一个 \App\Contracts\Interfaces\Sms 类型的参数时,容器将类名字符串到已经绑定中去查找,因为我们已经再前面注册过,所以就相当于实现了给类一个别名,最终还是由 \App\Tools\MeiSms 来执行结果给我们。

好处

那么回到议题,我们在考虑我们之前遇到问题,看上去已经解决了,那相比之前的方法有什么好处呢,一个个来讲。

  • 更好的规范
因为我们在路由那里限定了接口限定,所以我们不用再担心调用 send 或者 check 不存在了
不用再担心因为 send 或者 check 方法 返回值参数不知道怎么判断结果了(因为我们已经限定了只能返回 bool 值)
  • 不再改动原来的业务代码,更少的 bug
是的,没错。我们不再需要去改动现有的业务代码,只需要把新加入的类实现接口后绑定到容器即可,其他的都没有发生改变。
  • 更好的测试

补缺

当然,说到这里,你可能觉得我少说了什么东西。

singleton 方法

其实从源码很容易看到,singleton 还是调用了 bind 方法,只是 shared 参数 不一样,表示绑定了一个单例对象。

    /**
     * Register a shared binding in the container.
     *
     * @param  string  $abstract
     * @param  \Closure|string|null  $concrete
     * @return void
     */
    public function singleton($abstract, $concrete = null)
    {
        $this->bind($abstract, $concrete, true);
    }

instance 方法。

这个和 bind 几乎一样,只是 bind 可以绑定一个匿名方法或者直接类名(内部会处理)。
而 instance 正如其名字,用来绑定一个实例到容器里面去。

结束

在小范围看来,服务容器工厂模式有很多相似的地方,但是服务容器会让你接触到 PHP 的另一个知识块 反射,这个强大的 API 。

其实我也没想通,我为啥要用 Laravel 来举例写这篇文章。
因为看了一下 ThinkPHP 的实现,相对要好读 容易一些。
其实一开始我是准备把两个框架都说一下,但是感觉都又差不多,就挑了 Laravel ,虽然其中要复杂些,甚至很多点都没有照顾到,但是我还是把这个文章写出来了,不是吗?

也希望,这个文章对你在了解服务容器方面有所帮助。当然,我更加推荐你去 Laravel 或者 ThinkPHP 的源码,因为这样能够更加加深自己对其的理解。

补充

2019年11月9日

推荐扩展阅读

参考资料

查看原文

赞 35 收藏 22 评论 0

Yomut 关注了专栏 · 2019-07-14

掘金精选

掘金-最优质的互联网资源

关注 701

认证与成就

  • 获得 0 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-06-19
个人主页被 78 人浏览