设计模式之美

引言 学习设计模式要回答两个问题: 什么是好代码? —— 见 §02。 怎样写出好代码? —— 面向对象、设计原则、设计模式、编程规范、代码重构这五件套各管一段,见 §03。 KISS 原则人人会背,难的是判断"多简单算简单"。后面所有内容都在给这类判断提供尺子。 02 代码质量:把 30 个形容词压成 2 个问题 书里列了 30 多个评价代码的英文词。它们都可以归到三层: 层面 关心的问题 代表属性 编码层 当下能不能读懂 readability、simplicity、clean 模块层 改动能不能控制住 maintainability、testability、reusability 系统层 长期能不能演进 extensibility、flexibility、scalability 剥到最里面,只剩两个问题: 改一行,要扫几个文件? —— 决定可维护性、可测试性。 加一个新需求,是插一段,还是重写? —— 决定可扩展性、灵活性。 其余术语的定位: flexibility 是结果不是属性:易扩展 / 易复用 / 易用三者满足任一即可称"灵活"。 elegant / good / clean 偏主观,不构成评价维度,适合表态不适合度量。 robustness / reliability / scalability 描述运行时行为,属于架构而非代码质量。 DRY 是手段不是目标:复用是结果,过早抽象反而降低可维护性。 边界:可读性和可维护性常被混用。可读性是"读"的成本,可维护性是"改"的成本。一段代码可以读得懂但改不动(逻辑分散在 10 个文件),反之少见。 03 五件套:面向对象 / 设计原则 / 设计模式 / 编程规范 / 代码重构 工具 解决什么 抽象层次 关键内容 面向对象 复杂程序的组织方式 最高 封装、抽象、继承、多态(§04~§10) 设计原则 写代码时的判断依据 高 SOLID、DRY、KISS、YAGNI、LOD 设计模式 反复出现的设计问题 中 23 种,分创建型 / 结构型 / 行为型 编程规范 可读性 低 命名、注释、函数长度、模块划分 代码重构 代码质量不下降的兜底 低 单元测试为前提,分大重构 / 小重构两种规模 依赖关系: ...

February 5, 2021 · 5 min · 984 words · Me, LLM

PHP 月份加减问题

看现象 var_dump(date("Y-m-d", strtotime("+1 month", strtotime("2020-07-31")))); // string(10) "2020-08-31" 符合预期 var_dump(date("Y-m-d", strtotime("+1 month", strtotime("2020-05-31")))); // string(10) "2020-07-01" 不符合预期,预期 2020-06-30 var_dump(date("Y-m-d", strtotime("-1 month", strtotime("2020-02-29")))); // string(10) "2020-01-29" 符合预期 var_dump(date("Y-m-d", strtotime("-1 month", strtotime("2020-03-31")))); // string(10) "2020-03-02" 不符合预期,预期 2020-02-29 // Carbon\Carbon Carbon::parse("2020-07-31")->addMonth()->toDateString(); // "2020-08-31" Carbon::parse("2020-05-31")->addMonth()->toDateString(); // "2020-07-01" Carbon::parse("2020-02-29")->subMonth()->toDateString(); // "2020-01-29" Carbon::parse("2020-03-31")->subMonth()->toDateString(); // "2020-03-02" // 结果与 strtotime 一致。 原因 var_dump(date("Y-m-d", strtotime("+1 month", strtotime("2020-05-31")))); // string(10) "2020-07-01" date 内部的处理逻辑: 2020-05-31 做 +1 month 也就是 2020-06-31。 再做日期规范化,因为没有 06-31,所以 06-31 就等于了 07-01。 var_dump(date("Y-m-d", strtotime("2020-06-31"))); // string(10) "2017-07-01" var_dump(date("Y-m-d", strtotime("next month", strtotime("2017-01-31")))); // string(10) "2017-03-03" var_dump(date("Y-m-d", strtotime("last month", strtotime("2017-03-31")))); // string(10) "2017-03-03" 解决方案 var_dump(date("Y-m-d", strtotime("last day of -1 month", strtotime("2017-03-31")))); // string(10) "2017-02-28" var_dump(date("Y-m-d", strtotime("first day of +1 month", strtotime("2017-08-31")))); // string(10) "2017-09-01" // 但要注意短语的含义: var_dump(date("Y-m-d", strtotime("last day of -1 month", strtotime("2017-03-01")))); // string(10) "2017-02-28" 如果使用 Carbon\Carbon 可以用 subMonthNoOverflow 与 addMonthNoOverflow 防止进位: ...

January 27, 2021 · 2 min · 271 words · Me

Composer 文档笔记

