工厂方法与抽象工厂模式

1,034

前言

  • 为什么选择该模式?
    • 实用,很多场景都可以看成“工厂生产产品”的“工厂类”模型,所以很多时候都能用到;
  • 本文概述
    • 本文大量参考了《Head First 设计模式》一书,以比萨的生产过程为例,从简单原始的代码开始,通过不断的添加需求,来引入工厂方法模式和抽象工厂模式解决需求问题,符合认知常识且容易理解。

常见代码的问题

  • 背景:美国某地新开了一家比萨店,包含芝士比萨和希腊风味比萨,接受线上预定

  • 我们就从预订 Pizza 开始吧

    public class PizzaStore{
      public Pizza orderPizza(String type){
        Pizza pizza;
        // 容易变化的地方
        if (type.equals("cheese"){
          pizza = new CheesePizza();
        } else if((type.equals("greek")){
          pizza = new GreekPizza();
        } else{
          pizza = new NormalPizza();
        }
        return pizza;
      }
    }
    
    • 用了实现,而不是接口;
    • 耦合严重,PizzaStore 依赖了所有的比萨种类,添加新产品或者修改产品时都需要修改创建的地方,违背开闭原则,即 orderPizza 无法对修改关闭。不利于维护;
    • 哪里出了问题?
      • 出问题的是“改变”,压力来自于增加更多 pizza 类型;
    • 如何修改?
      • "找出变化的方面,把它们从不变的部分分离出来。"

简单工长的出现

  • 封装创建对象的代码:将变化转移到另一个类中,该类只负责创建比萨,称这个新类为“工厂”;

    public class SimplePizzaFactory{
    
    	public Pizza createPizza(String type){
        Pizza pizza = null;
        if (type.equals("cheese"){
          pizza = new CheesePizza();
        } else if((type.equals("greek")){
          pizza = new GreekPizza();
        } else if(type.equals("clam")){
          pizza = new ClamPizza();
        } else{
          pizza = new NormalPizza();
        }
        pizza.bake();
      	pizza.cut();
        pizza.box();
        return pizza;
      }
    }
    
    • 如此一来,orderPizza 不用再关心具体的 Pizza 类型,只关心从工厂得到一个比萨;

      public class PizzaStore{
        
      	private SimplePizzaFactory factory;
        
      	public PizzaStore(SimplePizzaFactory factory){
          this.factory = factory;
        }
        
        public Pizza orderPizza(String type){
      		Pizza pizza = factory.createPizza(type);
      		return pizza;
      	}
      }
      
    • 而且任何想要创建比萨的地方,都可以使用该工厂创建,提高了复用性;

    • 以后需求改变时,只修改这个类即可;

    简单工厂并没有被纳入设计模式,更像是一种编程习惯。

工厂方法引入

  • 新需求:比萨广受好评,发展迅速,老板打算开启加盟模式,但考虑到每个地区的人们不同的口味,每家加盟店都可能需要提供不同风味的比萨,比如纽约、芝加哥、加州。

  • 方法一:利用现有的结构实现

    NYPizzaFactory nyFactory = new NYPizzaFactory ();
    PizzaStore nyStore = new PizzaStore(nyFactory);
    nyStore.orderPizza("Veggie");
    
    • 缺点:虽然都是加盟店,但是他们的工厂不同,制作比萨的过程(烘烤、切片、包装)就变得不可控;
  • 方法二:利用工厂方法实现

    • 给比萨店加入限制流程,对加盟店进行限制
     public abstract class PizzaStore{
       
       public Pizza orderPizza(String type){
         Pizza pizza;
         pizza = createPizza(type);
         pizza.bake();
         pizza.cut();
         pizza.box();
         return pizza;
       }
       
       protected abstract Pizza createPizza(String type);
     }
    
    • 制作流程从工厂转移到 PizzaStore 中,这样起到限制加盟店的作用;

    • 类改为 abstract,并加入了抽象方法 createPizza,让子类(NYPizzaStore、ChicagoPizzaStore)只生产具有特色风味的比萨,不再进行其他处理。

    • 生产对象由工厂转为抽象方法, 这也是工厂方法的由来;

    • 来看一下该方法下的预订过程:

      PizzaStore nyPizzaStore = new NYPizzaStore();
      nyPizzaStore.orderPizza("cheese");
      
  • 对比

    • 所有加盟店对于订单的处理(orderPizza)都能够达到一致;
    • 允许子类做决定:加盟的核心是允许加盟店做决定,而不是允许"工厂"做决定
    • 关注问题本质,用户预定时只关注店面和比萨口味,不关心工厂
    • 简单工厂把全部的事情在一个地方都处理完了,然而工厂方法却是创建一个框架,让子类决定要如何实现。

    核心问题还是“改变”,尽量只抽离容易改变的地方(制作比萨和扩展披萨种类),而统一控制不经常改变的地方(烘烤、切片、包装)。

  • 补充一件事:比萨本身

    • 上面一直提到,不同的加盟店可以创建不同风味的比萨,可见比萨本身也具有共性与差异,也是一种继承关系。基类和继承关系示例如下:
     public class Pizza{
     	String name;
     	float size;
     	float thickness;
     }
    

工厂方法定义

  • 定义:工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
  • 解释:
    • 创建对象的接口:抽象方法,如 createPizza
    • 由子类决定实例化哪一个类:编写创建者时,不需要知道实际创建的产品是哪一个,选择了哪个子类,自然就决定了创建的产品;
    • 创建者是一个类,他实现了所有操纵产品的方法,但不实现工厂方法;
  • UML
  • Activity 中的 onCreate 的方法其实就是工厂方法的使用

抽象工厂引入

  • 确保原料一致

    • 又有新需求:有一些加盟店使用低价原料来增加利润,需要采取手段,确保原料一致,以免损坏公司声誉。
    • 比较直观的方法是,建造一家生产原料的工厂,并将原料运送到各家加盟店。不过,虽然所有加盟店需要的原料种类一样,但每种具体的原料不同。比如,纽约的红酱料和芝加哥的红酱料是不一样的。而且,使用当地的原料,不仅免除运送费用,还更符合当地人的需求,所以更好的方法是,“总部”只定义一组原料,而让各个地区的加工厂生产每一种原料。
  • 建造原料工厂

     public interface PizzaIngredientFactory{
       public Dough createDough();// 面团
       public Sauce createSauce();// 酱
       public Clams createCheese();// 芝士
     }
    
    • 为每个区域建造一个工厂,需要创建一个继承自 PizzaIngredientFactory 的子类来实现每一个创建方法。

    • 最后将这一切组织起来,将新的原料工厂整合进旧的 PizzaStore 代码中。

    • 比如,创建纽约原料工厂

     public class NYPizzaIngredientFactory implements PizzaIngredientFactory{
     	public Dough createDough(){
     		return new ThinCrustDough();
     	}
       
     	public Sauce createSauce(){
     		return new MatrinaraSauce();
     	}
       ...
     }
    
    • 对于原料家族中的每一种原料,都提供了纽约的版本。
    • 其他区域类似,省略。
  • 重做比萨

    • 上面提到过 Pizza 类,这里我们给 Pizza 类加入些原料属性。
     public abstract class Pizza{
     	String name;
     	float size;
     	float thickness;;
       Dough dough;
       Sauce sauce;
       Cheese cheese;
       
       abstract void prepare();
     }
    
    • 接下来创建纽约风味的比萨,注意,从现在起,加盟店必须直接从工厂取得原料,就不会再有偷工减料的事情发生了。
     public class CheesePizza extends Pizza{
       PizzaIngredientFactory ingredientFactory;
       
       public CheesePizza(PizzaIngredientFactory ingredientFactory){
         this.ingredientFactory = ingredientFactory;
       }
       
       @over
       void prepare(){
         dough = ingredientFactory.createDough();
         sauce = ingredientFactory.createSauce();
         clam = ingredientFactory.createCheese();
     	}
     }
    
  • 总结一下我们做了什么

    • 引入了新类型的工厂,也就是所谓的抽象工厂,来创建原料家族
    • 通过抽象工厂所提供的接口,可以创建产品家族。各式各样的工厂可以创建出各种不同的产品。
    • 他们的关系如下

抽象工厂定义

  • 定义:抽象工厂模式提供了一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
  • 解释:
    • 和工厂方法类似,具体创建哪一种产品也由子类决定,达到了与具体产品解耦的目的
    • 创建的是一组产品(对象家族),而不是单个产品。对于这个产品家族,每个工厂都有不同的实现。
  • UML
    抽象工厂UML

工厂方法与抽象工厂对比

  • 工厂方法一般只生产一种产品,核心是定义抽象方法,通过子类创建具体产品,不同的子类生产不同的产品;抽象工厂是生产一组产品,每个工厂子类都需要生产一组产品,并且每一种产品在每个子类都有不同的具体类型;
  • 工厂方法潜伏在抽象工厂中。这很有道理,因为抽象工厂的任务是定义负责创建一组产品的接口,而这个接口的每个方法都负责创建一个具体产品,所以让工厂的子类实现这些方法。比如,回到比萨原料工厂,如果我们只看一种原料:面团,在工厂接口中定义了创建面团的方法 createDough(),子类实现该方法创建不同的面团,即“由子类决定要实例化的类是哪一个”,完全符合工厂方法的定义。
  • 具体表现:工厂方法只有一个抽象方法,抽象工厂拥有一个大的接口(或多个抽象方法);
  • 共同点:
    • 依赖倒置原则:都依赖抽象,而不依赖具体类;
    • 类爆炸:尤其是抽象工厂,会使类显著增多;
  • 适用场景:
    • 抽象工厂:当需要创建产品家族和想让制造的相关产品集合起来时适用。
    • 工厂方法:创建一种产品时,或者,可以把你的客户代码从需要实例化的具体类中解耦,再或者,你目前还不知道将来需要实例化哪些类,都可以使用。

工厂方法与模板方法对比

  • 父类定义抽象接口让子类去实现,看上去有点像模板方法的做法,其实二者还是有区别的:

    • 工厂方法的子类是生产不同的产品(ProductA,ProductB),这些产品是有共同父类的(Product)
    • 工厂方法的目的,是使调用者(Client)与具体产品解耦
  • 上述两点才显示出“工厂”的意义和作用,而模板方法重在父类封装某种逻辑的主要框架,让子类处理某些步骤。

  • 附:模板方法定义:定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

实战场景

  • 日常开发中,可以把一个页面的创建过程看成工厂生产产品的过程,很多页面都与状态有关,比如登录态与未登录态、已退课与退课中的状态等,这就等价于不同的产品,可以使用工厂方法模式,将不同状态下的页面延迟到子类初始化;
  • 如果再把页面分割,比如页头、页中、页脚,每个部分都与状态有关,这样就可以使用抽象工厂,定义创建页头、页中、页脚的接口,由子类决定具体创建什么状态下的具体实例;

个人感悟

  • 对象总是要创建的。我们要处理的永远是“改变”,对于不变的地方可以直接引用创建对象。"找出变化的方面,把它们从不变的部分分离出来。"
  • 设计模式是一种思维模式,不需要拘泥于具体框架和严格定义,指导思想都是面向对象的设计原则。

Article by Panxc