📅 2024年12月13日

漫画设计模式笔记

工厂模式分为三种

1.简单工厂

2.工厂方法

3.抽象工厂

1️⃣ 简单工厂模式

简单工厂模式将多个不同的类由一个类方法或者函数创建

比如一个推荐页面中有很多模块,有笔记本推荐、手机推荐、显示器推荐等等,每个推荐都是不一样的,用户在点击不同模块的时候需要推荐不同的商品,如果将这个代码封装到一个类中代码如下:

package NoFactory

import "fmt"

type ProductRecommend struct {
	Recommend []string
}

// 推荐方法
func (t *ProductRecommend) recommend(category string) {
	t.Recommend = make([]string, 0)
	switch category {
	case "tv":
		t.recommendTv()
	case "phone":
		t.recommendPhone()
	case "laptop":
		t.recommendLaptop()
	default:
		fmt.Println("没有此推荐")
	}
}


// 推荐不同的商品

func (t *ProductRecommend) recommendTv() {
	t.Recommend = append(t.Recommend, "TCL")
	t.Recommend = append(t.Recommend, "Haier")
	t.Recommend = append(t.Recommend, "HuaWei")
}

func (t *ProductRecommend) recommendPhone() {
	t.Recommend = append(t.Recommend, "HuaWei")
	t.Recommend = append(t.Recommend, "Xiaomi")
	t.Recommend = append(t.Recommend, "Vivo")
}

func (t *ProductRecommend) recommendLaptop() {
	t.Recommend = append(t.Recommend, "HuaWei")
	t.Recommend = append(t.Recommend, "Xiaomi")
	t.Recommend = append(t.Recommend, "MacBook")
}

func (t *ProductRecommend) show() {
	fmt.Println(t.Recommend)
}

以上代码有几个不好的点,第一个是代码太过于臃肿,而且不“单一”,这里的单一指的是单一原则的单一,我这个 ProductRecommend类,我又推荐笔记本又推荐手机的;二是如果我需要增加一个推荐书籍就需要修改这个类,就违反了开闭原则;

🐛 接下来一步一步优化

首先第一步将每个类拆开为单独的类,每个类实现单独的推荐,这样就解决问题了

将3个推荐拆分为3个不同的类,每个类都有一个推荐方法

// 手机推荐
type PhoneRecommend struct {
	RecommendList []string
}

func (t PhoneRecommend) recommend() {
	t.RecommendList = append(t.RecommendList, "XiaoMi")
	t.RecommendList = append(t.RecommendList, "HuaWei")
	t.RecommendList = append(t.RecommendList, "MeiZu")
}

ProductRecommendrecommend方法提出来变成一个函数(也可以不提出来)

搞到这里突然意识到,Go里面的设计模式可能与 Java的不太一致

func ProductRecommend(category string) (recommend []string) {
	switch category {
	case "phone":
		return LaptopRecommend{}.recommend()
	case "tv":
		return TvRecommend{}.recommend()
	case "laptop":
		return LaptopRecommend{}.recommend()
	default:
		return nil
	}
}

这样就满足了单一原则(每个类至提供一种品类的商品),开闭原则(如果要新添加一个推荐商品品种,那就只需要添加一个新类,并且在product函数中在添加即可)

但是注意到一个问题,在 ProductRecommend函数中返回的只是推荐的商品列表,这样无异于你去汉堡店买汉堡,你付了钱之后,店员只是将你买的汉堡名字给了你,并没有把 “汉堡” 给你,也就是说我这里是需要返回一个对象的

如果仔细查看以上代码就会发现,3个推荐类中都有 recommend方法,可以提取出来一个变成接口,演示代码如下:

// Recommend 推荐接口
type Recommend interface {
	recommend()
}

添加这个接口之后,由于go语言是隐式的实现,有 recommend方法的类就表示实现了 Recommend方法,那么在 ProductRecommend函数中只需要返回 Recommend即可代码如下

其实这样理解有点勉强,如果是在Java代码中就好理解多了

func RecommendFactory(category string) (recommend Recommend) {
	switch category {
	case "phone":
		return LaptopRecommend{}
	case "tv":
		return TvRecommend{}
	case "laptop":
		return LaptopRecommend{}
	default:
		return nil
	}
}

