jjzjj

我的设计模式之旅、10 抽象工厂

小能的博客 CanAngle's Blog 2023-03-28 原文

一个菜鸟的设计模式之旅,文章可能会有不对的地方,恳请大佬指出错误。

编程旅途是漫长遥远的,在不同时刻有不同的感悟,本文会一直更新下去。

程序介绍

你想要购买一组运动装备, 比如一双鞋与一件衬衫这样由两种不同产品组合而成的套装。 相信你会想去购买同一品牌的商品, 这样商品之间能够互相搭配起来。如果把这样的行为转换成代码的话, 帮助创建此类产品组的工具就是抽象工厂,便于产品之间能够相互匹配。

案例中 Shoe、Shirt 是两个抽象产品,具体产品NikeShirt、NikeShoe、AdidasShirt、AdidasShoe 是对两个抽象产品的具体分类的实现。ISportsFactory\IAbstractFactory 是抽象工厂接口,里面包含所有产品创建的抽象方法。客户端用的都是抽象产品接口,没有出现任何一个具体产品了字样,达到了解耦的目的!

最大的好处是易于交换产品系列,只需要修改具体的工厂实例!

阿迪达斯工厂正在生产衬衫
阿迪达斯工厂正在生产衬衫
阿迪达斯工厂正在生产鞋子
耐克工厂正在生产衬衫
耐克工厂正在生产衬衫
耐克工厂正在生产鞋子
衬衫 品牌 阿迪达斯 大小 14	
衬衫 品牌 阿迪达斯 大小 14
衬衫 品牌 耐克 大小 12
衬衫 品牌 耐克 大小 12
鞋子 品牌 阿迪达斯 大小 10
鞋子 品牌 耐克 大小 12

程序代码、抽象工厂模式 Golang

iShirt.go 产品接口类

package main

type IShirt interface {
	setLogo(logo string)
	setSize(size int)
	getLogo() string
	getSize() int
}

type Shirt struct {
	logo string
	size int
}

func (s *Shirt) setLogo(logo string) {
	s.logo = logo
}

func (s *Shirt) getLogo() string {
	return s.logo
}

func (s *Shirt) setSize(size int) {
	s.size = size
}

func (s *Shirt) getSize() int {
	return s.size
}

iShoe.go 产品接口类

package main

type IShoe interface {
	setLogo(logo string)
	setSize(size int)
	getLogo() string
	getSize() int
}

type Shoe struct {
	logo string
	size int
}

func (s *Shoe) setLogo(logo string) {
	s.logo = logo
}

func (s *Shoe) getLogo() string {
	return s.logo
}

func (s *Shoe) setSize(size int) {
	s.size = size
}

func (s *Shoe) getSize() int {
	return s.size
}

adidas.go 阿迪达斯工厂

package main

type Adidas struct {
}

type AdidasShirt struct {
	Shirt
}

type AdidasShoe struct {
	Shoe
}

func (a *Adidas) makeShoe() IShoe {
	return &AdidasShoe{
		Shoe: Shoe{
			logo: "adidas",
			size: 14,
		},
	}
}

func (a *Adidas) makeShirt() IShirt {
	return &AdidasShirt{
		Shirt: Shirt{
			logo: "adidas",
			size: 14,
		},
	}
}

nike.go 耐克生产工厂

package main

type NikeShirt struct {
	Shirt
}

type NikeShoe struct {
	Shoe
}

type Nike struct {
}

func (n *Nike) makeShoe() IShoe {
	return &NikeShoe{
		Shoe: Shoe{
			logo: "nike",
			size: 14,
		},
	}
}

func (n *Nike) makeShirt() IShirt {
	return &NikeShirt{
		Shirt: Shirt{
			logo: "nike",
			size: 14,
		},
	}
}

iSportsFactory.go 抽象工厂接口、简单工厂

package main

import "fmt"

