曾经,我以为:“好的代码像首诗,烂的代码像坨屎。”
但对于代码质量的好坏、阅读性的好坏,却没有一个有效的度量标准。每个人心中的一杆秤,是不同的。
那么多年过去了,依然无法去统一所有人的编程思想。而什么样的代码,才是好的代码,渐渐地,在心中有了一部分标准的答案。
在做一个项目时,通过以下几个方面因素来考虑代码架构的设计:
不管是在一个项目中,还是在一个人的开发生涯中,慢慢地积累沉淀下来,应该从代码中体现出编写者的思想来,融入代码灵魂。而不是东一榔头,西一棒槌,没有任何的特色;也不是某一方面深钻到了极致,像偏科严重,忽略了其他的方面。
对于个人来说,最终应当变成“一精多专”,在某一个核心领域范围称为领袖,在其他各个方面均有涉猎。对于项目来说,最终应当变成,输出核心技术价值,产品各方面挑不出太大毛病。
没有银弹。
所有的最佳实践可能都是阶段性的,或者针对某个领域范围内的。不用刻意魔化某几类技术、语言、工具、方案之类。
写代码从新手到入门,却必须需要养成良好的习惯。比如如下几点,是无论在任何语言、项目中,都可以推行的:
另外需要明确说明一点:在没有覆盖测试用例之前,所有的安全、质量都是空谈。
与时俱进,保持学习者的心态即可。建议是关注新技术、新框架及语言的新特性,并尝试去用一用。当这些新的技术、方案成熟或者能够稳定时,运用到已有项目中进行优化。要明白一点,再好的东西,总有被淘汰的一天(就比如我学习的第三个语言 Pascal,包括它母公司及产品 Delphi)。
以下要具体展开的几个方面互为关联,有可能会有重叠。必读代码的可维护性高,那么代码开发效率不会低到哪里,代码的阅读性也是。
常见的问题:
先从一段简单的代码开始:
function func1(arg1 = 'sth') {
// ...
return 'sth';
}
function func2(args) {
const { arg1 = 'sth' } = args || {};
// ...
return 'sth';
}
好像都没什么问题。那么,复杂一点:
function func1(arg1 = 'sth1', arg2 = false, arg3 = 3) {
// ...
return 'sth';
}
function func2(args) {
const { arg1 = 'sth', arg2 = false, arg3= 3 } = args || {};
// ...
return 'sth';
}
这时候,就能够发现一些端倪,比如其他都用默认值,arg3
用 10
:
func1('sth', false, 10);
func2({ arg3 = 10 });
同时,在不借助第三方 IDE 插件情况下,func2
的代码阅读性也比 func1
高很多。这样,也更加方便进行 Code Review。
接下来再说一个例子:
/**
* 时间
*/
export type Time = {
[value]: moment.Moment
}
/**
* 增加一天
*/
export function addDay(a: Time): Time {
return {
[value]: a[value].clone().add(1, "d"),
}
}
/**
* 减少一天
*/
export function subDay(a: Time): Time {
return {
[value]: a[value].clone().subtract(1, "d"),
}
}
依然是对上面的方法进行衍生:
/**
* 格式化
*/
export function format(a: Time, 形式: string): string {
return a[value].format(形式)
}
此时,已经有三个方法名了 format
、addDay
和 subDay
,如果 IDE 有引入提示的话,已经需要记住 3 个了。同时,这里是时间的格式化,方法名叫 format
。后面可能还会有 SQL 语句的格式化、GraphQL 语句的格式化等等叫 format
的重名方法。在选择和使用的时候,就会麻烦。
如果以 DateTime 封装为例:
/**
* 时间
*/
export class Time {
#value: moment.Moment
// 看情况,这里其实允许给 any 的话,对于使用该 Class 开发的人来说是一件非常省心省力的事情
constructor(a?: moment.MomentInput) {
// 只需要在构建器里做好判断和异常的抛出即可
const _data = moment(a)
if (isNaN(_data.unix())) throw new Error(`输入时间无法解析: ${a}`)
this.#value = _data
}
// 可以注入验证器,验证输入参数是否为合法类型
// @validatePattern()
format(pattern = "YYYY-MM-DD") {
return this.#value.format(pattern)
}
toString() {
return this.#value.toString()
}
get value() {
return this.#value.unix()
}
}
const demo = new Time(new Date())
console.log(demo.format())
// 2023-08-31
console.log(demo.toString())
// Thu Aug 31 2023 15:52:30 GMT+0800
console.log(demo.value)
// 1693468350
// console.log(demo.#value)
// 报错
在 TypeScript 中,使用类和接口可以实现面向对象编程的封装、继承和多态特性,提高代码的可维护性和可扩展性。
// 使用类和接口实现面向对象编程
// 如果有一些通用方法,也可以用 Class + extends 方式
interface PostInterface {
save(): void;
}
class MySQLProvider implements PostInterface {
save() {
// 将日志存入 MySQL 数据库
console.log("hello");
}
}
class PostService {
constructor(
@Inject(MySQLProvider) provider
) { }
save() {
//
}
}
这样,只需要将各个存储 Store 使用相同的 interface 进行实现,即可替换存储源从 MySQL 到 MongoDB、File System 等其他地方。
装饰器是一种使用简单语法来为类、方法或属性添加额外功能的方式。它们是一种增强类的行为而不修改其实现的方式。
例如,可以使用装饰器为方法添加日志记录:
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
let originalMethod = descriptor.value;
descriptor.value = function(…args: any[]) {
console.log(Calling ${propertyKey} with args: ${JSON.stringify(args)});
let result = originalMethod.apply(this, args);
console.log(Called ${propertyKey}, result: ${result});
return result;
}
}
class Calculator {
@logMethod
add(x: number, y: number): number {
return x + y;
}
}
还可以使用装饰器为类、方法或属性添加元数据,这些元数据可以在运行时使用。
function setApiPath(path: string) {
return function (target: any) {
target.prototype.apiPath = path;
}
}
@setApiPath("/users")
class UserService {
// …
}
console.log(new UserService().apiPath); // "/users"
一只水桶能装多少水取决于它最短的那块木板。
可以提高代码质量的方式有很多。以下几个方面常来综合评定代码质量:
arr.forEach
会比 arr.map
性能更高。但 for
语句一定是最高的在 js/ts 中,常用如下几种方式提高代码编写的质量。
可以参考我以前的几篇文章:
在 TypeScript 中,严格模式可以提供更严格的类型检查和错误检测,帮助开发者在开发过程中发现潜在的错误和类型问题。
// 在 tsconfig.json 中开启严格模式
{
"compilerOptions": {
"strict": true
}
}
在开启严格模式时,需要注意一些语言特性的变化和规范,比如不能隐式地将
null
或undefined
赋值给非空类型,不能在类定义之外使用private
和protected
等等。
在 TypeScript 中,使用枚举类型可以方便地定义常量和枚举值,提高代码的可读性和可维护性。
// 使用枚举类型定义常量
enum MyEnum {
Foo = "foo",
Bar = "bar",
Baz = "baz",
}
function doSomething(value: MyEnum) {
console.log(value);
}
doSomething(MyEnum.Foo);
在使用枚举类型时,需要注意枚举值的正确性和可读性,避免出现歧义或冲突。
有时,我们可能没有有关变量类型的所有信息,但仍然需要在代码中使用它。在这种情况下,我们可以利用 any
类型。但是,像任何强大的工具一样,使用 any
应该谨慎和有目的地使用。而 unknown
类型是 TypeScript 3.0 中引入的一种强大且限制性更强的类型。它比 any
类型更具限制性,并可以帮助你防止意外的类型错误。
与 any
不同的是,当你使用 unknown
类型时,除非你首先检查其类型,否则 TypeScript 不允许你对值执行任何操作。这可以帮助你在编译时捕捉到类型错误,而不是在运行时。
例如,你可以使用 unknown
类型创建一个更加类型安全的函数:
function printValue(value: unknown) {
if (typeof value === "string") {
console.log(value);
} else {
console.log("Not a string");
}
}
当在 TypeScript 中处理数据时,你可能希望确保某些值无法更改。这就是“只读”和“只读数组”的用武之地。
“只读”关键字用于使对象的属性只读,意味着在创建后它们无法被修改。例如,在处理配置或常量值时,这非常有用。
interface Point {
x: number;
y: number;
}
let point: Readonly<Point> = {x: 0, y: 0};
point.x = 1; // TypeScript会报错,因为“point.x”是只读的
“只读数组”与“只读”类似,但是用于数组。它使一个数组变成只读状态,在创建后不能被修改。
let numbers: ReadonlyArray<number> = [1, 2, 3];
numbers.push(4); // TypeScript会报错,因为“numbers”是只读的
包括两个部分,代码运行效率,和代码开发效率。这两者没什么实际关联,但却又避不开问题同时出现。
参考:https://github.com/alsotang/fast-js
new Array(arr.length)
性能最高常见的一些特性都会有现成的性能测试用例。如果不确定的情况下,可以写两个方法对比一下,自己跑一个 Benchmark 脚本。
参考: https://github.com/js-benchmark/mysql
timestamp
(uint(10)
) 比 DateTime
类型读写性能均会高char
会比 varchar
类型读写性能均会高text
与 blob
读写速度接近, blob
略高不多可忽略,但 blob
存储更推荐这部分没有什么实际的参考经验,因为往往需要比较,不确定的两种方案,不一定会有现成的性能测试结果。运行效率需要通过跑 Benchmark 来确定。
无为而治,是开发的最高境界。
无为不是无所作为,不是无所事事,而是不做无效的工作。
道家的第一原则是“道法自然”。顺应自然,不要过于刻意,“去甚,去奢,去泰”。人要以自然的态度对待自然,对待他人,对待自我。所以会有“自然——释然——当然——怡然”。
温馨建议 1:避免 GUI 依赖,图形界面的戳戳点点是非常低效的。比如画流程图,你的手速可能跟不上你的思维,那么这时候需要两点:
以此为例,在选择脚手架工具的时候,以下几点建议:
温馨建议 2:避免工具依赖,比如:
依然以上面 class Time
为例,我没有额外去定义类型声明,但会自动生成如下:
// out.d.ts
/**
* 时间
*/
declare class Time {
#private;
constructor(a?: moment.MomentInput);
format(pattern?: string): string;
toString(): string;
get value(): number;
}
export { Time };
同理,能够自动完成的工作不要人为参与。类型的定义声明、Open API 的文档 Schema、GraphQL 的 Schema 等,都是。同时,也可以借助工具进行自动化流程。比如自动化测试、CI/CD 等,进一步充分提高人效。
勤能补拙。最好的工具是机械记忆。写代码这东西,想得心应手,还是得多写。哪怕就是一句很简单的
console.log
,有的人打这么多个字符,也比用一个用插件C+L+空格
敲三个键更飘逸更流畅。
提升开发效率,运行效率,以及降低代码错误率这些很多问题,不能够去依赖工具和框架。
在不借助第三方工具,比如直接在 Github 上进行 Code Review 时,能够清晰看懂代码的逻辑。常见的问题有:
正则表达式
的使用:代码人都应该会写正则表达式,至少能看懂。所以正则表达式的可阅读性暂时不去具体讨论infer
推导等)大部分情况下仁者见仁智者见智,当不太懂的小白也能大概看懂代码的意思时,阅读性就算可以了。类似于白居易写诗先问老妪是否听懂。
而有的时候,你会发现,在读一段代码的时候,会有这样一种感慨——“小学生的心思像星空,我看得见却完全看不懂。”这种情况,就非常尴尬了。写代码追求的不是逼逼仄仄的东西拿来炫技,而是在满足需求的前提下,高效,易于团队的协同。
下期分解。