命令行 composer dump 能跑,因为 symfony/console 允许 unambiguous prefix —— 任何 composer 子命令都可缩到能唯一匹配的最短前缀: composer dump # = dump-autoload composer i # = install composer show --platform # 列本机 platform package(php / ext-* / lib-*) --prefer-install=source|dist|auto 是 install / update 的同名 flag;--prefer-source / --prefer-dist 是它的快捷写法,三者底层同一开关。 发包瘦身 dist tarball 默认把 repo 里所有文件打进去(demo / test / CI 配置)。.gitattributes 加 export-ignore 把它们排除出 dist: /demo export-ignore phpunit.xml.dist export-ignore /.github/ export-ignore 本地 git archive 预览实际 dist 包内容: git archive HEAD --format zip -o preview.zip 几个反直觉默认 Config 默认 坑 process-timeout 300s 装大包(satis 全量索引、monorepo)容易 5 分钟撞 timeout platform-check php-only 只查 PHP 版本,不查扩展。缺扩展要等运行时 Call to undefined function 才暴露 discard-changes false source 装的包本地有改动时 update 会卡 prompt;CI 设 true 或 stash preferred-install auto 可以 per-package 配,自家 fork 走 source 方便 patch,其余走 dist { "config": { "process-timeout": 900, "preferred-install": { "myorg/*": "source", "*": "dist" } } } 生产环境 autoload 优化 composer install --no-dev 是基操,再加: ...

October 28, 2020 · 2 min · 279 words · Me, LLM

PHP float 精度

实例 1 $a = 1.1; var_dump(gettype($a)); // string(6) "double" var_dump($a); // float(1.1) 实例 2 $a = "123456789.1100110011"; $a = (float) $a; var_dump($a); // float(123456789.11001) var_dump(sprintf('%.11f', $a)); // string(21) "123456789.11001099646" $b = 123456789.11001; var_dump($b); // float(123456789.11001) var_dump(sprintf('%.11f', $b)); // string(21) "123456789.11000999808" $c = '123456789.1100110011'; $c = (float) $c; var_dump($c); // float(123456789.11001) $c = (string) $c; var_dump($c); // string(15) "123456789.11001" $c = (float) $c; var_dump($c); // float(123456789.11001) var_dump(sprintf('%.11f', $c)); // string(21) "123456789.11000999808" var_dump($a === $b); // bool(false) - 说明 $a 还是携带着 float 的精度 var_dump($b === $c); // bool(true) 实例 3 // # 1 var_dump(120085 === 1200.85 * 100); // bool(false) // # 2 var_dump(120085 == 1200.85 * 100); // bool(false) // # 3 var_dump(120081 == 1200.81 * 100); // bool(true) // # 4 var_dump(120085 - 1200.85 * 100); // float(1.4551915228367E-11) 实例 4 $a = 0.1; $b = 0.9; $c = 1; var_dump(($a + $b) == $c); // bool(true) var_dump(($c - $b) == $a); // bool(false) var_dump(sprintf('%.20f', $a + $b)); // string(22) "1.00000000000000000000" var_dump(sprintf('%.20f', $c - $b)); // string(22) "0.09999999999999997780" var_dump((0.5 - 0.25) === 0.25); // bool(true) 0.5 二进制 0.1,0.25 二进制 0.01 var_dump((0.25 + 0.25) === 0.5); // bool(true) 实例 5 $n = 19.99; var_dump($n * 100); // float(1999) var_dump((int) ($n * 100)); // int(1998) !!! var_dump((string) ($n * 100)); // string(4) "1999" var_dump((int) (string) ($n * 100)); // int(1999) var_dump(round($n * 100)); // float(1999) var_dump((int) round($n * 100)); // int(1999) 19.99 * 100 实际结果是 1998.9999999999998…,(int) 走的是向零截断而非四舍五入,所以掉到 1998。打印时 PHP 默认精度只显示到 1999,肉眼上看不出来这个陷阱。 ...

May 9, 2020 · 3 min · 449 words · Me

Lonicera Framework

项目代码:imzyf/lonicera | GitHub Lonicera Framework - Every French soldier carries a marshal’s baton in his knapsack. MVC MVC 模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。 Lonicera 0.1 bootstrap index.php 单一入口模式。 启动 PHP 内置 Web 服务器: php -S localhost:7070 路由器层 更偏向于使用 PATH_INFO 方式来访问。 从传统 URL 参数模式的访问地址进行解析,提取里面的 group、controller、action、param 4 个参数,随后交给 bootstrap 进行 dispatch 处理。 数据模型 用 PDO 来实现连接数据库。 ORM Object Relational Mapping 对象与数据库的映射叫作对象关系映射 PO Persistent Object 把一个数据库中的表的一行记录对应的对象称为持久对象 BO Business Object 业务对象 把业务逻辑封装为一个对象 VO Value Object 值对象 界面显示的数据对象 DTO Data Transfer Object 用在热呵呵需要数据传输的地方 DAO Data Access Object 指代 Active Record 模式中的数据对象 传统的 ORM 模式提倡数据对象和负责持久化的代码的分开,但是这并没有坚持数据操作的工作量。还有一种 ORM 模式叫作 Active Record。在 Active Record 中,模型层集成了 ORM 的功能,他们及代表实体,包含因为业务逻辑,又是数据对象,并负责把自己存储到数据库中。 ...