// 这个是抽象工厂接口
type ISportsFactory interface {
	makeShoe() IShoe
	makeShirt() IShirt
}

// 这里是简单工厂,根据brand生成特定工厂实例
func GetSportsFactory(brand string) (ISportsFactory, error) {
	if brand == "adidas" {
		return &Adidas{}, nil
	}

	if brand == "nike" {
		return &Nike{}, nil
	}

	return nil, fmt.Errorf("wrong brand type passed")
}

main.go

package main

import "fmt"

func main() {
	adidasFactory, _ := GetSportsFactory("adidas")
	nikeFactory, _ := GetSportsFactory("nike")

	nikeShoe := nikeFactory.makeShoe()
	nikeShirt := nikeFactory.makeShirt()

	adidasShoe := adidasFactory.makeShoe()
	adidasShirt := adidasFactory.makeShirt()

	printShoeDetails(nikeShoe)
	printShirtDetails(nikeShirt)

	printShoeDetails(adidasShoe)
	printShirtDetails(adidasShirt)
}

func printShoeDetails(s IShoe) {
	fmt.Printf("Logo: %s", s.getLogo())
	fmt.Println()
	fmt.Printf("Size: %d", s.getSize())
	fmt.Println()
}

func printShirtDetails(s IShirt) {
	fmt.Printf("Logo: %s", s.getLogo())
	fmt.Println()
	fmt.Printf("Size: %d", s.getSize())
	fmt.Println()
}

Console 输出

Logo: nike
Size: 14
Logo: nike
Size: 14
Logo: adidas
Size: 14
Logo: adidas
Size: 14

程序代码、抽象工厂模式 C#

Shirt.cs 产品类

namespace 抽象工厂;

public abstract class Shirt
{
    public string Logo { get; private set; }
    public int Size { get; private set; }

    protected Shirt(string logo, int size)
    {
        Logo = logo;
        Size = size;
    }
}

public class AdidasShirt : Shirt
{
    public AdidasShirt(int size) : base("阿迪达斯", size)
    {
    }
}

public class NikeShirt : Shirt
{
    public NikeShirt(int size) : base("耐克", size)
    {
    }
}

Shoe.cs 产品类

namespace 抽象工厂;

public abstract class Shoe
{
    public string Logo { get; private set; }
    public int Size { get; private set; }

    protected Shoe(string logo, int size)
    {
        Logo = logo;
        Size = size;
    }
}

public class AdidasShoe : Shoe
{
    public AdidasShoe(int size) : base("阿迪达斯", size)
    {
    }
}

public class NikeShoe : Shoe
{
    public NikeShoe(int size) : base("耐克", size)
    {
    }
}

AbstractFactory.cs 抽象工厂类

namespace 抽象工厂;

public interface IAbstractFactory
{
    Shoe createShoe(int size);
    Shirt createShirt(int size);
}

public class AdidasFactory : IAbstractFactory
{
    public Shoe createShoe(int size)
    {
        Console.WriteLine("阿迪达斯工厂正在生产鞋子");
        return new AdidasShoe(size);
    }

    public Shirt createShirt(int size)
    {
        Console.WriteLine("阿迪达斯工厂正在生产衬衫");
        return new AdidasShirt(size);
    }
}
public class NikeFactory : IAbstractFactory
{
    public Shoe createShoe(int size)
    {
        Console.WriteLine("耐克工厂正在生产鞋子");
        return new NikeShoe(size);
    }

    public Shirt createShirt(int size)
    {
        Console.WriteLine("耐克工厂正在生产衬衫");
        return new NikeShirt(size);
    }
}

// 一个类用于存放工厂单例对象,使用饿汉式单例类
public sealed class AbstractFactorySingleton
{
    public static readonly IAbstractFactory Adidas = new AdidasFactory();
    public static readonly IAbstractFactory Nike = new NikeFactory();
    
    // 常规写法应该提供一个全局访问点,这里为了方便省略
}

Program.cs