那么如果需要创建对象就只需要调用工厂方法即可创建,这样 ProductRecommend不需要直接依赖于三个推荐类,而是在他们中间加了一层抽象(接口),来间接依赖实现了依赖倒置

func ProductRecommend(category string) []string {
	recommend := RecommendFactory("tv")
	recommend.recommend()
}

简单工厂模式的优点: 解耦了对象的创建和使用,支持对于产品的扩展,当需要创建由同一接口实现的不同产品对象时,可以考虑使用简单工厂

简单工厂模式的缺点:工厂无法扩展,难以增加新的工厂,随着产品线的不断发展,工厂承担的职责会越来越多;

2️⃣工厂方法模式

工厂方法模式就是工厂在进行抽象,在上面代码中我的“工厂” 只使用了一个函数就实现了,`ProducerRecommend`是直接依赖于这个函数的,违法了依赖倒置原则的,所以后面就可以将工厂进行抽象变成一个接口,工厂类实现这个接口,就可以做到依赖抽象,而且你可以通过这个接口建筑多个不同的工厂,代码实现如下:

首先定义工厂接口

type Factory interface {
	CreateRecommend(category string) (recommend Recommend)
}

创建一个工厂类实现这个方法

type RecommendFactory struct{}

// 实现工厂接口
func (t *RecommendFactory) CreateRecommend(category string) (recommend Recommend) {
	switch category {
	case "phone":
		recommend := PhoneRecommend{}
		return
	case "tv":
		tvRecommend := TvRecommend{}
		return tvRecommend
	case "laptop":
		laptopRecommend := LaptopRecommend{}
		return laptopRecommend
	default:
		return nil
	}
}

这样就是工厂方法模式中的参数化工厂方法,首先引入工厂接口,然后工厂的实现类更具参数创建不同的产品对象。

与简单工厂相比参数化工厂方法只是添加了一个工厂类,就让 ProductRecommend摆脱了对 RecommendFactory的依赖,提升了工厂的扩展性

抽象出工厂的接口相当于打造了工厂的标准。依赖接口就相当于只认定标准,不与具体实现绑定。

⭐️ 多种工厂切换自如

现在又有一个需求,为了提高推荐效率,需要对不同产品添加不同的推荐引擎,现在市面上最火的是

baido搜索引擎,将所有商品都换成 baido推荐引擎

仔细想了一下,如果要添加推荐引擎的话,每个类就需要新添加一个字段,那么现在每个类都有相同的字段和相同的方法,在Java中有个叫做抽象类的方法,可以将抽象方法和字段都写进去,而Go没有抽象类,可以使用如下方法实现

// Recommend 推荐接口
type Recommend interface {
	recommend() []string
	setEngine(string)
}

type RecommendAbstract struct {
	Recommend
	RecommendEngine string
}

接下来推荐类也继承 RecommnedAbstract类即可,直接拿 PhoneRcommend举例子

type PhoneRecommend struct {
	RecommendAbstract
}

func (t *PhoneRecommend) setEngine(engine string) {
	t.RecommendEngine = engine
}

func (t *PhoneRecommend) recommend() []string {
	recommendList := make([]string, 0)
	recommendList = append(recommendList, "XiaoMi")
	recommendList = append(recommendList, "HuaWei")
	recommendList = append(recommendList, "MeiZu")
	return recommendList
}

修改工厂类为 RecommendBaidoFactory

type RecommendBaidoFactory struct{}

func (t *RecommendBaidoFactory) CreateRecommend(category string) (recommend Recommend) {
	switch category {
	case "phone":
		recommend = &PhoneRecommend{}
		recommend.setEngine("baidu")
		return
	case "tv":
		tvRecommend := &TvRecommend{}
		tvRecommend.setEngine("baidu")
		return tvRecommend
	case "laptop":
		laptopRecommend := LaptopRecommend{}
		laptopRecommend.setEngine("bing")
		return &laptopRecommend
	default:
		return nil
	}
}

那如果要添加一个 binq推荐引擎呢,那就在添加一个工厂就好啦!如下

type RecommendBinqFactory struct{}

