一个菜鸟的设计模式之旅,文章可能会有不对的地方,恳请大佬指出错误。
编程旅途是漫长遥远的,在不同时刻有不同的感悟,本文会一直更新下去。
由于 Go 中缺少类和继承等 OOP 特性, 所以无法使用 Go 来实现经典的工厂方法模式。 不过, 我们仍然能实现模式的基础版本, 即简单工厂。案例中使用工厂结构体来构建多种类型的武器。因此工厂方法模式代码使用 C# 表示。
Gun: AK47 gun
Power: 4
Gun: Musket gun
Power: 1
程序代码、简单工厂模式 Golang
首先, 创建一个名为 iGun
的接口, 其中将定义一支枪所需具备的所有方法。 然后是实现了 iGun 接口的 gun
枪支结构体类型。 两种具体的枪支——ak47
与 musket
火枪 ——两者都嵌入了枪支结构体, 且间接实现了所有的 iGun
方法。
gunFactory
枪支工厂结构体将发挥工厂的作用, 即通过传入参数构建所需类型的枪支。 main.go 则扮演着客户端的角色。 其不会直接与 ak47
或 musket
进行互动, 而是依靠 gunFactory
来创建多种枪支的实例, 仅使用字符参数来控制生产。
iGun.go: 产品接口
package main
type IGun interface {
setName(name string)
setPower(power int)
getName() string
getPower() int
}
gun.go: 具体产品
package main
type Gun struct {
name string
power int
}
func (g *Gun) setName(name string) {
g.name = name
}
func (g *Gun) getName() string {
return g.name
}
func (g *Gun) setPower(power int) {
g.power = power
}
func (g *Gun) getPower() int {
return g.power
}
ak47.go: 具体产品
package main
type Ak47 struct {
Gun
}
func newAk47() IGun {
return &Ak47{
Gun: Gun{
name: "AK47 gun",
power: 4,
},
}
}
musket.go: 具体产品
package main
type musket struct {
Gun
}
func newMusket() IGun {
return &musket{
Gun: Gun{
name: "Musket gun",
power: 1,
},
}
}
gunFactory.go: 工厂
package main
import "fmt"
func getGun(gunType string) (IGun, error) {
if gunType == "ak47" {
return newAk47(), nil
}
if gunType == "musket" {
return newMusket(), nil
}
return nil, fmt.Errorf("Wrong gun type passed")
}
main.go: 客户端代码
package main
import "fmt"
func main() {
ak47, _ := getGun("ak47")
musket, _ := getGun("musket")
printDetails(ak47)
printDetails(musket)
}
func printDetails(g IGun) {
fmt.Printf("Gun: %s", g.getName())
fmt.Println()
fmt.Printf("Power: %d", g.getPower())
fmt.Println()
}
Console: 输出
Gun: AK47 gun
Power: 4
Gun: Musket gun
Power: 1
程序代码、工厂方法模式 C#
程序功能和简单工厂相同,用工厂方法模式实现。
Gun.cs: 武器类
namespace 工厂方法;
public abstract class Gun
{
protected Gun(string name, int power)
{
Name = name;
Power = power;
}
public string Name { get; private set; }
public int Power { get; private set; }
public void setName(string name)
{
Name = name;
}
public void setPower(int power)
{
Power = power;
}
}
public class AK47 : Gun
{
public AK47() : base("ak47", 10)
{
}
}
public class Musket : Gun
{
public Musket() : base("musket", 5)
{
}
}
Factory.cs: 工厂类
namespace 工厂方法;
public interface GunFactory
{
Gun createGun();
}
public class AK47Factory : GunFactory
{
public Gun createGun()
{
Console.WriteLine("正在生产AK47");
return new AK47();
}
}
public class MusketFactory : GunFactory
{
public Gun createGun()
{
Console.WriteLine("正在生产Musket");
return new Musket();
}
}
Program.cs: 客户端代码
using 工厂方法;
GunFactory gunFactory = new AK47Factory();
Gun ak47_1 = gunFactory.createGun();
Gun ak47_2 = gunFactory.createGun();
gunFactory = new MusketFactory();
Gun Musket_1 = gunFactory.createGun();
List<Gun> guns = new List<Gun>() { ak47_1, ak47_2, Musket_1 };
foreach (Gun gun in guns)
{
Console.WriteLine($"武器名字:{gun.Name}");
Console.WriteLine($"武器伤害:{gun.Power}");
}
Console: 输出
正在生产AK47
正在生产AK47
正在生产Musket
武器名字:ak47
武器伤害:10
武器名字:ak47
武器伤害:10
武器名字:musket
武器伤害:5
思考总结
什么是简单工厂模式
简单工厂模式也是工厂模式的一种,但不属于23种设计模式。
目的使客户端与产品解耦。将产品创建实例过程从客户端代码中独立出去。生成具体对象的逻辑判断也从客户端分离至简单工厂类中。
简单工厂模式的最大优点在于工厂类中包含了必要的逻辑判断,根据客户端的选择条件动态实例化相关的类,对于客户端来说,去除了与具体产品的依赖。
但简单工厂违背了开放-封闭原则
,导致每次添加新类别的时候,都需要去修改工厂类的分支case判断,可以用反射动态生成实例解决。
简单工厂反射案例:我的设计模式之旅、01 策略模式、简单工厂、反射 - 小能日记
什么是工厂方法模式
工厂方法是一种创建型设计模式, 其在父类中提供一个创建对象的方法,允许子类决定实例化对象的类型。
工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到子类。
工厂方法类 (创建者类):主要职责并非是创建产品。其中通常会包含一些核心业务逻辑,这些逻辑依赖于由工厂方法返回的产品对象。子类可通过重写工厂方法并使其返回不同类型的产品来间接修改业务逻辑。
主要解决:主要解决接口选择的问题。客户端将所有产品视为抽象的接口,客户端知道所有产品都提供该接口要求的方法,并不关心其具体实现方式。
何时使用:
- 我们明确地计划不同条件下创建不同实例时。
- 当你在编写代码的过程中,如果无法预知对象确切类别及其依赖关系时,可使用工厂方法。
- 希望用户能扩展你软件库或框架的内部组件,可使用工厂方法。
- 希望复用现有对象来节省系统资源,而不是每次都重新创建对象,可使用工厂方法。在处理大型资源密集型对象(数据库连接、文件系统和网络资源)时,你会经常碰到这种资源需求。
- 在许多设计工作的初期都会使用工厂方法(较为简单,而且可以更方便地通过子类进行定制),随后演化为使用抽象工 厂、原型或生成器(更灵活但更加复杂)。
如何解决:让其子类实现工厂接口,返回的也是一个抽象的产品。使用特殊的工厂方法代替对于对象构造函数new的直接调用。
实现步骤:
- 让所有产品遵循同一接口。改接口必须声明对所有产品都有意义的方法。
- 在工厂方法类(创建类中)添加一个空的工厂方法。该方法的返回类型必须遵循通用的产品接口。
- 找到对于产品构造函数的所有引用。依次替换为对工厂方法的调用,并将创建产品的代码移入工厂方法。(类似简单工厂,此时switch分支是庞大的)
- 为每种产品编写创建者子类,在子类中重写工厂方法,将基本方法的相关创建代码移动到工厂方法中。子类过多时,可以给子类创建者传入参数,判断生成。
- 如果代码经过上述移动后,基础工厂方法中已经没有任何代码,你可以将其转变为抽象类。如果基础工厂方法中还有其他语句,你可以将其设置为该方法的默认行为。一般总是把创建者基类作为抽象类。
关键代码:创建过程在其子类执行。
应用实例:
- 您需要一辆汽车,可以直接从工厂里面提货,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现。
- Hibernate 换数据库只需换方言和驱动就可以。
优点:
- 一个调用者想创建一个对象,只要知道其名称就可以了。
- 开闭原则。扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
- 屏蔽产品的具体实现,调用者只关心产品的接口。避免创建者和具体产品之间的紧密耦合。
- 单一职责原则。你可以将产品创建代码放在程序的单一位置, 从而使得代码更容易维护。
缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
使用场景:
- 日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。
- 数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。
- 设计一个连接服务器的框架,需要三个协议,"POP3"、"IMAP"、"HTTP",可以把这三个作为产品类,共同实现一个接口。
注意事项:
-
作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。
-
创建对象的逻辑判断依旧在客户端中实现。
与其他模式的关系:
- 抽象工厂模式通常基于一组工厂方法,但你也可以使用原型模式来生成这些类的方法。
- 你可以同时使用工厂方法和迭代器来让子类集合返回不同类型的迭代器,并使得迭代器与集合相匹配。
- 工厂方法是模板方法的一种特殊形式。同时,工厂方法可以作为一个大型模板方法中的一个步骤。
思考对象复用的方法
-
首先,你需要创建存储空间来存放所有已经创建的对象。
-
当他人请求一个对象时,程序将在对象池中搜索可用对象。
-
然后将其返回给客户端代码。
-
如果没有可用对象,程序则创建一个新对象(并将其添加到对象池中)。
四个步骤的代码必须位于同一处,确保重复代码不会污染程序!
参考资料
- 《Go语言核心编程》李文塔
- 《Go语言高级编程》柴树彬、曹春辉
- 《大话设计模式》程杰
- 《深入设计模式》亚历山大·什韦茨
- 设计模式讲解和代码示例
- 菜鸟教程