// See https://aka.ms/new-console-template for more information

using 抽象工厂;

// 产品的库存
List<Shoe> shoes = new();
List<Shirt> shirts = new();

IAbstractFactory factory = AbstractFactorySingleton.Adidas;
Shirt s1 = factory.createShirt(14);
Shirt s2 = factory.createShirt(14);
shirts.Add(s1);
shirts.Add(s2);
Shoe s3 = factory.createShoe(10);
shoes.Add(s3);

factory = AbstractFactorySingleton.Nike;
s1 = factory.createShirt(12);
s2 = factory.createShirt(12);
shirts.Add(s1);
shirts.Add(s2);
s3 = factory.createShoe(12);
shoes.Add(s3);

foreach (var shirt in shirts)
{
    Console.WriteLine($"衬衫 品牌 {shirt.Logo} 大小 {shirt.Size}");
}

foreach (var shoe in shoes)
{
    Console.WriteLine($"鞋子 品牌 {shoe.Logo} 大小 {shoe.Size}");
}

Console 输出

阿迪达斯工厂正在生产衬衫
阿迪达斯工厂正在生产衬衫
阿迪达斯工厂正在生产鞋子
耐克工厂正在生产衬衫
耐克工厂正在生产衬衫
耐克工厂正在生产鞋子
衬衫 品牌 阿迪达斯 大小 14
衬衫 品牌 阿迪达斯 大小 14
衬衫 品牌 耐克 大小 12
衬衫 品牌 耐克 大小 12
鞋子 品牌 阿迪达斯 大小 10
鞋子 品牌 耐克 大小 12

思考总结

注:把不同的产品变体、产品族理解为不同的产品系列,如现代风格家具类、北约风格家具类。

什么是抽象工厂模式

抽象工厂模式:创建型设计模式。提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

何时使用:系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。

如何解决:

  • 为系列中的每件产品明确声明接口,确保所有产品变体都继承这些接口。
  • 我们需要声明抽象工厂——包含系列中所有产品构造方法的接口。
  • 这些方法必须返回抽象产品类型。每个工厂类都只能返回特定类别的产品。

关键代码:在一个工厂里聚合多个同类产品。

应用场景:

  • 代码需要与多个不同系列的相关产品交互,但是由于无法提前获取相关信息,或出于对未来扩展性的考虑。
  • 如果你有一个基于一组抽象方法的类,且其主要功能因此变 得不明确,那么在这种情况下可以考虑使用抽象工厂模式。
  • 设计良好的程序中,每个类仅负责一件事。如果一个类与多种类型产品交互,就可以考虑将工厂方法抽取到独立的工厂类或具备完整功能的抽象工厂类

实现步骤:

  1. 以不同的产品类型与产品变体为维度绘制矩阵。
  2. 为所有产品声明抽象产品接口。然后让所有具体产品类实现这些接口。
  3. 声明抽象工厂接口,并且在接口中为所有抽象产品提供一组构建方法。
  4. 每种产品变体实现一个具体工厂类
  5. 在应用程序中开发初始化代码。该代码根据应用程序配置或当前环境,对特定具体工厂类进行初始化。然后将该工厂对 象传递给所有需要创建产品的类。
  6. 找出代码中所有对产品构造函数的直接调用,将其替换为对工厂对象中相应构建方法的调用。

优点:

  • 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
  • 确保同一工厂生成的产品相互匹配
  • 避免客户端和具体产品代码的耦合
  • 单一职责原则。你可以将产品生成代码抽取到同一位置,使得代码易于维护。
  • 开闭原则。向应用程序中引入新产品变体时,你无需修改客户端代码。

缺点:

  • 产品族扩展非常困难,要增加一个系列的某一产品,既要在接口里加代码,又要在具体类里面加代码。

使用场景:

  • QQ 换皮肤,一整套一起换。
  • 生成不同操作系统的程序。
  • 跨平台UI元素