December 19, 2019 · 2 min · 276 words · Me

最左前缀原理与相关优化

MySQL 中的索引可以按一定顺序引用多个列,这种索引叫做联合索引。形式上,一个联合索引是一个有序元组 <a1, a2, …, an>,其中每个元素是表中的一列;单列索引可视为元素数为 1 的特例。 下文以 MySQL 8.0 + Employees Sample Database 为实验环境。MySQL 5.7 的历史情况会在相关章节穿插说明 —— 5.7 已结束官方支持,但很多旧项目仍在用,行为差异值得标出来。 以 employees.titles 为例,查看其索引。注意 8.0 比 5.7 多了 Visible 与 Expression 两列: SHOW INDEX FROM employees.titles; +--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression | +--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ | titles | 0 | PRIMARY | 1 | emp_no | A | 301292 | NULL | NULL | | BTREE | | | YES | NULL | | titles | 0 | PRIMARY | 2 | title | A | 442605 | NULL | NULL | | BTREE | | | YES | NULL | | titles | 0 | PRIMARY | 3 | from_date | A | 442605 | NULL | NULL | | BTREE | | | YES | NULL | +--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ Visible:8.0 引入的 Invisible Index。ALTER TABLE ... ALTER INDEX idx INVISIBLE 让索引对优化器不可见但物理结构仍在,适合「灰度删除索引」的场景 —— 先标为不可见观察一段时间,没出问题再真删。 Expression:8.0.13+ 的 Functional Key Parts(函数索引)会在这一列展示表达式,后文会用到。 全列匹配 EXPLAIN SELECT * FROM employees.titles WHERE emp_no = '10009' AND title = 'Senior Engineer' AND from_date = '1995-02-18'; +----+-------------+--------+------------+-------+---------------+---------+---------+-------------------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+--------+------------+-------+---------------+---------+---------+-------------------+------+----------+-------+ | 1 | SIMPLE | titles | NULL | const | PRIMARY | PRIMARY | 159 | const,const,const | 1 | 100.00 | NULL | +----+-------------+--------+------------+-------+---------------+---------+---------+-------------------+------+----------+-------+ 对索引所有列进行精确匹配(这里精确匹配指 = 或 IN)时,索引被完整用到。 ...

May 25, 2019 · 6 min · 1155 words · Me, LLM

归并排序

