目录
什么是设计模式
设计模式(Design pattern) :由软件开发人员在软件开发中面临常见问题的解决方案,是经过长时间的试验积累总结出来的,它使设计更加灵活和优雅,复用性更好。从实用的角度来看,它代表了某一类问题的最佳实践。
设计模式是研究类和本身以及类和类之间如何协作的模式,是在软件开发中常见问题的解决方案模板。这些模板是经验丰富的开发者在解决各种问题时提出的最佳实践的总结。设计模式提供了一种通用的、可重复使用的方法,可以用于解决特定类型的问题,以改善软件的结构、可维护性和可扩展性。总之一句话:设计模式是针对于面向对象编程而设计的一套代码编程规范或者说是套路。
设计模式到底解决了开发过程中的哪些难题呢,它又是如何来解决的呢?
其核心是:复用和解耦。使不稳定依赖于稳定、具体依赖于抽象,以此增强软件设计适应变化的能力。
什么是编程范式
要探讨设计模式和编程语言的关系,还得从编程范式谈起。编程范式一词最早来自 Robert Floyd 在1979年图灵奖的颁奖演说,是程序员看待程序的观点,代表了程序设计者认为程序应该如何被构建和执行的看法,与软件建模方式和架构风格有紧密关系。
当前主流的编程范式有三种:
1. 结构化编程(structured programming)
2. 面向对象编程(object-oriented programming)
3. 函数式编程(functional programming)
这几种编程范式之间的关系如下:
1. 起初是非结构化编程,指令(goto指令)可以随便跳转,数据可以随便引用。后来有了结构化编程,人们把 goto 语句去掉了,约束了指令的方向性,过程之间是单向的,但数据却是可以全局访问的;
2. 后来面向对象编程的时候,人们干脆将数据与其紧密耦合的方法放在一个逻辑边界内,约束了数据的作用域,靠关系来查找;
3. 到函数式编程的时候,人们约束了数据的可变性,通过一系列函数的组合来描述数据,从源到目标映射规则的编排,中间它是无状态的;
编程范式是抽象的,编程语言是具体的。编程范式是编程语言背后的思想,要通过编程语言来体现。C 语言的主流编程范式是结构化编程,而 Java 语言的主流编程范式是面向对象编程,后来 Java8 开始支持 Lambda 表达式,将函数式编程范式的内容融合进来,同时新诞生的语言一开始就支持多范式,比如 Scala,Go 和 Rust 等。
从结构化编程到面向对象编程,再到函数式编程,抽象程度越来越高(离图灵机模型越来越远),与领域问题的距离越来越近。直观地来讲,就是解决现实问题的效率提升了,灵活性和执行效率随之有所下降。
设计模式无论用什么语言实现都是可以的,然而由于语言的各自差异化特点,不是每种语言都完美或统一实现各种设计模式。比如 Java 里面有策略模式,那是因为 Java8 之前不支持方法传递,不能把一个方法当作参数传给别人,所以有了策略模式。而 JavaScript 等语言可以直接传函数,就根本没必要造一个策略模式出来。
什么是多态特性
面向对象编程语言有三大特性:封装、继承和多态。
1. 封装即信息隐藏或数据保护,“数据结构”通过暴露有限的访问接口,授权外部仅能通过"数据结构"提供的方法(函数)来访问其内部的数据;
2. 继承的好处是可以实现代码复用,但不应过度使用,如果继承的层次过深就会导致代码可读性和可维护性变差。因此建议少用继承而多用组合模式;
3. 多态可以分为变量的多态,方法的多态,类的多态。通常强调的是类的多态,多态的实现是指子类可以替换父类,在实际代码运行过程中调用子类的方法实现;
多态可以说是面向对象中最重要的一个特性,是解决项目中紧偶合的问题,提高代码的可扩展性和可复用性的核心,是很多设计模式、设计原则、编程技巧的代码实现基础。
多态比较直观的理解就是去完成某个动作,当不同的对象去完成时会产生出不同的状态,其作用范围可以是方法的参数和方法的返回类型。
多态这种特性也需要编程语言提供特殊的语法机制来实现,Java 中多态可以通过"子类继承父类+子类重写父类方法+父类引用指向子类对象"的方式实现,还可以通过"接口语法"的方式实现。C++中则使用 virtual(虚函数)关键字来实现。像一些动态语言如 Python 也可以通过 duck-typing 的语法实现,另外 Go 语言中的"隐藏式接口"也算是 duck-typing。
Python 语言,实现多态示例如下
class MyFile:
def write(self):
print('I write a message into file.')
class MyDB:
def write(self):
print('I write data into db. ')
def doIt(writer):
writer.write()
def demo():
myFile= MyFile()
myDB = MyDB()
doIt(myFile)
doIt(myDB )
设计模式与架构模式
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
了解架构模式
对给定上下文的软件架构中常见问题的一种通用的可复用的解决方案,可以为设计大型软件系统的各个方面提供相应的指导。它不仅显示了软件需求和软件结构之间的对应关系,而且指定了整个软件系统的组织和拓扑结构,提供了一些设计决策的基本原理,常见的架构设计模式如下:
架构模式 | 模式描述 | 适用场景 |
分层模式 (Layered pattern) | 用于可分解为子任务的结构化程序,每个子任务都位于特定的抽象层级,每一层都为上一层提供服务。 | 桌面应用程序; |
客户端-服务器模式 (Client-server pattern) | 服务器将向多个客户端提供服务。客户端从服务器请求服务,服务器向这些客户端提供相关服务。 | 电子邮件、文档共享和银行等在线应用程序; |
主从模式 (Master-slave pattern) | 主节点将工作分配给相同的从节点,并根据从节点返回的结果计算最终结果。 | 数据库主从复制; |
管道-过滤器模式 (Pipe-filter pattern) | 用于构造生成和处理数据流的系统。每个处理步骤都包含一个过滤器组件。要处理的数据通过管道传递。这些管道可用于缓冲或同步目的。 | 编译器; |
代理模式 (Broker pattern) | 通过解耦组件来构造分布式系统。 | 消息中间件; |
点对点模式 (Peer-to-peer pattern) | 每个组件都称为对等节点。对等节点既可以作为客户机(从其他对等节点请求服务),也可以作为服务器(向其他对等节点提供服务)。 | 文件共享网络; |
事件-总线模式 (Event-bus pattern) | 订阅发布模式,事件源将消息发布到事件总线上的特定通道,监听者订阅特定的通道。 | 通知服务; |
模型-视图-控制器模式(Model-view-controller pattern) | MVC模式,解耦组件并允许有效的代码重用。 | web应用程序架构; |
黑板模式 (Blackboard pattern) | 对于没有确定解决方案策略的问题非常有用,所有的组件都可以到达黑板。组件可以生成添加到黑板上的新数据对象。组件在黑板上查找特定类型的数据,并通过与现有的知识源进行模式匹配找到这些数据。 | 语音识别; |
解释器模式 (Interpreter pattern) | 用于设计一个解释专用语言编写的程序组件。 | 数据库查询语言,如SQL; |
了解设计模式
在1995年,有四位编程界的前辈合著了一本书,书名叫做《Design Patterns: Elements of Reusable Object-Oriented Software》,翻译过来就是《设计模式:可复用面向对象软件的基础》,书里面总共收录了23种设计模式。这本书是软件研发领域重要的里程碑,合著此书的四位作者,被业内称为GoF(Gang of Four),因此这本书也被人称为GoF设计模式。
设计模式按照目的来分类有:创建、结构、行为三种,按照作用范围来分类有:类模式和对象模式两种。
1. 创建型模式:用于创建对象,就是将对象的创建与使用分离。从而降低系统的耦合度,使用者不需要关注对象的创建细节,对象的创建由相关的工厂来完成。
2. 结构型模式:描述如何将类,对象,接口之间按某种布局组成更大的结构。
3. 行为型模式:用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
23种设计模式如下:
类型 | 模式名称 | 模式描述 |
创建型 | 单例模式 (Singleton) | 某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。 |
工厂方法模式 (Factory Method) | 定义一个用于创建产品的接口,由子类决定生产什么产品。 | |
抽象工厂模式 (AbstractFactory) | 提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。 | |
建造者模式 (Builder) | 将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。 | |
原型模式 (Prototype) | 将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。 | |
结构型 | 适配器模式 (Adapter) | 将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。 |
桥接模式 (Bridge) | 将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。 | |
组合模式 (Composite) | 将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。 | |
装饰模式 (Decorator) | 动态地给对象增加一些职责,即增加其额外的功能。 | |
外观模式 (Facade) | 为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。 | |
亨元模式 (Flyweight) | 运用共享技术来有效地支持大量细粒度对象的复用。 | |
代理模式 (Proxy) | 为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。 | |
行为型 | 模板方法模式 (TemplateMethod) | 定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。 |
策略模式 (Strategy) | 定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。 | |
命令模式 (Command) | 将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。 | |
职责链模式 (Chain of Responsibility) | 把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。 | |
状态模式 (State) | 允许一个对象在其内部状态发生改变时改变其行为能力。 | |
观察者模式 (Observer) | 多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。 | |
中介者模式 (Mediator) | 定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。 | |
迭代器模式 (Iterator) | 提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。 | |
访问者模式 (Visitor) | 在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。 | |
备忘录模式 (Memento) | 在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。 | |
解释器模式 (Interpreter) | 提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。 |
小结
· 架构模式更像是宏观战略层面的设计,设计模式则更像是战略目标拆解出来的具体任务的实现方案;
• 软件架构是软件的一种搭建形式,往往规定了软件的模块组成,通信接口(含通信数据结构),组件模型,集成框架等,往往规定了具体的细节;
• 设计模式是一种软件的实现方法,是一种抽象的方法论,是为了更好的实现软件而归纳出来的有效方法;
• 实现一种软件架构,不同组成部分可能用到不同的设计模式,某个部分也可能可以采用不同的设计模式来实现。
为什么需要设计模式
它是为了解决结构混乱、代码阅读困难、代码扩展麻烦以及代码重用很复杂这几个棘手的问题的。
经过上面我粗浅的介绍,我们有请今天的主角:设计模式的七大原则出场。。。当当。。。
设计模式的七大原则
-
单一职责原则(类和方法,接口)
-
开闭原则 (扩展开放,修改关闭)
-
里氏替换原则(基类和子类之间的关系)
-
依赖倒置原则(依赖抽象接口,而不是具体对象)
-
接口隔离原则(接口按照功能细分)
-
合成复用原则
-
迪米特法则 (类与类之间的亲疏关系)
接下来逐一介绍一下这七大原则
1.单一职责原则
单一职责原则指的是类的职责单一,对外只提供一种方法。通俗理解:你让司机把车开到修理厂,并且对车进行维修,不好意思,司机只负责把车开到修理厂,不负责维修。
代码示例如下:
张三写的初版代码
package main
import "fmt"
// 单一职责:每个类只对外提供一种功能
// 司机类
type Driver struct {
}
// 司机类有一个开车的方法
func (d *Driver) Drive() {
fmt.Println("开车")
}
// 司机类还有一个修车的方法
func (d *Driver) Fix() {
fmt.Println("修车")
}
func main() {
d := Driver{}
// 司机开车
d.Drive() //输出:开车
// 司机修车
d.Fix() //输出:修车
}
这时候张三想要调整一下代码,把修车的逻辑也改成开车的逻辑,这就是不遵循单一职责原则的坏处,修改某个类方法的逻辑时,有可能会影响到该类的其他方法的准确性,造成误解。在留下这一个大坑后,张三离职了。。。
package main
import "fmt"
// 单一职责:每个类只对外提供一种功能
// 司机类
type Driver struct {
}
// 司机类有一个开车的方法
func (d *Driver) Drive() {
fmt.Println("开车")
}
// 司机类还有一个修车的方法
func (d *Driver) Fix() {
fmt.Println("开车") /***这行做了修改***/
}
func main() {
d := Driver{}
// 司机开车
d.Drive() //输出:开车
// 司机修车
d.Fix() //输出:开车
}
这时候,李四入职了,看到张三的代码,发现Drive方法和Fix方法的逻辑是一样一样的,于是李四也修改了一版代码,发现结果仍旧是不变的,就变成了下面这样。
package main
import "fmt"
// 单一职责:每个类只对外提供一种功能
// 司机类
type Driver struct {
}
// 司机类有一个开车的方法
func (d *Driver) Drive() {
fmt.Println("开车")
}
// 司机类还有一个修车的方法
func (d *Driver) Fix() {
fmt.Println("开车")
}
func main() {
d := Driver{}
// 司机开车
d.Drive() //输出:开车
// 司机修车
d.Drive() //输出:开车 /**这行做了修改**/
}
这时候李四也离职了,可怜的王五入职了,他发现无论是司机开车,还是司机修车,都需要调用Drive方法,他可能就认为:哦~司机在开车和修车之前都需要调用Drive方法。这就造成了很严重的歧义。
这种歧义其实就是没有遵守单一职责原则而导致的。那么正确的写法应该怎样呢,王五做了如下修改:
package main
import "fmt"
// 单一职责:每个类只对外提供一种功能
// 司机类:专门负责开车
type Driver struct {
}
// 司机类有一个开车的方法
func (d *Driver) Drive() {
fmt.Println("开车")
}
// 修理工类:专门负责修车
type Fixer struct { /**修改:增加了一个修理工类**/
}
// 修理工类有一个修车的方法
func (f *Fixer) Fix() { /**修改:对修理工类增加一个修理方法**/
fmt.Println("修车")
}
func main() {
d := Driver{}
// 司机开车
d.Drive() //输出:开车
// 修理工修车
f := Fixer{}
f.Fix() // 输出:修车
}
这样呢,无论你修改司机类的方法还是修理工类的方法,都不会影响到其他方法。
2.开闭原则
开闭原则是指类的改动是通过增加代码来实现的,而不是修改源代码。(对扩展开放,对修改关闭)通俗理解:你需要汽车司机,就去招募汽车司机,需要飞行员就去招募飞行员,你不能要求一个人既会开汽车又会开飞机,甚至以后还要求他会开坦克...
代码示例如下:
张三写的初版代码(不遵循开闭原则的)
package main
import "fmt"
type People struct {
}
func (p *People) Drive() {
fmt.Println("司机开车")
}
func (p *People) Fly() {
fmt.Println("飞行员开飞机")
}
func main() {
var p People
p.Drive() //司机开车
p.Fly() //飞行员开飞机
}
这种结构有个问题就是,如果还有其他的人物角色,比如船员,那么我们只能再增加一个船员的方法
func (p *People) Ship() {
fmt.Println("船员开船")
}
这样其实就对People这个类进行了修改,那么就有可能会影响到这个类的其他方法的功能。
那么应该怎么修改呢,我们来看一下王五的优化方法
package main
// 开闭原则
import "fmt"
/*
开闭原则:
类的改动是通过增加代码来实现的,而不是修改源代码
*/
// 通过抽象出People,让其他类直接实现People的方法即可
type People interface {
doWork()
}
type Driver struct {
}
func (d *Driver) doWork() {
fmt.Println("司机开车")
}
type Pilot struct {
}
func (p *Pilot) doWork() {
fmt.Println("飞行员开飞机")
}
func main() {
// 司机开车
d := Driver{}
d.doWork()
// 飞行员开飞机
p := Pilot{}
p.doWork()
}
这样写的好处在于,当有一个新的职业出现时,只需要继承People的doWork方法即可,完全不会影响之前其他职业的方法。
*更进一步:开闭原则的基础上进行拓展,多态的实现
package main
// 开闭原则
import "fmt"
/*
开闭原则:
类的改动是通过增加代码来实现的,而不是修改源代码
*/
type People interface {
doWork()
}
type Driver struct {
}
func (d *Driver) doWork() {
fmt.Println("司机开车")
}
type Pilot struct {
}
func (p *Pilot) doWork() {
fmt.Println("飞行员开飞机")
}
// 对抽象对象进行操作,用于实现多态方法
// 多态:父类指针指向子类对象,调用子类对象的方法
func PeopleDoWork(p People) { /***修改的部分***/
p.doWork()
}
func main() {
d := Driver{}
d.doWork() // 司机开车
p := Pilot{}
p.doWork() // 飞行员开飞机
fmt.Println("---------------")
PeopleDoWork(&Driver{}) // 司机开车 /**修改的部分**/
PeopleDoWork(&Pilot{}) // 飞行员开飞机 /**修改的部分**/
}
当我们新增一个类时,完全不会影响到之前的代码逻辑,可以放心地进行修改。
3.里氏替换原则
里氏替换原则指的是任何抽象类/基类(interface)都可以用它的实现类来进行替换。通俗理解:任何拥有A类驾照的人,都能开C类驾照的车。(这里可以把C类驾照理解为基类)
这个原则的代码示例可以参考前面一段代码示例,People是基类,Driver和Pilot是实现类,任何People出现的地方,都可以用Diver或Pilot来替换。
4.依赖倒转原则
依赖倒转原则是实现层和业务逻辑层只依赖于抽象类(interface),不依赖于具体实现类(struct),面向接口编程。通俗理解:汽车企业造车,同一类车型(抽象),都按照相同的构造制造,不同的型号或批次(具体实现)可以添加一些不同的细节,不是直接按照每一辆车的型号进行制造。
不使用依赖倒转原则的代码
package main
import "fmt"
type BMWCar struct {
}
func (c *BMWCar) Run() {
fmt.Println("BMW car is running")
}
type AudiCar struct {
}
func (c *AudiCar) Run() {
fmt.Println("Audi car is running")
}
type Zhang3 struct {
}
func (z *Zhang3) DriveBMW(car *BMWCar) {
fmt.Println("zhang3 is driving car")
car.Run()
}
func (z *Zhang3) DriveAudo(car *AudiCar) {
fmt.Println("zhang3 is driving car")
car.Run()
}
type Li4 struct {
}
func (l *Li4) DriveBMW(car *BMWCar) {
fmt.Println("li4 is driving car")
car.Run()
}
func (z *Li4) DriveAudo(car *AudiCar) {
fmt.Println("Li4 is driving car")
car.Run()
}
func main() {
var bmw *BMWCar
var audi *AudiCar
var z3 Zhang3
var l4 Li4
z3.DriveBMW(bmw)
z3.DriveAudo(audi)
l4.DriveBMW(bmw)
l4.DriveAudo(audi)
}
大家发现问题没有,不使用依赖倒转,如果新增一个司机wang5,并且新增一个车型benz,就需要把wang5重新实现DriveAudi,DriveBMW,DriveBenz这三个方法,形成司机和车型的全组合。每新增一个用户和一个车型,都需要补充全部的方法。
使用依赖倒转原则以后,再进行扩充,只需要实现各自抽象类(interface)的方法即可,无需关注其他类都有哪些方法。
package main
import "fmt"
// 依赖倒转原则:实现层和业务逻辑层都只依赖于抽象层
// 抽象层
// 抽象层之间相互依赖
type Car interface {
Run()
}
type Driver interface {
Drive(car Car)
}
// 实现层
// 汽车实现层
// Benz只需要实现Run方法即可
type Benz struct {
}
func (b *Benz) Run() {
fmt.Println("benz is running")
}
type Bmw struct {
}
func (b *Bmw) Run() {
fmt.Println("bmw is running")
}
// 实现层
// 司机实现层
// zhang3只需要实现Drive方法即可
type zhang3 struct {
}
func (z *zhang3) Drive(car Car) {
fmt.Println("zhang3 开汽车")
car.Run()
}
type li4 struct {
}
func (l *li4) Drive(car Car) {
fmt.Println("li4 开汽车")
car.Run()
}
// 业务逻辑层
func main() {
// 只依赖于抽象层,针对抽象层编程
// 抽象汽车
var car Car
car = new(Benz)
// 抽象司机
var driver Driver
// 里氏替换原则,用具体实现类替换抽象类
driver = new(zhang3)
driver.Drive(car) // zhang3 开汽车 benz is running
car = new(Bmw)
driver = new(li4)
driver.Drive(car) // li4 开汽车 bmw is running
}
如果需要增加一个wang5,只需要让wang5实现Drive方法即可
// 司机实现层增加代码如下
type wang5 struct {
}
func (l *wang5) Drive(car Car) {
fmt.Println("wang5 开汽车")
car.Run()
}
// 业务逻辑层增加代码如下
var wang5 Driver
wang5=new(wang5)
wang5.Drive(car)
如上图所示,司机只需要实现司机的方法,汽车只需要实现汽车的方法,完全不需要关心其他司机或者其他汽车有什么方法。大大降低了耦合性。
5.接口隔离原则
接口隔离原则是指接口应该“小而专”,不应该强迫用户依赖那些用不到的接口方法。这个原则的代码跟合成复用原则的代码合并到一起介绍。
6.合成复用原则
合成复用原则是指,如果修改父类的方法会影响子类的方法,那么这个父类和子类就不应该采用继承,而应该使用组合。
★补充知识点:如果一个struct嵌套了另一个有名结构体,那么这个模式就叫组合。如果一个struct嵌套了另一个匿名结构体(只有类型没有名字),那么这个结构可以直接访问匿名结构体的方法,从而实现了继承。如果一个struct嵌套了多个匿名结构体,那么这个结构可以直接访问多个匿名结构体的方法,从而实现了多重继承。
”
如下代码,举例说明继承和组合的关系:
package main
import "fmt"
type Dog struct {
}
func (d *Dog) Eat() {
fmt.Println("dog eat food")
}
// 继承Dog所有的属性,其中就包括继承了Eat方法
type ChaiDog struct {
Dog
}
func (c *ChaiDog) Sleep() {
fmt.Println("dog is sleeping")
}
// 组合方式有两种
// 1.直接在结构体中组合
type JingDog struct {
d Dog
}
func (j *JingDog) Eat() {
j.d.Eat()
}
func (j *JingDog) Sleep() {
fmt.Println("jingDog is sleeping")
}
// 2.通过参数传递的方式组合
type GuifuDog struct {
}
// 只把Eat方法跟Dog对象耦合,其他方法都不耦合
func (gf *GuifuDog) Eat(dog Dog) {
dog.Eat()
fmt.Println("guifuDog eat food")
}
func main() {
// 原始Dog类对象
d1 := Dog{}
d1.Eat()
// chaiDog继承Dog类的所有
cd := ChaiDog{}
cd.Eat() // 调用继承过来的Eat方法
cd.Sleep()
jd := JingDog{}
jd.Eat()
gfd := GuifuDog{}
gfd.Eat(d1)
}
继承的弊端:
1. 灵活性低,继承容易导致代码嵌套层次很深,可维护性变差。
2. 耦合性高。父类修改方法可能影响到子类行为。
3. 使用继承,在子类对象调用父类方法的时候,父类的方法也会被调用(不局限于Golang语言),如果继承层级很深的话,所有祖先对象相同的方法也都会被调用一遍,大大降低了效率。
使用组合的方法,可以仅仅依赖某个对象的属性或者某个方法,能够极大降低依赖关系,减小耦合。所以一般推荐使用聚合/组合代替继承。
7.迪米特原则
迪米特原则是指一个对象应该尽量少的了解其他对象,从而降低耦合度。
这里留一个扣子,等到本系列后续介绍到外观模式的时候,再来补充迪米特法则的代码示例,敬请期待。。。