归纳一些软件系统设计的经验和知识
固定窗口限流算法 首维护一个计数器，将单位时间段当做一个窗口，计数器记录这个窗口接收请求的次数。 当次数少于限流阀值，就允许访问，并且计数器+1 当次数大于限流阀值，就拒绝访问。 当前的时间窗口过去之后，计数器清零。 假设单位时间是1秒，限流阀值为3。在单位时间1秒内，每来一个请求,计数器就加1，如果计数器累加的次数超过限流阀值3，后续的请求全部拒绝。等到1s结束后，计数器清0，重新开始计数。如下图： 伪代码如下： /** * 固定窗口时间算法 * @return */ boolean fixedWindowsTryAcquire() { long currentTime = System.currentTimeMillis(); //获取系统当前时间 if (currentTime - lastRequestTime > windowUnit) { //检查是否在时间窗口内 counter = 0; // 计数器清0 lastRequestTime = currentTime; //开启新的时间窗口 } if (counter < threshold) { // 小于阀值 counter++; //计数器加1 return true; } return false; } 但是，这种算法有一个很明显的临界问题：假设限流阀值为5个请求，单位时间窗口是1s,如果我们在单位时间内的前0.8-1s和1-1.2s，分别并发5个请求。虽然都没有超过阀值，但是如果算0.8-1.2s,则并发数高达10，已经超过单位时间1s不超过5阀值的定义啦。 滑动窗口限流算法 滑动窗口限流解决固定窗口临界值的问题。它将单位时间周期分为n个小周期，分别记录每个小周期内接口的访问次数，并且根据时间滑动删除过期的小周期。 一张图解释滑动窗口算法，如下： 假设单位时间还是1s，滑动窗口算法把它划分为5个小周期，也就是滑动窗口（单位时间）被划分为5个小格子。每格表示0.2s。每过0.2s，时间窗口就会往右滑动一格。然后呢，每个小周期，都有自己独立的计数器，如果请求是0.83s到达的，0.8~1.0s对应的计数器就会加1。 我们来看下滑动窗口是如何解决临界问题的？ 假设我们1s内的限流阀值还是5个请求，0.8~1.0s内（比如0.9s的时候）来了5个请求，落在黄色格子里。时间过了1.0s这个点之后，又来5个请求，落在紫色格子里。如果是固定窗口算法，是不会被限流的，但是滑动窗口的话，每过一个小周期，它会右移一个小格。过了1.0s这个点后，会右移一小格，当前的单位时间段是0.2~1.2s，这个区域的请求已经超过限定的5了，已触发限流啦，实际上，紫色格子的请求都被拒绝啦。 TIPS: 当滑动窗口的格子周期划分的越多，那么滑动窗口的滚动就越平滑，限流的统计就会越。 滑动窗口算法伪代码实现如下： /** * 单位时间划分的小周期（单位时间是1分钟，10s一个小格子窗口，一共6个格子） */ private int SUB_CYCLE = 10; /** * 每分钟限流请求数 */ private int thresholdPerMin = 100; /** * 计数器, k-为当前窗口的开始时间值秒，value为当前窗口的计数 */ private final TreeMap<Long, Integer> counters = new TreeMap<>(); /** * 滑动窗口时间算法实现 */ boolean slidingWindowsTryAcquire() { long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //获取当前时间在哪个小周期窗口 int currentWindowNum = countCurrentWindow(currentWindowTime); //当前窗口总请求数 //超过阀值限流 if (currentWindowNum >= thresholdPerMin) { return false; } //计数器+1 counters.get(currentWindowTime)++; return true; } /** * 统计当前窗口的请求数 */ private int countCurrentWindow(long currentWindowTime) { //计算窗口开始位置 long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1); int count = ; //遍历存储的计数器 Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<Long, Integer> entry = iterator.next(); // 删除过期的子窗口计数器 if (entry.getKey() < startTime) { iterator.remove(); } else { //累加当前窗口的所有计数器之和 count =count + entry.getValue(); } } return count; } 滑动窗口算法虽然解决了固定窗口的临界问题，但是还是有一些问题：因为滑动窗口算法，需要将请求记录起来，然后下次请求来的时候，要把过期的请求清空，然后再计算数量，这个比较耗费时间，thinkphp6的Throttle中间件就是这个算法。 class Throttle { /** * 缓存对象 * @var Cache */ protected $cache; /** * 配置参数 * @var array */ protected $config = [ // 缓存键前缀，防止键值与其他应用冲突 'prefix' => 'throttle_', // 节流规则 true为自动规则 'key' => true, // 节流频率 null 表示不限制 eg: 10/m 20/h 300/d 'visit_rate' => null, // 访问受限时返回的http状态码 'visit_fail_code' => 429, // 访问受限时访问的文本信息 'visit_fail_text' => 'Too Many Requests', ]; protected $wait_seconds = 0; protected $duration = [ 's' => 1, 'm' => 60, 'h' => 3600, 'd' => 86400, ]; protected $need_save = false; protected $history = []; protected $key = ''; protected $now = 0; protected $num_requests = 0; protected $expire = 0; public function __construct(Cache $cache, Config $config) { $this->cache = $cache; $this->config = array_merge($this->config, $config->get('throttle', [])); } /** * 生成缓存的 key * @param Request $request * @return null|string */ protected function getCacheKey($request) { $key = $this->config['key']; if ($key instanceof \Closure) { $key = call_user_func($key, $this, $request); } if (null === $key || false === $key || null === $this->config['visit_rate']) { // 关闭当前限制 return; } if (true === $key) { $key = $request->ip(); } elseif (false !== strpos($key, '__')) { $key = str_replace(['__CONTROLLER__', '__ACTION__', '__IP__'], [$request->controller(), $request->action(), $request->ip()], $key); } return md5($this->config['prefix'] . $key); } /** * 解析频率配置项 * @param $rate * @return array */ protected function parseRate($rate) { list($num, $period) = explode("/", $rate); $num_requests = intval($num); $duration = $this->duration[$period] ?? intval($period); return [$num_requests, $duration]; } /** * 计算距离下次合法请求还有多少秒 * @param $history * @param $now * @param $duration * @return void */ protected function wait($history, $now, $duration) { $wait_seconds = $history ? $duration - ($now - $history[0]) : $duration; if ($wait_seconds < 0) { $wait_seconds = 0; } $this->wait_seconds = $wait_seconds; } /** * 请求是否允许 * @param $request * @return bool */ protected function allowRequest($request) { $key = $this->getCacheKey($request); if (null === $key) { return true; } list($num_requests, $duration) = $this->parseRate($this->config['visit_rate']); $history = $this->cache->get($key, []); $now = time(); // 移除过期的请求的记录 $history = array_values(array_filter($history, function ($val) use ($now, $duration) { return $val >= $now - $duration; })); if (count($history) < $num_requests) { // 允许访问 $this->need_save = true; $this->key = $key; $this->now = $now; $this->history = $history; $this->expire = $duration; $this->num_requests = $num_requests; return true; } $this->wait($history, $now, $duration); return false; } /** * 处理限制访问 * @param Request $request * @param Closure $next * @return Response */ public function handle($request, Closure $next) { $allow = $this->allowRequest($request); if (!$allow) { // 访问受限 $code = $this->config['visit_fail_code']; $content = str_replace('__WAIT__', $this->wait_seconds, $this->config['visit_fail_text']); $response = Response::create($content)->code($code); $response->header(['Retry-After' => $this->wait_seconds]); return $response; } $response = $next($request); if ($this->need_save && 200 == $response->getCode()) { $this->history[] = $this->now; $this->cache->set($this->key, $this->history, $this->expire); // 将速率限制 headers 添加到响应中 $remaining = $this->num_requests - count($this->history); $response->header([ 'X-Rate-Limit-Limit' => $this->num_requests, 'X-Rate-Limit-Remaining' => $remaining < 0 ? 0: $remaining, 'X-Rate-Limit-Reset' => $this->now + $this->expire, ]); } return $response; } public function setRate($rate) { $this->config['visit_rate'] = $rate; } } 令牌桶算法 面对突发流量的时候，我们可以使用令牌桶算法限流。 令牌桶算法原理： 有一个令牌管理员，根据限流大小，定速往令牌桶里放令牌 如果令牌数量满了，超过令牌桶容量的限制，那就丢弃。 系统在接受到一个用户请求时，都会先去令牌桶要一个令牌。如果拿到令牌，那么就处理这个请求的业务逻辑； 如果拿不到令牌，就直接拒绝这个请求 漏桶算法伪代码实现如下： /** * 每秒处理数（放入令牌数量） */ private long putTokenRate; /** * 后刷新时间 */ private long refreshTime; /** * 令牌桶容量 */ private long capacity; /** * 当前桶内令牌数 */ private long currentToken = 0L; /** * 漏桶算法 * @return */ boolean tokenBucketTryAcquire() { long currentTime = System.currentTimeMillis(); //获取系统当前时间 long generateToken = (currentTime - refreshTime) / 1000 * putTokenRate; //生成的令牌 =(当前时间-上次刷新时间)* 放入令牌的速率 currentToken = Math.min(capacity, generateToken + currentToken); // 当前令牌数量 = 之前的桶内令牌数量+放入的令牌数量 refreshTime = currentTime; // 刷新时间 //桶里面还有令牌，请求正常处理 if (currentToken > 0) { currentToken--; //令牌数量-1 return true; } return false; } 参考 https://z.itpub.net/article/detail/B049B6F216829EDD0827E97BC1AA9100
概述 抽象类和接口使用场景 抽象类是对一种事物的抽象，即对类抽象，而接口是对行为的抽象。 抽象类是对整个类整体进行抽象，包括属性、行为，但是接口却是对类局部（行为）进行抽象。 例子 举个简单的例子，飞机和鸟是不同类的事物，但是它们都有一个共性，就是都会飞。那么在设计的时候，可以将飞机设计为一个抽象类AirplaneAbstract，将鸟设计为一个抽象类BirdAbstract，但是不能将 飞行 这个特性也设计为类，因为它只是一个行为特性，并不是对一类事物的抽象描述。 此时可以将 飞行 设计为一个接口FlyInterFace，包含方法fly( )，然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机，比如战斗机、民用飞机等直接继承Airplane即可，对于鸟也是类似的，不同种类的鸟直接继承Bird类即可。 从这里可以看出， 继承是一个 "是不是"的关系 ，而 接口 实现则是 "有没有"的关系 。如果一个类继承了某个抽象类，则子类必定是抽象类的种类，而接口实现则是有没有、具备不具备的关系，比如鸟是否能飞（或者是否具备飞行这个特点），能飞行则可以实现这个接口，不能飞行就不实现这个接口。 // 对飞机这事务进行抽象，包括属性、行为 // 只要是飞机都有这些东西 abstract class AirplaneAbstract { // 飞机有机长 protected $captain; // 飞机有检票，具体各种飞机的检票方式不一样 // 例如：客机需要候机，战斗机刷卡 abstract public function checkIn(); } // 对鸟这事务进行抽象，包括属性、行为 // 只要是鸟都有这些东西，具体怎么吃，要看具体的鸟 abstract class BirdAbstract { // 鸟有性别 protected $sex; // 鸟要吃 abstract public function eat(); } // 对飞的行为的抽象 interface FlyInterFace { // public function fly(); } // 客机 class Airliner extends AirplaneAbstract implements FlyInterFace { public function checkIn() { echo "客机候机检票"; } public function fly() { echo "客机喷气飞行"; } } class Eagle extends GlobalBirdAbstract implements GlobalFlyInterFace { public function eat() { echo "老鹰抓小鸡"; } public function fly() { echo '老鹰翱翔'; } } 注意 需要注意的是，修改抽象类，不一定要修改其之类，例如添加抽象类的方法； 但是修改接口，实现改接口的类都必须修改。
概述 thinkphp6中，中间件是针对http组件来说的，在http组件入库方法runWithRequest里 // 加载全局中间件 $this->loadMiddleware(); // 执行全局中间件 return $this->app->middleware->pipeline()// pipeline方法里将中间件转为闭包 // 初始化 ->send($request) // 执行 ->then(function ($request) { // 最后执行路由分发 return $this->dispatchToRoute($request); }); // 加载基础目录下的中间件 protected function loadMiddleware(): void { if (is_file($this->app->getBasePath() . 'middleware.php')) { $this->app->middleware->import(include $this->app->getBasePath() . 'middleware.php'); } } 核心实现逻辑 通过中间件组件来实现，队列queue里有全局类型的中间件队列、route类型的队列、controller类型的队列 class Middleware { // 队列 protected $queue = []; /** *******添加中间件******** */ public function import(array $middlewares = [], string $type = 'global'): void { foreach ($middlewares as $middleware) { $this->add($middleware, $type); } } public function add($middleware, string $type = 'global'): void { $middleware = $this->buildMiddleware($middleware, $type); if (!empty($middleware)) // 队列里添加中间件 $this->queue[$type][] = $middleware; // 队列去重 $this->queue[$type] = array_unique($this->queue[$type], SORT_REGULAR); } } // 中间件可以是数组、闭包、字符串 protected function buildMiddleware($middleware, string $type): array { if (is_array($middleware)) { [$middleware, $params] = $middleware; } if ($middleware instanceof Closure) { // 闭包+参数 return [$middleware, $params ?? []]; } if (!is_string($middleware)) { throw new InvalidArgumentException('The middleware is invalid'); } //中间件别名检查 $alias = $this->app->config->get('middleware.alias', []); if (isset($alias[$middleware])) { $middleware = $alias[$middleware]; } if (is_array($middleware)) { $this->import($middleware, $type); return []; } // 类名+handle方法+参数，如果没有传入方法名称，默认是handle方法 return [[$middleware, 'handle'], $params ?? []]; } public function route($middleware): void { // route类型的队列 $this->add($middleware, 'route'); } public function controller($middleware): void { // controller类型的队列 $this->add($middleware, 'controller'); } /** *******管道执行中间件******** */ public function pipeline(string $type = 'global') { return (new Pipeline()) // through是把中间转为闭包，方便后续执行 ->through(array_map(function ($middleware) { // 返回中间件闭包数组，也就把中间件转为闭包 return function ($request, $next) use ($middleware) { // 调用中间件 [$call, $params] = $middleware; if (is_array($call) && is_string($call[0])) { $call = [$this->app->make($call[0]), $call[1]]; } // 必须有request、next参数、外加自己传入的参数 // next参数是下一个要执行的中间件闭包，所以在回调函数里必须被调用，不然后续无法执行 $response = call_user_func($call, $request, $next, ...$params); // 返回http响应类，因为最后一个执行的是dispatchToRoute if (!$response instanceof Response) { throw new LogicException('The middleware must return Response instance'); } return $response; }; }, $this->sortMiddleware($this->queue[$type] ?? [])))// 排序中间件，并且传给array_map，用于把中间件转为闭包 // 设置如果有异常的时候，处理异常的方法名称 ->whenException([$this, 'handleException']); } public function handleException($passable, Throwable $e) { // 系统异常类 $handler = $this->app->make(Handle::class); $handler->report($e); return $handler->render($passable, $e); } } 管道类 class Pipeline { // 闭包形式的中间件存在这里 protected $pipes = []; // 这种闭包管道 public function through($pipes) { $this->pipes = is_array($pipes) ? $pipes : func_get_args(); return $this; } // 执行管道 public function then(Closure $destination) { $pipeline = array_reduce( // 倒叙 array_reverse($this->pipes), $this->carry(), function ($passable) use ($destination) { try { return $destination($passable); } catch (Throwable | Exception $e) { return $this->handleException($passable, $e); } }); return $pipeline($this->passable); } protected function carry() { return function ($stack, $pipe) { return function ($passable) use ($stack, $pipe) { try { return $pipe($passable, $stack); } catch (Throwable | Exception $e) { return $this->handleException($passable, $e); } }; }; } } 管道的核心逻辑 简化下上述的管道逻辑，如下所示执行闭包： $middleware = [ function ($request1, $next) { echo 'request1闭包'; // $next为request2闭包 return $next($request1); }, function ($request2, $next) { echo 'request2闭包'; // $next为request3闭包 return $next($request2); }, function ($request3, $next) { echo 'request3闭包'; // $next为last闭包 return $next($request3); } ]; // 需要倒叙，因为闭包执行是按相反的顺序进行的 $middleware = array_reverse($middleware); $pipe = array_reduce($middleware, function ($stack, $pipe) { // $stack永远指向上一个闭包 return function ($passable) use ($stack, $pipe) { // 第一次执行：$stack=last闭包 // 第二次执行：$stack=request1闭包 // 第三次执行：$stack=request2闭包，此时$pipe为request3闭包 return $pipe($passable, $stack); }; }, function ($requestLast) { echo 'last闭包'; }); $pipe('request');
Thinkphp6的Facade门面详解 | Guide and Tutorial
概述 门面主要是为了方便调用类方法，可以像调用静态方法的方式调用非静态方法。例如 class Test { public function hello($name) { return 'hello,' . $name; } } // 这个类名不一定要和Test类一致，但通常为了便于管理，建议保持名称统一 class Test extends Facade { protected static function getFacadeClass() { return 'app\common\Test'; } } // 无需进行实例化 直接以静态方法方式调用hello \app\facade\Test::hello('thinkphp'); 核心实现逻辑 主要是利用魔术方法、容器技术实现 class Facade { // 用于配置是否单例模式 protected static $alwaysNewInstance; // 魔术方法 public static function __callStatic($method, $params) { // 调用对象的方法 return call_user_func_array([static::createFacade(), $method], $params); } // 可以为其它类增加门面功能 protected static function getFacadeClass() {} // protected static function createFacade(string $class = '', array $args = [], bool $newInstance = false) { // 默认只需要继承这个类的话，本类就有门面功能 $class = $class ?: static::class; // 可以为其它类设置门面功能 $facadeClass = static::getFacadeClass(); if ($facadeClass) { $class = $facadeClass; } // 是否单例模式 if (static::$alwaysNewInstance) { $newInstance = true; } // 容器单例 return Container::getInstance()->make($class, $args, $newInstance); } }
概述 tp6的事件，是通过观察者模式设计的，事件的基本原理是，首先在系统初始化的时候注册一些监听事件，然后观察事件的触发，只要事件触发的时候就就能执行这些观察者。 事件机制再不修改框架代码的情况下，提升了框架的扩展性。 源码解析 别名功能 事件的别名，触发的时候可以直接触发这个别名，系统自定义的基本事件别名已经写死在事件类的属性里了。 class Event { // 事件的别名，触发的时候可以直接触发这个别名 protected $bind = [ 'AppInit' => event\AppInit::class, ]; // 批量注册别名 public function bind(array $events) { $this->bind = array_merge($this->bind, $events); return $this; } } 事件配置，因为事件相关的配置，最好放在一个配置文件里，所以通过key来分开 return [ 'bind' => [ 'UserLogin' => 'app\event\UserLogin', ], ]; 然后系统初始化加载配置的时候，会批量加载别名，配置文件如下 $this->event->bind($event['bind']); 绑定别名后，就可以通过别名触发事件 $this->event->trigger('AppInit'); 当然这个别名不设置也是可以的，只要触发的时候，直接触发事件类名称就好了，不要触发别名，下面触发事件的方式和上面别名触发的方式效果是一样的 $this->event->trigger(AppInit::class); 需要注意的是，这个别名最好是大写开头，因为事件自动订阅模式，截取on开头后，第一个字符是大写，详情见下面的事件订阅 事件的监听 事件的监听就是观察者，是事件的核心 class Event { // 监听类的类名 protected $listener = []; // 批量注册监听 public function listenEvents(array $events) { foreach ($events as $event => $listeners) { if (isset($this->bind[$event])) { $event = $this->bind[$event]; } $this->listener[$event] = array_merge($this->listener[$event] ?? [], $listeners); } return $this; } // 单个监听 public function listen(string $event, $listener) { if (isset($this->bind[$event])) { $event = $this->bind[$event]; } $this->listener[$event][] = $listener; return $this; } } 由上我门可以知道，监听基本的参数是事件的名称、监听类（到时候会执行监听类里的handle方法）或者闭包，例如批量监听的时候，是通过系统配置的方式 return [ 'listen' => [ 'UserLogin' => ['app\listener\UserLogin'], ], ]; 这个配置文件，会在系统初始化的是被批量注册 $this->event->listenEvents($event['listen']); 当然，我们还可以注册单个 // 类名称，触发的时候会执行UserLogin里的handle方法 Event::listen('UserLogin', UserLogin::class); // 闭包 Event::listen('UserLogin', function($user) { // }); // 对象的具体方法 $this->listen('UserLogin', [$userLoginObject, 'onUserLogin']); 订阅功能 订阅的概念跟关注差不多，类似一次性可以关注多个明星，因此我们也可以一次性订阅多个事件，例如用户相关的事件，可以放在用户订阅类里，这样好管理事件的监听 // 事件类 class Event { // 批量订阅，可以传入字符串或者对象 public function subscribe($subscriber) { // 如果不是数组的，转成数组，支持订阅单个 $subscribers = (array) $subscriber; foreach ($subscribers as $subscriber) { if (is_string($subscriber)) { $subscriber = $this->app->make($subscriber); } // 如果订阅类里有subscribe方法，代表需要手动订阅 if (method_exists($subscriber, 'subscribe')) { // 手动订阅，在该方法里，手工动态监听事件 $subscriber->subscribe($this); } else { // 智能订阅，根据规则，自动注册事件监听 $this->observe($subscriber); } } return $this; } // 根据方法on+事件别名名称的方法，自动注册对应事件监听 public function observe($observer, string $prefix = '') { if (is_string($observer)) { $observer = $this->app->make($observer); } $reflect = new ReflectionClass($observer); $methods = $reflect->getMethods(ReflectionMethod::IS_PUBLIC); if (empty($prefix) && $reflect->hasProperty('eventPrefix')) { $reflectProperty = $reflect->getProperty('eventPrefix'); $reflectProperty->setAccessible(true); $prefix = $reflectProperty->getValue($observer); } foreach ($methods as $method) { $name = $method->getName(); if (0 === strpos($name, 'on')) { $this->listen($prefix . substr($name, 2), [$observer, $name]); } } return $this; } } 同样，我们可以同配置文件，批量订阅 return [ 'subscribe' => [ 'app\subscribe\User', ], ]; 系统初始化的时候，会加载配置文件 if (isset($event['subscribe'])) { $this->event->subscribe($event['subscribe']); } 当然，我们也可以动态订阅 Event::subscribe('app\subscribe\User'); 订阅类有subscribe方法，手动监听到指定的方法 class User { public function onUserLogin($user) { // UserLogin事件响应处理 } public function subscribe(Event $event) { $event->listen('UserLogin', [$this,'onUserLogin']); } } 订阅类没有subscribe方法，自动监听 class User { // UserLogin必须在事件别名 public function onUserLogin($user) { // UserLogin事件响应处理 } } 事件的触发 我们一般在可能需要扩展的地方触发响应的事件，触发逻辑如下所示 class Event { public function trigger($event, $params = null) { // 由此可知，触发可以传入事件类的对象 if (is_object($event)) { // 对象的话，把对象作为参数传入观察者 $params = $event; $event = get_class($event); } // 由此可知，触发可以传入事件类的别名 if (isset($this->bind[$event])) { $event = $this->bind[$event]; } $result = []; $listeners = $this->listener[$event] ?? []; $listeners = array_unique($listeners, SORT_REGULAR); // 执行事件的所有监听 foreach ($listeners as $key => $listener) { // 触发可以传入参数 $result[$key] = $this->dispatch($listener, $params); } // 可以返回每个监听者的返回值 return $result; } // 执行监听者 protected function dispatch($event, $params = null) { if (!is_string($event)) { // 闭包类型的监听者 $call = $event; } elseif (strpos($event, '::')) { // 静态方法类型的监听者 $call = $event; } else { // 类名称类型的监听者 $obj = $this->app->make($event); $call = [$obj, 'handle']; } return $this->app->invoke($call, [$params]); } } 可以通过别名触发 $this->event->trigger("AppInit"); 可以通过，监听类名称触发 $this->event->trigger(AppInit::class); 可以通过对象触发 $this->event->trigger(new UserLogin([ 'login_time'=>time()// 事件对象可以设置一些参数，如果事件对象有这个属性的话 ])); 可以通过静态方法触发，并且传入参数 $this->event->trigger("events\UserLogin::OnLogin",[$userId]);
深入理解设计模式 - 精通面向对象编程
设计模式 介绍 面向对象编程里有 6 大原则和 24 种设计模式。 什么是设计模式 设计模式是一套被反复使用、容易被他人理解的、可靠的代码设计经验的总结。 设计模式的目的是为了更好的代码重用性，可读性，可靠性和可维护性。 设计模式的种类 23 种设计模式里，可以按照以下 3 种进行分类： 创建型：如何创建对象 结构型：如何实现类和对象的组合的 行为型：类或者对象怎样交互，以及怎样分配职责 有一个“简单工厂模式”不属于Gof23种设计模式，但是大部分设计模式的书籍里，都会对它进行专门的介绍，所以目前设计模式的种类可以分为： GoF的23种设计模式+简单工厂模式 = 24种设计模式 六大原则 单一职责原则 (Single Responsibility Principle) 指的是：类和方法的职能要单一。这样做好处非常多：可以降低复杂度，提高代码的重用性、可靠性、可读性、可维护性。所以这个原则很重要，一定要满足！ 尽量避免方法功能的重复，不然不知道情况的人，就不知道调用哪个方法了，容易造成困扰。 对于php代码来说，就是class和method职责要单一 对于go语言来说，就是结构体和method职责要单一 但是，也要不能拆分的过细，不然类就会太多，耦合度就高，要追求高内聚，低耦合 package main import "fmt" type WorkerClothes struct { } func (w WorkerClothes) Style() { fmt.Println("穿工作服") } type ShopClothes struct { } func (s ShopClothes) Style() { fmt.Println("穿工作服") } func main() { // 每个类的职责单一，即使两个方法打印的结果都是一样，也不会歧义 // 只需要从类的职责上就可以发现，一个是工作服，一个是逛街衣服 wc := WorkerClothes{} wc.Style() sc := ShopClothes{} sc.Style() } 里是替换原则（Liskov Substitution Principle) 指的是：将一个基类对象替换成它的子类对象，程序将不会有任何问题，主要是对类继承的一种约束，这样做好处是不会破坏原来的职能，提升可靠性： 不能随便继承不合适的、有多余方法或属性的类。（例子 1） 子类可以扩展父类的功能（比如增加方法），但不能改变父类原有的功能 (比如覆盖父类非抽象方法)。（例子 2） package main import "fmt" // 抽象层 type Car interface { Run() } type Driver interface { Drive(car Car) } // 实现层，依赖抽象 type Benz struct { } func (b Benz) Run() { fmt.Println("benz is running...") } type Bwm struct { } func (b Bwm) Run() { fmt.Println("bwm is running....") } type ZhangSang struct { } func (z ZhangSang) Drive(car Car) { car.Run() } type LiSi struct { } func (l LiSi) Drive(car Car) { car.Run() } // 业务层，也要依赖抽象 func main() { var benz Car // 依赖抽象 benz = new(Benz) var zs Driver // 依赖抽象 zs = new(ZhangSang) zs.Drive(benz) var bwm Car // 依赖抽象 bwm = new(Bwm) var ls Driver // 依赖抽象 ls = new(LiSi) ls.Drive(bwm) } 依赖倒置原则 (Dependence Inversion Principle) 指的是：依赖于抽象接口，不要依赖具体实现类，也就是针对接口编程。 如果保证业务逻辑层向下依赖抽象层，实现层向上依赖抽象层，这样我们只需要关心抽象层有什么方法即可！！，如果依赖具体类，我们还得去看具体类有哪些方法。 package main import "fmt" // 抽象层 type Car interface { Run() } type Driver interface { Drive(car Car) } // 实现层，依赖抽象 type Benz struct { } func (b Benz) Run() { fmt.Println("benz is running...") } type Bwm struct { } func (b Bwm) Run() { fmt.Println("bwm is running....") } type ZhangSang struct { } func (z ZhangSang) Drive(car Car) { car.Run() } type LiSi struct { } func (l LiSi) Drive(car Car) { car.Run() } // 业务层，也要依赖抽象 func main() { var benz Car // 依赖抽象 benz = new(Benz) var zs Driver // 依赖抽象 zs = new(ZhangSang) zs.Drive(benz) var bwm Car // 依赖抽象 bwm = new(Bwm) var ls Driver // 依赖抽象 ls = new(LiSi) ls.Drive(bwm) } <?php interface IRead{ public function getConent(); } // 低层具体类 class Book extends IRead { public function getConent() { echo "很久很久以前"; }; } // 低层具体类 class Newspaper extends IRead { public function getConent() { echo "php的薪资很高"; }; } // 高层通用阅读类，可以阅读各种类型的文章内容 class YueDu { // 不应该直接依赖底层具体模块，例如Book、Newspaper类，而应该依他们的的接口 public read(IRead $iread) { $iread->getConent(); } } 接口隔离原则 (InterfaceSegregation Principles) 接口隔离和单一职责类似，只是接口隔离针对的是接口，接口里的方法都应该和该接口的职能强相关的。这样做的好处是，可以提升接口的重用性、可读性 但是需要注意的是：接口尽量小，但是也不能拆分的太小。 迪米特原则 (Law of Demeter) 又叫做最少知道原则，一个对象应该对其他对象保特最少的了解，要使用的时候只需要调用相应的方法即可。这样做可以提升代码的重用性、可读性、可维护性。 具体做法就是高内聚低耦合： 高内聚：将职能范围内的逻辑都封装在相应方法的里 (某一程度上说也算是单一职责原则) 低耦合：并且该方法要减少依赖，保持功能的独立性 开闭原则 (Open Close Principle) 很重要，写代码的时候，要注意啊自己的代码是否符合开闭原则。 指的是：对扩展开放，对修改关闭。这样做可以提升代码的可靠性。也就是对类的改动，是通过增加代码进行的，而不是修改源码 具体做法是：通过继承的方式，对原来的功能进行扩展，尽量不要去修改原来的功能，因为对旧代码进行修改，会容易出现问题，还要重新测试。 package main import "fmt" type AbstractBanker interface { DoBusi() } type SaveBanker struct { } func (s SaveBanker) DoBusi() { fmt.Println("进行了 存款业务") } // TranferBanker 新增转账业务，只需要增加代码即可，不需要修改原来的存款业务类 type TranferBanker struct { } func (s TranferBanker) DoBusi() { fmt.Println("进行了 转账业务") } func BankBusiness(banker AbstractBanker) { banker.DoBusi() } func main() { sb := SaveBanker{} sb.DoBusi() tb := TranferBanker{} tb.DoBusi() // 可以优化上面的代码 BankBusiness(&SaveBanker{}) BankBusiness(&TranferBanker{}) } 设计模式之-创建型模式 创建型模式的一个重要的思想其实就是封装，利用封装，把直接 new 获得一个对象，改为通过一个封装的类方法获得一个对象。 单例模式 （singleton） 我们把对象的生成从 new 改为通过静态方法的控制，使得我们总是返回同一个实例给调用者，确保了系统只有一个实例。 好处：使用单例模式，则可以避免大量的 new 操作消耗系统资源。 场景： 数据库连接实例使用单例模式，对数据库的操作很频繁，避免一直 new。 框架系统组件，可以设置为单例模式，保证整个框架都是只有一个对象 设计示例：3 私 1 公 class DbMysql { // 私有属性存储对象 private static $ins = null; // 私有化构造函数，避免被外部new private function __construct(){} // 公有静态方法，创建实例 public static function createIns() { if(self::$ins){ self::$ins = new self(); } return self::$ins; } // 私有化克隆方法，避免在类外克隆 private function __clone(){} } 工厂模式 工厂模式是我们最常用的实例化对象模式，是用静态工厂方法代替 new 操作的一种模式。 好处：如果你想要更改所实例化的类名等，则只需更改该工厂方法内容即可，不需逐一寻找代码中具体实例化的地方 (nw 处) 修改了。为系统结构提供灵活的动态扩展机制，减少了耦合。 简单工厂 一个工厂，创建多种对象。如果要创新新的对象，则需要修改工厂类的静态方法 // 简单工厂 class DbMysql { public function connect() { echo "连接mysql"; } } class DbSqlLite { public function connect() { echo "连接sqlLite"; } } class DbFactory { // 一个工厂，可以制造很多不同的商品 public static function createIns($type) { if ($type == 'mysql') { return new DbMysql(); } elseif ($type == 'sqlite') { return new DbSqlLite(); } } } $mysql = DbFactory::createIns('mysql'); $mysql->connect(); 工厂方法模式 每个对象都有专门的工厂类来创建。如果要新增一个类的对象，可以新增一个工厂类来创建，就不需要修改别的工厂，符合开闭原则 interface Db { public function connect(); } class DbMysql implements Db { public function connect() { echo "连接mysql"; } } class DbSqlLite implements Db { public function connect() { echo "连接sqlLite"; } } interface Factory { public static function createIns(); } class MysqlFactory implements Factory { public static function createIns() { return new DbMysql(); } } class SqliteFactory implements Factory { public static function createIns() { return new DbSqlLite(); } } $mysql = MysqlFactory::createIns(); 抽象工厂模式 一个工厂生产多种产品。例如百事公司的苹果汁、香蕉汁都放在百事工厂类里实现 //饮料接口 interface Fruit{ function getFruitName(); } class BaishiAppleFruit implements Fruit{ function getFruitName() { echo '百事苹果味饮料'; } } class BaishiBananaFruit implements Fruit{ function getFruitName() { echo '百事香蕉味饮料'; } } class ColeiAppleFruit implements Fruit{ function getFruitName() { echo '可口可乐苹果味饮料'; } } class ColeBananaFruit implements Fruit{ function getFruitName() { echo '可口可乐香蕉味饮料'; } } //工厂接口 interface FruITFactory{ //生产饮料方法 function makeAppleFruit(); function makeBananaFruit(); } //百事饮料工厂 class BaishiFruitFactory implements FruitFactory{ function makeAppleFruit() { //生产百事苹果饮料 return new BaishiAppleFruit(); } function makeBananaFruit() { //生产百事香蕉饮料 return new BaishiBananaFruit(); } } //可口可乐饮料工厂 class ColeFruitFactory implements FruitFactory{ function makeAppleFruit() { //生产可口可乐苹果饮料 return new ColeiAppleFruit(); } function makeBananaFruit() { //生产可口可乐香蕉味饮料 return new ColeBananaFruit(); } } $baishi_factory = new BaishiFruitFactory(); $baishi_factory->makeAppleFruit()->getFruitName(); $baishi_factory->makeBananaFruit()->getFruitName(); $cole_factory = new ColeFruitFactory(); $cole_factory->makeAppleFruit()->getFruitName(); $cole_factory->makeBananaFruit()->getFruitName(); 设计模式之-结构型模式 解析类和对象的内部结构和外部组合，通过优化程序结构解决模块之间的耦合问题。 适配器模式 比如 php 的返回的数据结构，java 不能解析，可以通过适配器，将返回结果转为 json，然后 java 就可以解析了。 interface Weather{ public function show(); } class PhpWeather implements Weather { public function show(){ $info = ['weather'=>'小雨','tep'=>'6']; // 不能直接改这个，不然违法开闭原则 return serialize($info); } } interface WeatherAdapter{ public function getWeather(); } class JavaWeather implements WeatherAdapter{ protected $weather; public function __construct(Weather $weather) { $this->weather = $weather; } public function getWeather(){ $info = unserialize($this->weather->show()); return json_encode($info); } } PHP 中的数据库操作有 MySQL, MySQLi, PDO 三种，可以用适配器模式统一成一致，使不同的数据库操作，统一成一样的 API。类似的场景还有 cache 适配器，可以将 memcache, redis, file, apc 等不同的缓存函数，统一成一致 装饰器模式 装饰器模式又叫装饰者模式。装饰模式是在不必改变原类文件（修改会违法开闭原则）和使用继承（继承会增加耦合）的情况下，动态地扩展一个对象的功能。它是通过创建一个包装对象，也就是装饰来包裹真实的对象。 interface IComponent { public function display(); } // 不对这个类进行修改和继承 class Person implements IComponent{ protected $name; public function __construct($name) { $this->name = $name; } public function display(){ echo "装饰的{$this->name}"; } } // 装饰类 class Clothes implements IComponent { protected $component; public function Decorate(IComponent $component) { $this->component = $component; } public function display() { if($this->component){ $this->component->display(); } } } class TShirt extends Clothes{ public function display() { echo "T恤"; parent::display(); } } class Shirt extends Clothes { public function display() { echo "衬衫"; parent::display(); } } $qd = new Person('乔丹'); $tShirt = new TShirt(); $tShirt->Decorate($qd); $tShirt->display(); $tShirt = new Shirt(); $tShirt->Decorate($qd); $tShirt->display(); 注册树模式 注册树模式也叫注册模式或注册器模式，顾名思义，注册树就是把对象实例注册到一棵全局的对象树上，需要对象的时候时候就从树上取下来，就好比树上涨的果子，需要的时候就摘一个下来，只是这个对象树果子是摘不完的。 注册树是控制反转（IOC）的思想： 控制：对象创建，属性赋值，对象生命周期管理【Bean 的生命周期】 反转：把管理对象的权限转移给了容器实现，由容器完成对象的管理 正转：使用 new 构造方法创建对象，开发人员掌握了对象的全部过程 <?php class DbMysql{ public function connect() { echo "连接mysql"; } } class DbSqlite { public function connect() { echo "连接mysql"; } } // 注册树（IOC控制反转的思想） class RegisterTree{ protected static $object = []; public static function set($alias,$object) { self::$object[$alias] = $object; } public static function get($alias) { return self::$object[$alias]; } } RegisterTree::set('DbMysql',new DbMysql()); RegisterTree::set('DbSqlite',new DbSqlLite()); $mysql = RegisterTree::get('DbMysql'); $mysql->connect(); 门面模式 门面模式 (Facade) 又称外观模式，用于为子系统中的一组接口提供一个一致的界面门面模式定义了一个高层接口，这个接口使得子系统更加容易使用：引入门面角色之后，用户只需要直接与门面角色交互，用户与子系统之间的复杂关系由门面角色来实现，从而降低了系统的耦合度。 有了门面之后，我们一般不通过注册树进行调用组件，可以通过门面统一来调用，隐藏内部复杂的逻辑，用起来比较方便。 <?php class Camera{ public function turnOn() { echo "打开相机"; } public function turnOff() { echo "关闭相机"; } } class CameraFacade{ public function __callStatic($name, $arguments) { $camera = new Camera(); $camera->$name($arguments); } } CameraFacade::turnOn(); 管道模式 管道 (Pipeline) 设计模式流水线模式，就是将会数据传递到一个任务序列中，管道扮演者流水线的角色，数据在这里被处理然后传递到下一个步骤。 管道需要三个角色：管道、阀门、载荷。 <?php class Pipeline { private $payload; private $pipes = [];// 阀门 public function __construct($payload) { $this->payload = $payload; } public function Pipe($stage) { $this->pipes[] = $stage; return $this; } public function process() { foreach($this->pipes as $pipe){ call_user_func([$pipe,'handle'],$this->payload); } } } class StageOne { public function handle($payload) { echo $payload."是个"; } } class StageTwo { public function handle($payload) { echo "帅哥"; } } $pipeLine = new Pipeline('Joke'); $pipeLine->Pipe(new StageOne())->Pipe(new StageTwo)->process(); 代理模式 代理模式 (Proxy Pattern)：构建了透明置于两个不同对象之内的一个对象，从而能够截取或代理这两个对象间的通信或访问。 interface Subject{ public function request(); } class RealSubject implements Subject{ public function request(){ echo "真实操作"; } } class ProxySubject implements Subject{ public $real; public function __construct() { $this->real = new RealSubject(); } // 代理操作 public function request() { echo "代理"; $this->real->request(); } } $proxy = new ProxySubject(); $proxy->request(); 请注意代理模式与装饰器、适配器的区别： 装饰器，一般是对对象进行装饰，其中的方法行为会有增加，以修饰对象为主 适配器，一般会改变方法行为，目的是保持接口的统一但得到不同的实现 代理模式有几种形式：远程代理（例如：第三方接口 SDK)、虚代理（例如：异步加 载图片)、保护代理&智能指引（例如：权限保护），而我们代码实现的最普通的代理，其实就是达代理类来代替真实类的操作 设计模式之-行为型模式 行为型模式用于描述程序在运行时复杂的流程控制，即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务，它涉及算法与对象间职责的分配。 策略模式 将一组特定的行为和算法封装起来，以适应某些特定的上下文环境，并让它们可以相互替换，这种模式就是策略模式。 多个类只区别在表现行为不同，可以使用 Strategy 模式，在运行时动态选择具体要执行的行为。比如上学，有多种策略：走路，公交，地铁… // 去学校 abstract class Strategy { abstract function goSchool(); } // 跑着去 class Run extends Strategy { public function goSchool() { // TODO: Implement goSchool() method. } } // 走路去 class Subway extends Strategy { public function goSchool() { // TODO: Implement goSchool() method. } } // 骑自行车去 class Bike extends Strategy { public function goSchool() { // TODO: Implement goSchool() method. } } // 上下文 class Context { protected $_stratege;//存储传过来的策略对象 public function goSchoole() { $this->_stratege->goSchoole(); } } //调用： $contenx = new Context(); $avil_stratery = new Subway(); $contenx->goSchoole($avil_stratery);// 选择走路去 观察者模式 观察者模式属于行为模式，是定义对象间的一种 一对多 的依赖关系，以便当一个对象的状态发生改变时，所有依赖于它的对象都得到通知并自动刷新。 当一个对象状态发生改变后，会影响到其他几个对象的改变，这时候可以用观察者模式。 观察者模式符合接口隔离原则，实现了对象之间的松散耦合。 角色： 被观察对象：部门领导，可以有各个不同的部门领导 观察者：相当于公司前台的角色，当部门领导请假后，前台通知该部门的员工 主题对象：相当于部门员工，有很多部门的员工，每个部门的员工可以增加或减少，当被通知领导请假后，就可以玩手机了 // 主题接口 interface Subject{ public function register(Observer $observer); public function notify(); } // 观察者接口 interface Observer{ public function watch(); } // 主题 class Action implements Subject{ public $_observers=[]; public function register(Observer $observer){ $this->_observers[]=$observer; } public function notify(){ foreach ($this->_observers as $observer) { $observer->watch(); } } } // 观察者 class Cat1 implements Observer{ public function watch(){ echo "Cat1 watches TV "; } } class Dog1 implements Observer{ public function watch(){ echo "Dog1 watches TV "; } } class People implements Observer{ public function watch(){ echo "People watches TV "; } } // 调用实例 $action=new Action(); $action->register(new Cat1()); $action->register(new People()); $action->register(new Dog1()); $action->notify(); 场景：一个事件发生后，要执行一连串更新操作。传统的编程方式，就是在事件的代码之后直接加入处理的逻辑。当更新的逻辑增多之后，代码会变得难以维护。 这种方式是耦合的，侵入式的 ，增加新的逻辑需要修改事件的主体代码。 具体例子，可以看 thinkphp6 的事件机制 命令模式 命令模式，也称为动作或者事务模式 如用餐厅举列，菜单是这个实际的命令，服务员是这个命令的发送者，而厨师是这个命令的接收者。 那么，这个模式解决了什么呢？当你要修改菜单的时候，只需要和服务员说就好了，她会转达给厨师，也就是说，我们实现了顾客和厨师的解耦。也就是调用者与实现者的解耦。 但是命令模式能够做到的是让一个命令接收者实现多个命令（服务员下单、拿酒水、上菜），或者把一条命令转达给多个实现者（热菜厨师、凉菜厨师、主食师傅）。 <?php class Invoker { // 命令发送者（服务员） private $command = []; public function setCommand(Command $command) { $this->command[] = $command; } public function exec() { // 创建 if (count($this->command) > 0) { foreach ($this->command as $command) { $command->execute(); } } } public function undo() { // 撤销 if (count($this->command) > 0) { foreach ($this->command as $command) { $command->undo(); } } } } abstract class Command { // 执行命令内容（菜单） protected $receiver; protected $state; protected $name; public function __construct(Receiver $receiver, $name) { $this->receiver = $receiver; $this->name = $name; } abstract public function execute(); } class ConcreteCommand extends Command { // 具体命令内容 public function execute() { // 具体创建方法 if (!$this->state || $this->state == 2) { $this->receiver->action(); $this->state = 1; } else { echo $this->name . '命令正在执行，无法再次执行了！', PHP_EOL; } } public function undo() { // 具体取消方法 if ($this->state == 1) { $this->receiver->undo(); $this->state = 2; } else { echo $this->name . '命令未执行，无法撤销了！', PHP_EOL; } } } class Receiver { // 命令接收者（厨师） public $name; public function __construct($name) { $this->name = $name; } public function action() { echo $this->name . '命令执行了', PHP_EOL; } public function undo() { echo $this->name . '命令撤销了', PHP_EOL; } } // 命令发送者（服务员） $invoker = new Invoker(); // 命令接收者（厨师） $receiverA = new Receiver('A'); // 具体执行的命令内容（菜单） $commandOne = new ConcreteCommand($receiverA, 'A'); // 执行命令 $invoker->setCommand($commandOne); $invoker->exec(); $invoker->undo(); // 新加一个单独的执行者，只执行一个命令 $invokerA = new Invoker(); $invokerA->setCommand($commandOne); $invokerA->exec(); // 命令A已经执行了，再次执行全部的命令执行者，A命令的state判断无法生效 $invoker->exec(); 迭代器模式 迭代器模式是遍历集合的成熟模式，迭代器模式的关键是将遍历集合的任务交给一个叫做迭代器的对象，它的工作时遍历并选择序列中的对象，而客户端程序员不必知道或关心该集合序列底层的结构。 迭代器模式 (lterator), 又叫做游标 (Cursor) 模式。提供一种方法访问一个容器 (Container) 对象中各个元素，而又不需暴露该对象的内部细节。 thinkphp6 里的模型集合，就是用迭代器进行遍历，实现 Iterrator 接口就好了。 参考 < https://www.bilibili.com/video/BV1vK4y1375m/?p=22&spm_id_from=pageDriver&vd_source=c38eab5c82d0c7cca57364b72f733942> https://www.bilibili.com/video/BV1cg411e7mJ/?spm_id_from=pageDriver&vd_source=c38eab5c82d0c7cca57364b72f733942
场景 我们经常会将excel导入模板文件保存在一个目录，然后提供一个下载按钮，去下载这个模板文件，但是默认情况下thinkphp会在文件响应的时候，会让浏览器缓存一段时间，导致模板文件修改没有立即得到生效 // 下载模板 public function downloadExcelTpl($fileName) { $file = ErpFacade::getContainerAssetsPath() . DIRECTORY_SEPARATOR . $fileName; return (new File($file))->header([ 'Access-Control-Expose-Headers' => 'filename', 'filename' => $fileName, ]); } File类的默认缓存时间如下所示： 解决方法 我们可以手动设置文件缓存的时间为0，修改为如下代码即可 // 下载模板 public function downloadExcelTpl($fileName) { $file = ErpFacade::getContainerAssetsPath() . DIRECTORY_SEPARATOR . $fileName; return (new File($file))->header([ 'Access-Control-Expose-Headers' => 'filename', 'filename' => $fileName, ])->expire(0);// 设置缓存时间为0 }
APP类 一般来说，在index入口文件在加载autoLoad文件后，紧接着就是实例化一个App类，需要注意的是，这个App是可以扩展的，我们可以自定义一个类来继承它 class Erp extends App { protected $bind = [ 'app' => Erp::class,// 通过继承，替换掉系统的默认的类 'cache' => Cache::class, // 等等...，有很多默认设置好的绑定，这里列出几个举例子 ] public function __construct(string $rootPath = '') { parent::__construct($rootPath); if (is_file($this->getBootstrapPath() . 'provider.php')) { $this->bind(include $this->getBootstrapPath() . 'provider.php'); } } } 然后创建一个boostrap目录，用于放一些启动文件，常见的有一下几个，不过可以自己增加其它的 boostrap/app.php，用于实例化自定义类，然后直接在入口文件index.php里引入这个文件$app = require_once __DIR__.'/../bootstrap/app.php'; $app = new \ship\foundation\Erp( realpath(__DIR__.'/../') ); return $app; 1. boostrap/provider.php，用于设置容器的绑定，修改request类为自定义的类，这个就是容器的强大扩展之处，可以不修改框架代码，实现自定义扩展 ```php return [ 'think\exception\Handle' => \ship\foundation\exceptions\HandleException::class, 'think\Request' => \ship\foundation\AppRequest::class, 'think\Validate' => \ship\foundation\validate\Validate::class, 'console' => \ship\parents\console\AppConsole::class, 'export' => \ship\foundation\Export::class, 'event' => \ship\foundation\events\Event::class, ]; boostrap/service.php，用于设置启动服务 return [ \ship\services\ShipService::class, ]; 然后实例化app类的时候，会执行app类里的构造方法 ```php class App extends Container { protected $bind = [ 'app' => App::class,// 别名=>类明的形式，调用时候方便，$this->别名 'cache' => Cache::class, // 等等...，有很多默认设置好的绑定，这里列出几个举例子 ] public function __construct(string $rootPath = '') { $this->thinkPath = dirname(__DIR__) . DIRECTORY_SEPARATOR; $this->rootPath = $rootPath ? rtrim($rootPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : $this->getDefaultRootPath(); $this->appPath = $this->rootPath . 'app' . DIRECTORY_SEPARATOR; $this->runtimePath = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR; if (is_file($this->appPath . 'provider.php')) { // 绑定自定义的类，或者覆盖系统默认的类，实现扩展 $this->bind(include $this->appPath . 'provider.php'); } static::setInstance($this); $this->instance('app', $this); $this->instance('think\Container', $this); } } 由此构造方法可知，app类需要传入一个整个应用的根目录，然后所有其它的目录都是相对于这个根目录的，接着是是加载绑定文件，批量绑定。 这里的bind方法属于容器的方法，用于容器实例的代码定义的，可以绑定到类名、闭包、对象等，通过这个定义我们可以得到容器的实例，就好比docker容器里的容器shell脚本，通过这个脚本pull的时候，可以得到可以允许的容器实例。 接着是设置app容器实例，这个实例，可以最终被调用使用。 至此app的初始化应该完成了。 Container容器管理类 说到容器，我们首先想到的是docker里的容器，docker里有dockerFile文件，通过build这个dockerFile文件，可以得到镜像实例，然后这个镜像实例就可以运行了。这里的容器类也是实现类似的功能的，很多概念是互通的。 组件实例化 主要思路是，首先绑定一些组件别名到类、或者闭包、或者具体的对象实例；然后调用的时候，通过魔术方法make一个实例出来，如果实例已经存在，就直接返回实例。 make实例的时候： 如果发现组件是闭包，就直接调用，不过这个感觉没啥用，还不如直接定义一个函数调用下。 否则如果类名称，就去实例化这个类，不过好像传不了参数进去 class Container implements ContainerInterface, ArrayAccess, IteratorAggregate, Countable { // 类似docker里的docker file的功能 protected $bind = []; // 类型docker里的镜像的功能，组件的实例，组件可以是单例模式 protected $instances = []; // 存下本身的实例，也就是容器本身也可以是单例模式 protected static $instance; // app类在构造函数的时候static::setInstance($this); // 之后就可以这样使用了：Container::getInstance()->make($class, $args, $newInstance) public static function setInstance($instance): void { static::$instance = $instance; } // 单例模式 public static function getInstance() { if (is_null(static::$instance)) { static::$instance = new static; } if (static::$instance instanceof Closure) { // 闭包一般代表可以自定义 return (static::$instance)(); } return static::$instance; } // 返回别名对应的东西，比如类名、对象、或者闭包 public function getAlias(string $abstract): string { if (isset($this->bind[$abstract])) { $bind = $this->bind[$abstract]; if (is_string($bind)) { // 这里表示不同的别名可以对应同一个类，好像这也没啥用 return $this->getAlias($bind); } } return $abstract; } // 绑定别名到类名、闭包、对象 public function bind($abstract, $concrete = null) { if (is_array($abstract)) { foreach ($abstract as $key => $val) { $this->bind($key, $val); } } elseif ($concrete instanceof Closure) { $this->bind[$abstract] = $concrete; } elseif (is_object($concrete)) { $this->instance($abstract, $concrete); } else { $abstract = $this->getAlias($abstract); $this->bind[$abstract] = $concrete; } return $this; } // public function __get($name) { return $this->make($name); } // 类似docker里的build命令，用于将bind里定义的东西构建成实例 public function make(string $abstract, array $vars = [], bool $newInstance = false) { $abstract = $this->getAlias($abstract); if (isset($this->instances[$abstract]) && !$newInstance) { // 单例模式，对于依赖注入的话，就方便的获取到已经实例化的组件 return $this->instances[$abstract]; } if (isset($this->bind[$abstract]) && $this->bind[$abstract] instanceof Closure) { // 闭包直接调用 $object = $this->invokeFunction($this->bind[$abstract], $vars); } else { // 对象或者类名 $object = $this->invokeClass($abstract, $vars); } if (!$newInstance) { $this->instances[$abstract] = $object; } return $object; } } 组件执行 主要是通过反射类，来执行组件： 如果绑定的是函数或者闭包，使用的是new ReflectionFunction($function);； // 调用闭包、或者全局函数，这个好像没啥用 // invokeFunction('fucntionName',[参数1]); // invokeFunction(function(){},[参数1]); public function invokeFunction($function, array $vars = []) { try { $reflect = new ReflectionFunction($function); } catch (ReflectionException $e) { throw new FuncNotFoundException("function not exists: {$function}()", $function, $e); } // 返回的参数顺序函数的参数顺序 $args = $this->bindParams($reflect, $vars); return $function(...$args); } 如果绑定的是类名称，使用的是new ReflectionClass($class);; // 类实例化 public function invokeClass(string $class, array $vars = []) { try { $reflect = new ReflectionClass($class); } catch (ReflectionException $e) { throw new ClassNotFoundException('class not exists: ' . $class, $class, $e); } // 如果有__make方法，就不调用构造函数 if ($reflect->hasMethod('__make')) { $method = $reflect->getMethod('__make'); if ($method->isPublic() && $method->isStatic()) { $args = $this->bindParams($method, $vars); return $method->invokeArgs(null, $args); } } $constructor = $reflect->getConstructor(); $args = $constructor ? $this->bindParams($constructor, $vars) : []; $object = $reflect->newInstanceArgs($args); $this->invokeAfter($class, $object); return $object; } 参数绑定，这个很灵活，可以通过索引数组的方式设置参数，也可以通过关联数组的方式设置，这样的话就和顺序没有关系了，不过要注意参数是不是对象强类型，还可以依赖注入 protected function bindParams(ReflectionFunctionAbstract $reflect, array $vars = []): array { if ($reflect->getNumberOfParameters() == 0) { return []; } // 判断数组类型 数字数组时按顺序绑定参数 reset($vars); $type = key($vars) === 0 ? 1 : 0;// 判断参数数组是否是关联数组0是，1不是 $params = $reflect->getParameters(); $args = []; // 按方法参数顺序绑定 foreach ($params as $param) { $name = $param->getName(); $lowerName = Str::snake($name);// 驼峰参数会自动转下划线 $class = $param->getClass(); if ($class) { // 参数是类，如果此时$vars里的第一个是该类的实例则用这个，否则就实例化类 $args[] = $this->getObjectParam($class->getName(), $vars); } elseif (1 == $type && !empty($vars)) { // 参数是索引数组，按顺序绑定 $args[] = array_shift($vars); } elseif (0 == $type && isset($vars[$name])) { // 参数是关联数组，如果参数名称存在则绑定 // 如果变量名称一样的话，会绑定到同一个参数 $args[] = $vars[$name]; } elseif (0 == $type && isset($vars[$lowerName])) { // 参数是关联数组，如果参数下划线的形式存在则绑定 // 因为参数经常是驼峰，数组key经常是下划线 $args[] = $vars[$lowerName]; } elseif ($param->isDefaultValueAvailable()) {// 都没用的话用默认参数 // 没有的话就取默认值 $args[] = $param->getDefaultValue(); } else { throw new InvalidArgumentException('method param miss:' . $name); } } return $args; } // 由这个可知，如果参数（类名称 $object）形式的话，顺序不能随便改变 // 因为判断第一个变量不是该类的对象的话就不会使用了 // 如果想要自动依赖注入的话，这个参数可以不传，也就是$vars的数量可以比参数的数量少 // 所以，如果不希望依赖注入，参数名称前面最好不要加入类名称，不然变量是关联数组参数的是，容易出现问题 protected function getObjectParam(string $className, array &$vars) { $array = $vars; $value = array_shift($array); if ($value instanceof $className) { $result = $value; array_shift($vars); } else { // 依赖注入 $result = $this->make($className); } return $result; } 最后系统还提供了一个快捷的invoke方法，通过这个方法，可以参数绑定，调用的方法不存在时候，不会出现语法错误 public function invoke($callable, array $vars = [], bool $accessible = false) { if ($callable instanceof Closure) { return $this->invokeFunction($callable, $vars); } elseif (is_string($callable) && false === strpos($callable, '::')) { return $this->invokeFunction($callable, $vars); } else { return $this->invokeMethod($callable, $vars, $accessible); } } // 这个方法好像跟组件没啥关系，组件实例化的时候用不到，只用invoke的时候才用到 // 不过这个方法，通过参数accessible，好像可以调用私有的方法，这点还不错 public function invokeMethod($method, array $vars = [], bool $accessible = false) { if (is_array($method)) { [$class, $method] = $method; $class = is_object($class) ? $class : $this->invokeClass($class); } else { // 静态方法 [$class, $method] = explode('::', $method); } try { $reflect = new ReflectionMethod($class, $method); } catch (ReflectionException $e) { $class = is_object($class) ? get_class($class) : $class; throw new FuncNotFoundException('method not exists: ' . $class . '::' . $method . '()', "{$class}::{$method}", $e); } $args = $this->bindParams($reflect, $vars); if ($accessible) { $reflect->setAccessible($accessible); } return $reflect->invokeArgs(is_object($class) ? $class : null, $args); } 该方法经常用于参数绑定 // 调用对象的handle方法 $this->app->invoke([$obj, 'handle'], [$params]); // 静态方法调用 $this->app->invoke("UserLogin::handle", [$params]); // 闭包调用 $this->app->invoke(function($params){}, [$params]); // 函数名称调用 $this->app->invoke('handle', [$params]); 服务 这个在lavarel里也叫做服务提供者，主要是提供一系列的服务，例如短信服务，支付服务等，提供一些和系统业务无关的一些基础服务。 protected $initializers = [ Error::class,//异常类 RegisterService::class,// 注册服务 BootService::class,// 启动服务 ]; // APP类->initialize方法里初始化系统的时候，会注册服务 foreach ($this->initializers as $initializer) { $this->make($initializer)->init($this); } public function register($service, bool $force = false) { $registered = $this->getService($service); if ($registered && !$force) { return $registered; } // 注册服务，并且调用register方法，该方法主要是设置一些容器绑定 if (is_string($service)) { $service = new $service($this); } if (method_exists($service, 'register')) { $service->register(); } if (property_exists($service, 'bind')) { $this->bind($service->bind); } $this->services[] = $service; } 然后在初始化，BootService::class启动服务 public function bootService($service) { if (method_exists($service, 'boot')) { return $this->invoke([$service, 'boot']); } } boot方法里，主要是对服务组件的一些配置，为了提高性能，一般不实例化组件，等到用的时候再实例化，所以配置一般是配置到静态变量上 // 分页服务 class ValidateService extends Service { public function register() { if (!$this->app->bound(Validate::class)) { $this->app->bind(Validate::class, Validate::class); } } public function boot() { Validate::maker(function (Validate $validate) { $validate->setLang($this->app->lang); $validate->setDb($this->app->db); $validate->setRequest($this->app->request); }); } } class Validate { protected static $maker; public function __construct(){ if (!empty(static::$maker)) { foreach (static::$maker as $maker) { call_user_func($maker, $this); } } } public static function maker(Closure $resolver) { static::$maker = $resolver; } }
首先，在文件组织上，框架的所有文件都是放在think开头的命名空间下 然后，在入口文件上，有两个入口： 一个是console入口，在项目的根目录下，用于执行脚本和定时任务 另一个是http入口，在项目的public目录下，用于提供http服务 由于上图可知，这两个文件的工作流思路也比较简单，只有三个步骤 第一、引入autoload文件，用于管理第三方扩展包，已经提供自动加载的功能 第二、实例化一个全局的App应用，用于管理应用，加载配置文件、提供容器的支持等 第三、因为有app类的支持，可以使用对应的组件来执行相关的功能，例如http组件用于提供http服务，console组件用于命令行的支持
后台列表展示单子明细的专业解决方案
有时候为了方便阅读，会在列表也要展示明细，例如： 为了实现这个效果，其实有两种方式： 第一种是：数据是以单据的为主体分页的形式返回给前端，如果说操作按钮的主体是单据，那么用这种方式是最好不过的。 不过有个问题就是，如果单据的明细特别多，就会导致列表太长了，不好阅读，不过可以让前端实现合并展开的功能。同时如果明细数量多的话，一次性返回太多的结果，会影响列表的加载速度。 第二种是：数据以单据明细分页的方式返回给前端，单据明细以订单id进行排序，这样同一个订单的明细就会在一起，前端再把单据编号合并展示。如果列表的操作对象是单据明细的话，其实这个方式是不错的选择，这样就能确保每页的行数的固定的，阅读体验会比较好。 缺点： 可能同一个单子的明细，一个在第一页，另外一个在第二页。 同时如果操作按钮如果是针对单据的，那么就需要对操作按钮栏目也要做合并，如果明细一个在第一页，另外一个在第二页，那么同一个单子就会有两个操作按钮，逻辑上会比较不好点。 搜索明细的sn的时候，会得到一条明细结果，但是操作按钮是针对整个单子的话，点进去又是所有的明细，感觉上也有点怪怪的 综上所述，具体选择哪个需要自己根据实际情况权衡