🎉超级简单的设计模式教程🎉
_
这是一个会使任何人豁然开朗的主题。我会通过尽可能简单的方式来解释设计模式,使他能够深深的印在你们的(甚至我的)脑海里。
介绍
设计模式是解决问题的方法、是解决固定问题的指导原则。不是类、包或者库,你可以直接放到你的应用里,然后等待奇迹的发生。它们是解决特定场景下特定问题的指导原则。
设计模式是解决问题的方法,是解决特定问题的指导原则。
维基百科这样描述。
在软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。这个术语是由埃里希·伽玛(Erich Gamma)等人在1990年代从建筑设计领域引入到计算机科学的。
设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。面向对象设计模式通常以类别或对象来描述其中的关系和相互作用,但不涉及用来完成应用程序的特定类别或对象。设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力。
⚠️注意
示例代码使用golang,不过,这对你来说应该不是问题,因为设计模式是通用的。
设计模式的类型
创建型设计模式
直白的说。
创建型设计模式主要关注于如何创建一个对象或者如何组织相关的对象。
维基百科这样描述。
在软件工程中,创建型设计模式是处理对象创建机制,试着去使用合适的方式去创建对象。基本的对象创建方式会导致设计上的问题或者会带来设计上的复杂性。创建型设计模式就是用来解决这些问题的。
简单工场模式
工场方法模式
抽象工场模式
建造者模式
原型模式
单例模式
简单工场模式
现实世界中的例子。
想象一下,你正在建房子然后你需要门。你可以穿上木匠服,找一些木头、胶水、钉子以及其它制造门需要的工具,然后开始制作。当然,你也可以打电话给工场,让他们制作好门送过来,这样的话,你就不需要掌握制作门所需要的知识以及处理制作门的过程中糟心事。
直白的说。
简单工场模式简单的生成实例,而不需要暴露任何生成实例的逻辑。
维基百科这样描述。
在面向对象编程中,一个工场就是一个对象,它可经用来创建其它对象。通常一个工场是一个返回各种各样的对象的函数或方法,它通过调用一些方法来返回对象,比如"new"。
例子
首先我们有一个门的接口和实现。
type Door interface{
GetWidth() float64
GetHeight() float64
}
type WoodenDoor struct{
width float64
height float64
}
func NewWoodenDoor(width,height float64) *WoodenDoor{
return &WoodenDoor{
width: width,
height: height,
}
}
func (wd *WoodenDoor) GetWidth() float64{
return wd.width
}
func (wd *WoodenDoor) GetHeight() float64{
return wd.height
}
我们现在需要一个门工场来制作一个门。
type DoorFactory struct{}
func (*DoorFactory) MakeDoor(width,height float64) Door{
return NewWoodenDoor(width, height)
}
这样使用
df:=&DoorFactory{}
door:=df.MakeDoor(100,200)
fmt.Println("width:",door.GetWidth())
fmt.Println("height:",door.GetHeight())
door2:=df.MakeDoor(50,100)
何时使用?
当需要创建一个对象,并且这个创建过程不是简单的赋值,而且涉及一些逻辑,这个时候将创建过程放进工场里从而去除每次创建的重复代码,就显得很有意义了。
工场方法
现实世界中的例子。
想象一下,一个招聘经理不可能一个人面试所有的岗位。她得考虑根据岗位的不同,将不同的面试流程委托给不同的人。
直白的说。
工场方法将创建对象的逻辑委托给子类。
维基百科这样描述。
在基于类的编程中,工场方法模式用于处理创建对象而又不用指出具体对象的类型的一种创建型设计模式。这是通过调用一个工场方法来实现的。这个工场方法可以声明在接口里然后通过子类来实现,也可以声明在基类里然后通过子类来重写。而不是直接调用构造方法。
例子
以上面的招聘经理为例。首先要有一个面试官接口和它的一些实现。
type Interviewer interface {
AskQuestions()
}
type Developer struct {
}
func (d *Developer) AskQuestions() {
println("ask about design patterns!")
}
type CommunityExecutive struct {
}
func (c *CommunityExecutive) AskQuestions() {
println("ask about community building!")
}
现在再创建一个招聘经理接口
type HiringManager interface {
MakeInterviewer() Interviewer
}
然后可以创建招聘经理接口的一些实现类
type DeveloperManager struct {
}
func (d *DeveloperManager) MakeInterviewer() Interviewer {
return &Developer{}
}
type MarketingManager struct {
}
func (m *MarketingManager) MakeInterviewer() Interviewer {
return &CommunityExecutive{}
}
最后使用它
dm:=&DeveloperManager{}
developer:=dm.MakeInterviewer()
developer.AskQuetions()
mm:=&MarketingManager{}
marketing:=mm.MakeInterviewer()
MarketingManager.AskQuetions()
何时使用?
当一个类中有通用逻辑但是子类需要在运行时动态创建时,就派上用场了。或者换句话说,当客户端不知道它自己需要哪个子类时。
抽象工场
现实世界中的例子。
扩展一下简单工场中门的例子。基于你的需求,你可能会从木门店购买木门,铁门店购买铁门或者塑料门店购买塑料门。另外你可能还需要一个有专业技能的工人来安装门。例如木匠来安装木门,电焊工安装铁门等等。可以发现现在各种门都有依赖,木门需要一个木匠、铁门需要一个电焊工。
直白的说。
抽象工场是工场的工场。一个工场将分离的但是相关的/相互依赖的工场组织起来,而不需要指明它们的实体类。
维基百科这样描述。
抽象工场模式提供一个将一组分离且具有相同主题的工场封装起来的方法,并且不需要指明它们的实体类。
例子
修改一下上面关于门的例子。首先我们需要门的接口,以及它的一些实现。
type Door interface {
GetDescription()
}
type WoodenDoor struct {
}
func (d *WoodenDoor) GetDescription() {
println("i am a wooden door")
}
type IronDoor struct {
}
func (d *IronDoor) GetDescription() {
println("i am a iron door")
}
然后我们需要每种门的安装工人
type DoorFittingExpert interface {
GetDescription()
}
type Welder struct {
}
func (w *Welder) GetDescription() {
println("i can only fit iron doors")
}
type Carpenter struct {
}
func (w *Carpenter) GetDescription() {
println("i can only fit wooden doors")
}
再然后我们需要一个抽象工场将相关的对象组织起来,比如,木门工场需要提供木门和木门安装工人,而铁门工场需要提供铁门和电焊工人。
type DoorFactory interface {
MakeDoor() Door
MakeFittingExpert() DoorFittingExpert
}
type WoodenDoorFactory struct {
}
func (wdf *WoodenDoorFactory) MakeDoor() Door {
return &WoodenDoor{}
}
func (wdf *WoodenDoorFactory) MakeFittingExpert() DoorFittingExpert {
return &Carpenter{}
}
type IronDoorFactory struct {
}
func (idf *IronDoorFactory) MakeDoor() Door {
return &IronDoor{}
}
func (idf *IronDoorFactory) MakeFittingExpert() DoorFittingExpert {
return &Welder{}
}
使用
woodenDoorFactory:=&WoodenDoorFactor{}
door:=woodenDoorFactory.MakeDoor()
expert:=woodenDoorFactory.MakeFittingExpert();
door.GetDescription()
expert.GetDescription()
ironDoorFactory:=&IronDoorFactor{}
door:=ironDoorFactory.MakeDoor()
expert:=ironDoorFactory.MakeFittingExpert();
door.GetDescription()
expert.GetDescription()
可以看出木门工场已经将木匠和木门封装,铁门工场也将铁门和电焊工封装了。这样的话,我们就不会把门和对应的安装工人搞错了。
何时使用?
当有相互关联的依赖,且这些依赖创建起来时又不是那么简单时。
创建者模式
现实世界中的例子。
假设你在哈迪斯快餐店,你下了一个订单,比如:大份哈迪。然后店员会把大份会把大份哈迪给你,而不会问你任何问题,这是简单工场的例子。但是有些场景创建逻辑可能会涉及更多的步骤。例如你想要一份定制的赛百味,你可以有多个汉堡制作选项,你想要什么面包?你想要什么调味酱?你想要什么奶酪?等等。在这种场景下,创建者模式就有了用武之地。
直白的说。
允许创建不同种类的对象同时避免构造函数污染。当创建多种类型的对象或者创建对象涉及很多步骤的场景,比较有用。
维基百科这样描述。
创建者模式是用于解决重叠构造器这种反模式的一种对象创建设计模式。
简单说一下重叠构造器反模式是什么意思。我们经常会看到如下这种构造函数。
func New(size int,cheese bool,pepperoni bool,tomato bool,lettuce bool){
}
如你所见。构造函数的参数很快就会超出控制并且会很难理解。另外参数列表可能还会增长,假如将来你想去添加更多的选项。这就叫重叠构造器反模式。
例子
比较好选择是创建者模式。首先我们需要一个汉堡。
type Burger struct{
size int
cheese bool
pepperoni bool
lettuce bool
tomato bool
}
func NewBurger(builder *BurgerBuilder) *Burger{
return &Burger{
size:builder.Size,
cheese:builder.Cheese,
pepperoni:builder.Pepperoni,
lettuce:builder.Lettuce,
tomato:builder.Tomato,
}
}
然后还需要一个创建者。
type BurgerBuilder struct{
Size int
Cheese bool
Pepperoni bool
Lettuce bool
Tomato bool
}
func NewBurgerBuilder(size int) &BurgerBuilder{
return &BurgerBuilder{
Size:size,
}
}
func (bb *BurgerBuilder) AddPepperoni()&BurgerBuilder{
bb.Pepperoni=true
return bb
}
func (bb *BurgerBuilder) AddCheese()&BurgerBuilder{
bb.Cheese=true
return bb
}
func (bb *BurgerBuilder) AddLettuce()&BurgerBuilder{
bb.Lettuce=true
return bb
}
func (bb *BurgerBuilder) AddTomato()&BurgerBuilder{
bb.Tomato=true
return bb
}
func (bb *BurgerBuilder) Build() *Burger{
return NewBurger(bb)
}
使用。
burgerBuilder:=NewBurgerBuilder(14)
burgerBuilder.
AddPepperoni().
AddCheese().
AddLettuce().
AddTomato().
Build()
何时使用?
当一个对象有很多选项时为了避免构造函数重叠。与工场模式不同的是,工场模式的创建是一步模式,而创建者模式可能会有很多步。
原型模式
真实世界中的例子。
还记得多莉吗?就是那个克隆羊!我们不用关注细节,只需要了解这是关于克隆的例子。
直白的说。
通过克隆基于原有的对象创建一个新的对象。
维基百科这样描述。
在软件开发中,原型模式是一种创建型模式。当新创建的对象取决于已有的对象时,你可以使用它。它是使用克隆的方式产生新的对象。
简单的说,它允许你去创建一个已存在对象的副本然后根据需要去修改副本。这样你就不需要了解创建、设置新对象的过程。
例子
type Sheep struct{
name string
category string
}
func New(name, category string) *Sheep{
return &Sheep{
name:name,
category:category,
}
}
func (s *Sheep) SetName(name string){
s.name=name
}
func (s *Sheep) GetName() string{
return s.name
}
func (s *Sheep) SetCategory(category string){
s.category=category
}
func (s *Sheep) GetCategory(category string){
return s.category
}
func (s *Sheep) Clone() *Sheep{
r:=*s
return &r
}
可以像下面一样克隆。
original:=New('jolly','Mountain Sheep')
println(original.GetName())
println(original.GetCategory())
cloned:=original.Clone()
cloned.SetName('Dolly')
println(original.GetName())
println(original.GetCategory())
何时使用
当新创建的对象和已存在的对象相似,或者创建比克隆代价更大的时候。
单例模式
真实世界中的例子。
一个国家同一时间只能有一个总统。一旦有责任名唤他就会采取行动。在这里总统就是单例的。
直白的说。
确保指定的类永远只能创建唯一一个实例。
维基百科这样描述。
在软件工程中,单例模式是一种限制类的实例个数为一个软件设计模式。在整个系统中,为了协调操作,某个类只需要一个实例时就比较有用。
单例模式其实是一种反模式,尽量避免过多使用。使用它也不一定有坏处,而且也有一些有效的使用案例,但是使用的时候要小心。因为它会引入全局状态,在某处修改了它会影响到别处使用的地方,会使用程序难以调试。另外它也会使用代码紧紧藕合在一起,mock单例也比较困难。
例子
去创建单例,将构造函数设置为私有,关闭克隆、不允许扩展,然后使用一个静态变量存储实例。(golang语言可以使用sync.Once来实现)
type Persident struct{}
var persident *Persident
var once sync.Once
func GetInstance() *Persident {
once.Do(func() {
persident = &Persident{}
})
return persident
}
使用
p1:=GetInstance()
p2:=GetInstance()
println(p1==p2)
评论