func (t *RecommendBinqFactory) CreateRecommend(category string) (recommend Recommend) {
	switch category {
	case "phone":
		recommend = &PhoneRecommend{}
		recommend.setEngine("binq")
		return
	case "tv":
		tvRecommend := &TvRecommend{}
		tvRecommend.setEngine("binq")
		return tvRecommend
	case "laptop":
		laptopRecommend := LaptopRecommend{}
		laptopRecommend.setEngine("binq")
		return &laptopRecommend
	default:
		return nil
	}
}

如果创建产品的逻辑存在变化的肯,导致工厂类的实现可能被替换掉,那么可以考虑使用工厂方法模式

但是如何很难预测未来的变化,建议先使用简单工厂模式,毕竟将简单工厂模式重构为工厂方法模式也不难

⭐️ 需求膨胀,工厂也膨胀

如果我想要在不同场景使用不同的推荐引擎怎么办呢,比如我手机和笔记本使用 Baido,电视机使用 binq呢?是不是可以使用工厂接口呢?直接开干

将两个工厂推荐修改

type RecommendBaidoFactory struct{}

func (t *RecommendBaidoFactory) CreateRecommend(category string) (recommend Recommend) {
	switch category {
	case "phone":
		recommend = &PhoneRecommend{}
		recommend.setEngine("baidu")
		return
	case "laptop":
		laptopRecommend := LaptopRecommend{}
		laptopRecommend.setEngine("bing")
		return &laptopRecommend
	default:
		return nil
	}
}


// 只推荐tv
type RecommendBinqFactory struct{}

func (t *RecommendBinqFactory) CreateRecommend(category string) (recommend Recommend) {
	switch category {
	case "tv":
		tvRecommend := &TvRecommend{}
		tvRecommend.setEngine("binq")
		return tvRecommend
	default:
		return nil
	}
}

现在创建 RecommendFactorySelector 类用于根据不同板块来选择不同引擎的创建工厂

func (T *RecommendFactorySelector) createFactory(category string) (factory Factory) {
	switch category {
	case "tv":
		return &RecommendBinqFactory{}
	case "phone":
	case "laptop":
		return &RecommendBaidoFactory{}
	default:
		return nil
	}
    return nil
}

productrecommed就可以这样调用

func ProductRecommend(category string) {
	recommendFactorySelector := RecommendFactorySelector{}
	recommendFactory := recommendFactorySelector.createFactory(category)
	recommend := recommendFactory.CreateRecommend("phone")
	fmt.Println(recommend.recommend())
}

这种就很像是工厂的工厂

工厂方法模式有如下优点:

1.可以扩展或者方便的替换工厂

2.工厂职责比较单一

适用于场景:

  1. 创建产品逻辑复杂,并且产品创建逻辑可能变化,使用工厂方法模式,可以将复杂的工厂拆分成多个相对简单的工厂,降低工厂的复杂度
  2. 场景中存在多个平行的类层次

3️⃣ 抽象工厂模式

从这里开始使用 java代码

拿便利店的关东煮举例子:有时候如果突然觉得关东煮比之前吃的好吃,有可能是店家换关东煮的供应商了;在市面上品类差不多的供应商有很多,而且每一家的种类都很齐全;接下来我们来理解一下为什么店家换供应商这么容易吧:
将关东煮供应商看作是一个关东煮的工厂,它可以生产多种多样的关东煮,而不局限于某一种品类,在市面上不同的供应商都可以生产一样的产品,无论店家对接哪个供应商都可以获得齐全的关东煮品类,只要店家有供应商电话号码,打个电话订货,供应商送上门来即可,

如果需要换供应商也只需要打其他供应商电话即可

此时,店家手中的电话号码就可以看作对供应商的引用。使用抽象工厂模式,仅仅需要换一家工厂,就可以更换全系列产品,这就是抽象工厂模式的优点,除了增加一个新的供应商,便利店不需要做出任何改动,这就叫做,对扩展开发,对修改关闭

⭐️商品详情页的程序的实现

购物商场详情页中现有两个显示组件分别是商品图片、商品描述,该如何实现了?
  1. 创建两个类分别是 ImageText
  2. Image类字段为图片地址,Text字段为商品详情
  3. 都实现一个show方法用作展示