归并排序(英语:Merge sort,或 mergesort),是创建在归并操作上的一种有效的排序算法,效率为 O(nlogn)。1945 年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。 采用分治法: 分割:递归地把当前序列平均分割成两半。 集成:在保持元素顺序的同时将上一步得到的子序列集成到一起(归并)。 归并操作(merge),也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作。归并排序算法依赖归并操作。 <?php function mergeSort($arr) { $len = count($arr); if ($len <= 1) { return $arr; } // 递归结束条件,到达这步的时候,数组就只剩下一个元素了,也就是分离了数组 $mid = $len / 2; $left = array_slice($arr, 0, $mid); // 拆分数组 0-mid 这部分给左边 left $right = array_slice($arr, $mid); // 拆分数组 mid-末尾这部分给右边 right $left = mergeSort($left); // 左边拆分完后开始递归合并往上走 $right = mergeSort($right); // 右边拆分完毕开始递归往上走 $arr = merge($left, $right); // 合并两个数组,继续递归 return $arr; } // merge 函数将指定的两个有序数组 (arrA, arr) 合并并且排序 function merge($arrA, $arrB) { $arrC = array(); while (count($arrA) && count($arrB)) { // 这里不断的判断哪个值小,就将小的值给到 arrC, 但是到最后肯定要剩下几个值, // 不是剩下 arrA 里面的就是剩下 arrB 里面的而且这几个有序的值,肯定比 arrC 里面所有的值都大所以使用 $arrC[] = $arrA[0] < $arrB[0] ? array_shift($arrA) : array_shift($arrB); } return array_merge($arrC, $arrA, $arrB); } $startTime = microtime(1); $arr = range(1, 1000); shuffle($arr); echo 'before sort: ', implode(', ', $arr), "\n"; $sortArr = mergeSort($arr); echo 'after sort: ', implode(', ', $sortArr), "\n"; echo 'use time: ', microtime(1) - $startTime, "s\n"; 假设被排序的数列中有 N 个数。遍历一趟的时间复杂度是 O(N),需要遍历多少次呢? ...

May 23, 2019 · 1 min · 166 words · Me

Laravel 中 composer 加载流程

启动 Laravel 5.8 文章以 Laravel 学习。入口文件 public/index.php: // Register The Auto Loader require __DIR__.'/../vendor/autoload.php'; autoload.php 不负责具体功能逻辑,只做了两件事:初始化自动加载类、注册自动加载类。 autoload_real.php 中的类名为 ComposerAutoloaderInit... 这可能是为防止与用户自定义类名跟这个类重复冲突,加上了哈希值。 其实还有一个做法我们更加熟悉,是定义一个命名空间。这里为什么不定义一个命名空间呢?一种理解:命名空间一般都是为了复用,而这个类只需要运行一次即可,以后也不会用得到,用哈希值更加合适。 autoload_real.php autoload.php 主要调用了 getLoader(): public static function getLoader() { // 单例模式,自动加载类只能有一个 1 if (null !== self::$loader) { return self::$loader; } // 获得自动加载核心类对象 2 spl_autoload_register(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader')); // 初始化自动加载核心类对象 3 $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { require_once __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::getInitializer($loader)); } else { $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } } // 注册自动加载核心类对象 4 $loader->register(true); // 自动加载全局函数 5 if ($useStaticLoader) { $includeFiles = Composer\Autoload\ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$files; } else { $includeFiles = require __DIR__ . '/autoload_files.php'; } foreach ($includeFiles as $fileIdentifier => $file) { composerRequire76e88f0b305cd64c7c84b90b278c31db($fileIdentifier, $file); } return $loader; } 单例模式 1 if (null !== self::$loader) { return self::$loader; } 构造 ClassLoader 核心类 2 spl_autoload_register(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader')); public static function loadClassLoader($class) { if ('Composer\Autoload\ClassLoader' === $class) { require __DIR__ . '/ClassLoader.php'; } } composer 先向 PHP 自动加载机制注册了一个函数,这个函数 require 了 ClassLoader 文件。成功 new 出该文件中核心类 ClassLoader() 后,又销毁了该函数。 ...

April 28, 2019 · 7 min · 1434 words · Me

寻找数组中轴索引

将 pivot 索引定义为:左边的数字之和等于索引右边的数字之和。 Input: nums = [1, 7, 3, 6, 5, 6] Output: 3 Explanation: 1 + 7 + 3 = 5 + 6 Input: nums = [1, 2, 3] Output: -1 Explanation: There is no index that satisfies the conditions in the problem statement. Note: The length of nums will be in the range [0, 10000]. Each element nums[i] will be an integer in the range [-1000, 1000]. 关键点 动态规划 数组的和 - 中轴数 = 中轴数左边数组的和 * 2 解答 func findPivot(_ array: [Int]) -> Int { // 数组和 let sum = array.reduce(0, +) // 左侧数组和 var leftSum = 0 for (key, value) in array.enumerated() { if sum - value == leftSum * 2 { return key } leftSum += value } return -1 } let array = [1, 7, 3, 6, 5, 6] search(array) // 3 References 找到数组中左右两边的和相等的 pivot 的下标 Find Pivot Index – EOF –

March 6, 2019 · 1 min · 146 words · Me

m 进制转 n 进制

思路 m 进制 -> 十进制 -> n 进制 利用柯里化生成函数(炫技 🐶) m 进制 -> 十进制 // carry 范围值: 2-36 // origin 范围值: 0-9 [ascii 48-58], A-Z [65-90], a-z [97-122] func carryToDecimalism(_ carry: Int) -> (_ origin: String) -> Int { return { origin in // 得到字符串对应的 ascii 码 let asciis = origin.uppercased().unicodeScalars.map { Int($0.value) } // 累加每一位 let result = asciis.reversed().enumerated().map { (index, ascii) -> Int in var standard: Int if 65 <= ascii && ascii <= 90 { standard = ascii - 65 + 10 } else { standard = ascii - 48 } return standard * Int(pow(Double(carry), Double(index))) }.reduce(0, +) return result } } let 十六进制转十进制 = carryToDecimalism(16) print(十六进制转十进制("1a")) // 26 let 二进制转十进制 = carryToDecimalism(2) print(二进制转十进制("110")) // 6 十进制 -> n 进制 func decimalismToCarry(_ carry: Int) -> (_ origin: Int) -> String { return { origin in var result = [Int]() var remain = origin while remain > 0 { result.append(remain % carry) remain /= carry } if carry <= 10 { return result.reversed().map(String.init).joined() } else { return result.reversed().map { i -> String in return i < 10 ? String(i) : String(UnicodeScalar(i + 55)!) }.joined() } } } let 十进制转二进制 = decimalismToCarry(2) print(十进制转二进制(26)) // "11010" References ASCII 码对照表 – EOF –

March 2, 2019 · 1 min · 202 words · Me