重学设计模式

设计模式和设计原则的合理应用非常依赖个人经验,用不好有时候会适得其反。学生时代时学习设计模式觉得枯燥,是因为没有实践经验。要多实践,然后再温故知新。

为什么要学设计模式/设计原则?

  1. 在经典的开源软件库、框架中,大量使用了设计模式,不懂设计模式很难看懂开源代码!即使看懂了也无法领会其中的精髓。更别说自己去创造一个开源项目。
  2. 一些设计模式会使得代码变得不直观可读性变差,但是可扩展性、可维护性更强了。能写出直观的代码的人很多,但能写出“复杂”代码(指引入了设计模式,而不是指复杂的业务逻辑)的人很少。

    设计模式/SOLID设计原则是解决代码的扩展性问题,KISS设计原则是解决代码的可读性问题。

  3. 有能力去做持续重构。初级工程师在维护代码,高级工程师在设计代码,资深工程师在重构代码

面向对象和golang

面向对象分析、设计:程序被拆解为哪些类、类里面有哪些方法(golang的interface)和属性(golang的struct)、类和类之间如何交互(golang里应该使用接口进行交互)。

面向对象编程:把设计翻译成代码。

封装(Encapsulation)

封装也叫作信息隐藏或者数据访问保护:仅暴露有限的方法,并且限制类中的部分属性的访问权限。

比如将属性设置为private,避免直接修改对象属性;只提供部分属性的get、set方法,对于不可变更的属性,如id,不提供任何访问或修改方法。

golang中,struct中大写字母开头的字段相当于public,小写字母开头的字段相当于private。

抽象(Abstraction)

封装的主要目的是隐藏数据,而抽象的目的是隐藏方法的具体实现

函数就可以看成是一种抽象,即使用者无需关注底层实现,只需要通过注释、文档等知道这个接口/函数是干啥的、怎么用就行了。其实就是帮助大脑过滤非必要信息,让我们只关注功能点。

可以看出来抽象是一个非常通用的设计思想,并非面向对象设计特有。所以抽象有的时候会被排除在面向对象的四大特性之外。

继承(Inheritance)

继承最大的好处是代码复用。

代码复用的问题也可以利用组合来解决。golang就是利用组合而不是继承来实现代码复用

通过组合匿名接口或者匿名struct,还可以进行“覆盖”(其实不是真正意义上的覆盖)。

多态(Polymorphism)

多态是指,子类可以替换父类。利用继承+覆盖实现。

在golang中,通过接口来实现多态,也就是说任何实现了接口的对象都能当参数传入。而golang本身是支持duck type语法的,所以这些对象之间可以没有任何关系。

设计原则

同时这也是 Code Review 的重要标准之一

  1. 针对接口编程,而不是针对实现编程。

    在golang里,即使一个接口只有一个实现类,也要用接口,而不要操作具体类,因为要为后续的可扩展性做好准备;

  2. 多用组合,少用继承;换句话说,“has-a”,比“is-a”更好。因为继承可能导致层次过深的问题。

    golang没有继承的概念,通过组合多个小接口来实现一个更大的接口;

    使用匿名接口/匿名结构体可以达到类似继承的效果,但这不是继承。

  3. SOLID:

    1. 单一职责原则:一个类只负责一件事情。为了能做到单一职责,应该进行持续重构
    2. 开闭原则:对扩展开放,对修改关闭。开闭原则讲的就是代码的扩展性问题。在23种经典设计模式中,大部分设计模式都是为了解决代码的扩展性问题而存在的,主要遵从的设计原则就是开闭原则。只要它没有破坏原有的代码的正常运行,没有破坏原有的单元测试,我们就可以说,这是一个合格的代码改动。
    3. 里氏替换原则:按照“协议”来设计子类,也就是说子类在覆盖父类的方法的时候,不要违背父类对某个方法的“约定”,这样,当代码里用到父类的地方,才真正的可以用子类去进行替换,而不会引发问题(这里要区分它和多态的区别)。判断子类的设计实现是否违背里式替换原则,有一个小窍门,那就是拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败,就有可能说明,子类的设计实现没有完全地遵守父类的约定,子类有可能违背了里式替换原则。
    4. 接口隔离原则:不应该让调用方看到他不关心/不应该调用的接口。在golang中,提倡设计小接口,并将小接口组合成大的接口。
    5. 依赖反转原则:用来指导框架层面的设计,而不是业务代码开发。高层模块不依赖低层模块,二者中间通过接口抽象进行连接,低层模块依赖接口抽象层。比如web服务器 <-> gunicorn <-> web框架
  4. KISS和YAGNI

    KISS和YAGNI原则用来保持代码的可读性和可维护性。但是可读性很多时候和可扩展性互相矛盾。过度引入设计模式会导致可读性降低,我们需要进行权衡。

    此外,实践KISS原则时,也可能会和性能相互冲突(比如直接引用现成的三方库中的通用接口和自己来实现一种更高效的算法)。但是,除非遇到性能瓶颈,否则可读性更重要。

    YAGNI:You Ain’t Gonna Need It.不要去设计你用不到的东西,不要过度设计。但是还是要保持代码的可扩展性。

重构与编程规范

重构

why

技术在更新、需求在变化、人员在流动,代码质量总会在下降,代码总会存在不完美,重构就会持续在进行。时刻具有持续重构意识,才能避免开发初期就过度设计,避免代码维护的过程中质量的下降。

一旦出现“破窗效应”,一个人往里堆了一些烂代码,之后就会有更多的人往里堆更烂的代码。毕竟往项目里堆砌烂代码的成本太低了。

when

不要等到问题堆得太多了去做大刀阔斧的重构,甚至重写,要时刻有人对代码整体质量负责任,平时没事就改改代码,持续性的进行小重构

编程规范

  1. 单元测试。保证代码质量的两个手段:Code Review和单测。单元测试本身的代码质量可以放低要求,copy-paste、有重复代码也是可以允许的。单元测试只关心被测函数实现了什么功能,不用逐行阅读函数里面的代码。

  2. 命名。对于接口的命名,一般有两种比较常见的方式。一种是加前缀“I”,表示一个 Interface。比如 IUserService,对应的实现类命名为 UserService。另一种是不加前缀,比如 UserService,对应的实现类加后缀“Impl”,比如 UserServiceImpl。

  3. 当函数参数过多时,可以考虑将函数参数变成对象,函数变成方法。

  4. 不要在函数中使用布尔类型的标识参数来控制内部逻辑,true 的时候走这块逻辑,false 的时候走另一块逻辑。

golang设计模式

创建型:主要解决“对象的创建”问题

结构型:主要解决“类或对象的组合或组装”问题

行为型:主要解决“类或对象之间的交互”问题

  1. 创建型
  2. 结构型
  3. 行为型

拓展:golang常用编程模式