这样简简单单第一版就完成了

⭐️ 一键切换不同主题的组件

现在来了个新的需求,用户晚上看觉得太刺眼了,要求增加一个黑色主题
  1. ImageText新增主题字段,并且新增有参构建函数,参数为主题

    public class Text {
        // 添加主题字段
        public String Theme;
    
        public Text(String theme) {
            this.Theme = theme;
        }
    }
    
  2. 给这两个组件分别创建两个主题类分别是 LightenXXXDarkXXX,拿Text举列子:LightenText/DarkText继承相应组件的类,并实现构造方法

    public class DarkText extends Text {
    	public DarkText(){
            super("dark");
        }
    }
    
    public class LightenText extends Text {
    	public DarkText(){
            super("lighten");
        }
    }
    
  3. 这样就可以来去自如的创建不同主题了

    // 创建white主题
    text = new LightenText
    image = new LightenImage
    
    // 创建dark主题
    text = new DarkText
    image = new DarkImage
    

    但是这样也会有个问题那就是如果你的组件多起来,比如有10个组件,那岂不是得new个20下?这样成本太高了,而且复杂难于维护,接下来就可以利用工厂了,可以将Text和Image组件想象成关东煮里面的福袋和萝卜,他们都可以由同一个工厂来生产,那么现在就有俩家工厂一个是Dark一个是Lighten

  4. 创建工厂接口,接口方法为创建Text和创建Image

    public interface ThemeFactory {
         Text creteText();
         Image creteImage();
    }
    
  5. 创建两个工厂类,实现工厂接口

    // 暗黑工厂
    public class DarkThemeCreate  implements ThemeFactory{
    
        public Text creteText() {
           return new DarkText("dark");
        }
    
        public Image creteImage() {
            return new DarkImage("dark");
        }
    }
    
    // 白色工厂
    public class LightThemeCreate implements ThemeFactory{
    
        public Text creteText() {
           return new DarkText("light");
        }
    
        public Image creteImage() {
            return new DarkImage("light");
        }
    }
    
  6. 那么现在只需要修改一行代码就可以实现主题的切换

    // 白色主题
    ThemeFactory lightTheme = new LightThemeCreate();
    
    // 黑色主题
    ThemeFactory darkTheme = new DarkThemeCreate();
    

如下就是结构图,可以看到这里对工厂以及工厂的产品都做了依赖,在最后创建主题的时候也不会依赖具体的主题组件的依赖

image-20241218220246154

🍪 抽象工厂的好处:

  1. 方便切换主题,客户端只需要修改一行代码就可以切换到另外一个主题
  2. 保证产品的一致性。抽线工厂模式对产品的使用进行约束,保证客户端使用的组件都是来自同一个工厂,都是同一个工厂了看到就是同一个主题

🐛 缺点:

  1. 如果需要增加一个商品评价组件,那么就要为每一个主题工厂添加一个评价组件,如果有100个主题那是不是要添加100个? 所以说要获得主题的跨找以及切换主题的灵活性时就适合使用抽象工厂模式,如果想要组件越多那可能就不太适合使用抽线工厂模式了

产品有两个维度的分析,一个是产品的分类,一个是产品的主题,这是两个纵横交错的维度。抽象工厂模式支持对产品主题的扩展,但是不能解决新增产品雷达问题,甚至还会造成大量工厂类的修改

4️⃣ 三种工厂模式的比较

1.简单工厂模式只对某一类产品做接口抽象,并没有对工厂进行抽象,工厂根据不同参数来创建同一类产品的不同实现

比如一家椅子工厂,它可以生产电竞椅、休息椅、办公椅等等

2.工厂方法模式对工厂和产品都抽象了,使得可以有多个不同的工厂创建对同一类型的不同产品实现

现在有多个椅子工厂,但是每个工厂只生产一种类型的一致,比如电竞椅工厂就只生成电竞椅,办公椅工厂只生产办公椅

3.抽象工厂模式,对几类的产品分别抽象,在抽象工厂中都有了每个类品的生产行为

家具厂分为欧式家具厂和中式家具厂

⭕️ 三行真言

简单工厂,同类全套一家全

工厂方法,职责精专不慌乱

抽象工厂,风格一致多类产