注意事项:

  • 产品系列难扩展,具体产品易扩展。
  • 一般情况下,应用程序会在初始化阶段创建具体工厂对象。而在此之前,应用程序必须根据配置文件或环境设定选择工厂类别。

与其他模式的关系:

  • 在许多设计工作的初期都会使用工厂方法(较为简单,而且可以更方便地通过子类进行定制),随后演化为使用抽象工 厂、原型或生成器(更灵活但更加复杂)。
  • 生成器(建造者模式)重点关注如何分步生成复杂对象。抽象工厂专门用于生产一系列相关对象。抽象工厂会马上返回产品,生成器则允许你在获取产品前执行一些额外构造步骤。
  • 抽象工厂模式通常基于一组工厂方法,但你也可以使用原型模式来生成这些类的方法。
  • 当只需对客户端代码隐藏子系统创建对象的方式时,你可以 使用抽象工厂来代替外观。
  • 你可以将抽象工厂和桥接搭配使用。如果由桥接定义的抽象只能与特定实现合作,这一模式搭配就非常有用。在这种情 况下,抽象工厂可以对这些关系进行封装,并且对客户端代 码隐藏其复杂性。
  • 抽象工厂、生成器和原型都可以用单例来实现。

饿汉式单例模式

在c#的例子中使用了饿汉式单例模式来生成各个工厂实例,这是C#与公告语言运行库提供的一种静态初始化方法,这种方法不需要开发人员显式地编写线程安全代码,即可解决多线程环境下它是不安全的问题。这种静态初始化的方式是在自己被加载时就将自己实例化,所以被称为懒汉式单例类。

更多参考:我的设计模式之旅、02 单例模式 - 小能日记 - 博客园

参考资料

  • 《Go语言核心编程》李文塔
  • 《Go语言高级编程》柴树彬、曹春辉
  • 《大话设计模式》程杰
  • 《深入设计模式》亚历山大·什韦茨
  • 菜鸟教程

有关我的设计模式之旅、10 抽象工厂的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby-on-rails - Railstutorial : db:populate vs. 工厂女孩 - 2

    在railstutorial中,作者为什么选择使用这个(代码list10.25):http://ruby.railstutorial.org/chapters/updating-showing-and-deleting-usersnamespace:dbdodesc"Filldatabasewithsampledata"task:populate=>:environmentdoRake::Task['db:reset'].invokeUser.create!(:name=>"ExampleUser",:email=>"example@railstutorial.org",:passwo

  3. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  4. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  5. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

    我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

  6. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

    我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

  7. ruby - 我可以将我的 README.textile 以正确的格式放入我的 RDoc 中吗? - 2

    我喜欢使用Textile或Markdown为我的项目编写自述文件,但是当我生成RDoc时,自述文件被解释为RDoc并且看起来非常糟糕。有没有办法让RDoc通过RedCloth或BlueCloth而不是它自己的格式化程序运行文件?它可以配置为自动检测文件后缀的格式吗?(例如README.textile通过RedCloth运行,但README.mdown通过BlueCloth运行) 最佳答案 使用YARD直接代替RDoc将允许您包含Textile或Markdown文件,只要它们的文件后缀是合理的。我经常使用类似于以下Rake任务的东西:

  8. jquery - 我的 jquery AJAX POST 请求无需发送 Authenticity Token (Rails) - 2

    rails中是否有任何规定允许站点的所有AJAXPOST请求在没有authenticity_token的情况下通过?我有一个调用Controller方法的JqueryPOSTajax调用,但我没有在其中放置任何真实性代码,但调用成功。我的ApplicationController确实有'request_forgery_protection'并且我已经改变了config.action_controller.consider_all_requests_local在我的environments/development.rb中为false我还搜索了我的代码以确保我没有重载ajaxSend来发送

  9. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  10. java - 我的模型类或其他类中应该有逻辑吗 - 2

    我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

随机推荐