创建者在工厂模式中的作用

3
我是一个有用的助手,可以翻译文本。
我不明白为工厂类定义抽象类/接口的作用,这是我在网上所有教程中都看到的。请问有人能够解释一下CreatorInterface的重要性吗?{{link1:工厂模式的参考UML图表}}
为了以代码形式呈现,这是我的代码示例1:
// Product
public abstract class Vehicle
{
     public string VehicleType { get; set; }
}

// Concrete Product
public class Bike : Vehicle
{
    public Bike()
    {
        VehicleType = "Two Wheeler";
    }
}

// Concrete Product
public class Car : Vehicle
{
    public Car()
    {
        VehicleType = "Four Wheeler";
    }
}

// Concrete Factory
public class VehicleFactory
{
     public Vehicle GetVehicle(string VehicleType)
    {
        if (VehicleType == "Bike")
            return new Bike();
        else if (VehicleType == "Car")
            return new Car();
        else
            return null;
    }
}

// Client class
public class ClientClass
{
    public void Main()
    {
        VehicleFactory VehicleFactoryObj = new VehicleFactory();
        Vehicle BikeObj = VehicleFactoryObj.GetVehicle("Bike");
        Vehicle CarObj = VehicleFactoryObj.GetVehicle("Car");
    }
}

上述代码中没有包含任何“VehicleFactory”类的抽象类,但它可以正常工作。现在,为“VehicleFactory”添加抽象类的原因是什么?在我看来,为抽象工厂模式添加一个抽象类是有意义的。[如果我错了,请纠正我]
更新:迄今为止,我对工厂模式的理解。
GoF的定义:
定义一个用于创建对象的接口,但让子类决定要实例化哪个类。工厂方法允许一个类推迟实例化它使用的子类。
就我所理解的而言,这种模式背后的核心问题陈述是,您希望创建不同类的实例,而不会将创建逻辑暴露给消费者。请告诉我是否在这里有任何错误。由于我在网络上看到的示例,我也有点困惑。例如,在Wiki上的PHP和C#示例中,我可以理解C#示例中的模式要求,但无法理解PHP示例中的要求。无论如何,下面的语句将帮助您清楚地了解我的问题。
例如,在我们的库中有两个车辆类Bike和Car,它们都有车型号。自行车型号以"BK"开头,汽车型号以"CR"开头。现在,我们希望根据车辆型号返回任一类的实例,而不向客户端公开逻辑。[注意 这是一个更新的场景,因为早期的场景决定类的逻辑较弱,导致对字符串的使用产生困惑]
因此,我们可以创建一个车辆工厂类,它公开一个静态方法,该方法返回适当的车辆实例。
如果选择逻辑要为客户端所知,则我可能不需要模式本身。所以,代码看起来像这样:
// Product
public abstract class Vehicle
{
     public int NumberOfWheels { get; set; }
}

// Concrete Product
public class Bike : Vehicle
{
    public Bike()
    {
        NumberOfWheels = 2;
    }
}

// Concrete Product
public class Car : Vehicle
{
    public Car()
    {
        NumberOfWheels = 4;
    }
}

// Client class
public class ClientClass
{
    public void Main()
    {
        String ModelNumber = "BK-125";

        Vehicle CurrentVehicle;
        if (ModelNumber.Contains("BK"))
        {
            CurrentVehicle = new Bike();
        }
        else if(ModelNumber.Contains("CR"))
        {
            CurrentVehicle = new Car();
        }
    }
}

工厂模式让我可以通过创建一个工厂简单地隐藏客户端的创造逻辑。因此,现在客户端只需要调用工厂的创建方法,就能得到相应的类实例。现在代码看起来像这样:
// Product
public abstract class Vehicle
{
     public int NumberOfWheels { get; set; }
}

// Concrete Product
public class Bike : Vehicle
{
    public Bike()
    {
        NumberOfWheels = 2;
    }
}

// Concrete Product
public class Car : Vehicle
{
    public Car()
    {
        NumberOfWheels = 4;
    }
}

// Concrete Factory
public class VehicleFactory
{
     public Vehicle GetVehicle(string ModelNumber)
    {
        if (ModelNumber.Contains("BK"))
            return new Bike();
        else if (ModelNumber.Contains("CR"))
            return new Car();
        else
            return null;
    }
}

// Client class
public class ClientClass
{
    public void Main()
    {
        VehicleFactory VehicleFactoryObj = new VehicleFactory();
        Vehicle BikeObj = VehicleFactoryObj.GetVehicle("BK-125");
        Vehicle CarObj = VehicleFactoryObj.GetVehicle("CR-394");
    }
}

现在问题关于抽象工厂类 从讨论中我理解到添加抽象工厂类的一个好处是客户端将能够重写“GetVehicle”方法来覆盖逻辑。例如,他可能创建了更多的车辆类,比如“卡车”。但即使在这种情况下,如果他想要覆盖所有三个车辆类型(自行车、汽车和卡车)的工厂方法,他将没有整个逻辑,因为自行车和汽车的创建逻辑已经写在工厂方法中。尽管他将能够为他的所有新车辆类型创建新的逻辑。能否有人对此进行解释?
我想要提出的另一个观点是:这个问题涉及工厂模式,我确实理解抽象工厂模式需要一个抽象工厂,因为在抽象工厂模式中我们正在创建工厂的工厂。但在工厂模式中,我们只有一个对象工厂,那么为什么我们需要一个工厂的接口?
提前感谢! :-)

顺便提一下,标准语法是将方法参数小写。这样通常会导致更少的混淆。所以应该是 public Vehicle GetVehicle(string vehicleType) - Yair Halberstadt
4个回答

1
是的,它可以工作,但并不理想。VehicleFactory有一个非常通用的方法,一旦添加更多车辆,使用起来会很麻烦,因为您需要编写一个极长的方法来检查所有字符串。
想象一下您有15辆车。然后,您需要一个极长的方法来枚举所有选项并生成正确的汽车。这既不必要,而且容易出错,因为您可能会轻易地遗漏/删除某些内容,并且很难进行调试。通常,长方法是坏代码味道。
此外,这意味着每次添加从Vehicle继承的内容时,都需要编辑VehicleFactory类。但是,如果库的用户没有访问权限但想要从vehicle继承,会发生什么?但是,通过定义抽象VehicleFactory类,他可以继承该类并定义自己的工厂方法。
简而言之,抽象工厂方法使您的代码更易于扩展。
此外,根据字符串生成车辆是一个非常糟糕的想法;如果您使用大写字母或拼错了怎么办?此外,速度相当慢。最好像这样做。
public abstract class VehicleFactory
{
     public abstract Vehicle GetVehicle(string VehicleType)
}

public class CarFactory : VehicleFactory
{
    public override Vehicle GetVehicle(string VehicleType)
    {
          return new Car();
    }
}

public class BikeFactory : VehicleFactory
{
    public override Vehicle GetVehicle(string VehicleType)
    {
          return new Bike();
    }
}

public class ClientClass
{
    public void Main()
    {
        //Create Factories
        BikeFactory BikeFactoryObj = new BikeFactory();
        CarFactory CarFactoryObj = new CarFactory();

        //create Vehicles from factories. If wanted they can be casted to the inherited type.
        Vehicle VehicleObj=BikeFactoryObj.GetNewVehicle();
        Bike BikeObj = (Bike)BikeFactoryObj.GetVehicle();
        Car CarObj = (Car)CarFactoryObj.GetVehicle();

        //They are all inherited from Vehicle so can be used in a list of Vehicles
        List<Vehicle> Vehicles=new List<Vehicle>()
        {
             VehicleObj,
             BikeObj,
             CarObj
        }
    }
}

这里的错误机会要少得多,任何使用该类的用户都可以轻松扩展。

我承认我应该添加一个枚举,而不是传递字符串来表示VehicleType。但是,我无法理解你的陈述“VehicleFactory有一个非常通用的方法,并且一旦添加更多车辆,使用起来会非常麻烦,因为你需要一个非常长的方法”。能否请您详细说明一下? - Praveen Rai
1
你的方法存在问题,即使按照你提出的使用枚举的方式,对于未来想要处理的每个新情况,你都需要更改通用类(工厂)的源代码,在某些实际场景中可能没有这个选项。抽象/具体区分的想法是允许在一个地方定义接口(抽象部分),消费者可以与之交互,知道可以期望什么,然后在其他部分(甚至是不同的程序集,如果需要的话)中有许多不同版本的它(具体部分)。 - slashCoder
@PraveenRai。1)是的。他将无法搞乱您的任何逻辑,只能为自己编写的类创建逻辑。但他不能摆脱您的代码或影响任何其他车辆类。2)通过拥有抽象工厂,您不需要在抽象类中编写任何代码,而是每个车辆都有自己的工厂和自己的短工厂方法。因此,您不再需要在VehicleFactory类中拥有一个庞大的方法,这个方法很难阅读或调试,而是在15个不同的类中分别拥有15个单独且不相关的方法-在BikeFactory中有一个,在CarFactory中有一个等等。 - Yair Halberstadt
关于你所说的第二点,如果我们定义单独的工厂类,那么选择适当的工厂的逻辑将放在哪里?如果可能的话,你能否编辑你的答案,包括两种车辆类型的实现,例如汽车和自行车?如果你能直接编辑我的代码,那就更好了。 - Praveen Rai
@KedarTokekar 这正是我的问题。为什么我应该选择工厂方法而不是您在答案中描述的静态工厂方法?您能否详细说明一下工厂方法与静态方法的区别? - Praveen Rai
显示剩余5条评论

0
简单来说,你的基类应该是抽象的还是非抽象的,这取决于你是否想要声明一个新的基类。
如果你想要防止开发者(或者你自己)做以下操作:
var myVehicle = new Vehicle();

相反的,应该强制所有的车辆都应该是从Vehicle继承而来的子类,然后将其声明为抽象类。

这样当他们尝试进行上述代码时,会得到一个编译错误,因为他们无法声明一个新的抽象类。

如果你愿意让用户能够声明新的基类,那么将基类设为非抽象类也没有问题。


0

@Yair Halberstadt "...为什么我应该选择工厂方法而不是静态工厂...". 两者有不同的用途。静态工厂是一个代码块,它(只)收集对象层次结构的实例化代码(很容易被名称所说服)。但它的本质是静态的(尽管可以在实例级别编写),这意味着它在编译时绑定。当然,我们可以扩展静态工厂,但主要是作为补丁代码。(大多数情况下不建议客户端在框架级别进行扩展。)

相比之下,工厂方法对于编写常见业务逻辑非常有用,而无需指定客户端将在以后的日期上编写的具体受益者类型

一个虚构的例子(假设普遍税务计算)-

public abstract class TaxComputer {
    protected abstract Calculator getCalculator();// Factory method
    public void registerIncome(){
        //...
    }
    public Amount computeTax(){
        Calculator alculator = getCalculator();
        //... Tax computation logic using abstract Calculator
        // Note: Real Calculator logic has not been written yet
        return tax;
    }
}

public interface Calculator {
    Amount add(Amount a, Amount b);
    Amount subtract(Amount a, Amount b);
    Amount multiply(Amount a, double b);
    Amount roundoff(int nearestAmount);
    // ...
}

我所有的税务规则实现都是使用抽象计算器进行操作,该计算器需要使用金额。目前只需要抽象计算器。一旦税务计算机准备好,它就可以发布给客户进行扩展(在抽象类和挂钩工厂方法上使用扩展点)。

特定的客户可以将其扩展为适用于日元(不具有小数点)或美元/英镑等货币的计算,甚至可以是在每次操作后按照本地规则四舍五入(例如,四舍五入到下一个10卢比)的计算器。

同样,USDollarTaxCalculator将使用其自己的操作规则进行扩展(但不需要重新定义税务规则)。

public class YenTaxComputer extends TaxComputer {
    @Override
    protected Calculator getCalculator() {
        return new YenCalculator();
    }
}

public class YenCalculator implements Calculator {
    @Override
    public Amount add(Amount a, Amount b) {
        /*Yen specific additions and rounding off*/ 
    }
    @Override
    public Amount subtract(Amount a, Amount b) {/*...*/ }
    @Override
    public Amount multiply(Amount a, double b) {/*...*/ }
    @Override
    public Amount roundoff(int nearestAmount) {/*...*/  }
}

在工厂方法中,重点不在于隐藏创建逻辑,而在于为客户端保持扩展的可能性。

客户端将类似于以下内容

    public class TaxClient {
        public static void main(String[] args) {
            TaxComputer computer = new YenTaxComputer();//
            computeTax(computer);
        }
        /** A PLACE HOLDER METHOD FOR DEMO
         */
        private static void computeTax(TaxComputer computer) {
            Amount tax = computer.computeTax();
            Amount Payment = ...
            //...
        }
    }

需要注意的点:

  1. 客户端不使用工厂方法 getCalculator()
  2. 可以通过静态工厂/反射等方式创建具体的 Creator/ 工厂实例,选择可以根据系统变量、平台、语言环境等进行。
  3. 这种设计模式有其自身的缺点/成本,如并行的类层次结构(YenTaxComputer 只负责创建实例)。没有免费的午餐。

谢谢提供这个例子,我觉得很有帮助。如果有客户端实现的话会更好。对于“在工厂方法中,重点不是隐藏创建逻辑,而是为客户端保持扩展性”,我给予赞同。 - Praveen Rai
@PraveenRai - 我更新了我的答案,加入了客户端代码。 - Kedar Tokekar

0

我发现以下链接对于理解为何需要用抽象工厂类以及使用工厂模式的逻辑非常有帮助。

https://msdn.microsoft.com/en-us/library/ee817667.aspx

让我引用文章中该模式背后的逻辑

考虑一个应用程序,该应用程序模拟了各种个人电脑的组装过程。该应用程序包含一个ComputerAssembler类,负责计算机的组装,一个Computer类,模拟正在构建的计算机,以及一个ComputerFactory类,用于创建Computer类的实例。通过使用工厂模式,ComputerAssembler类将创建Computer实例的责任委托给ComputerFactory。这确保如果实例化和/或初始化过程发生更改(例如新的构造函数,使用激活器,自定义池等),则ComputerAssembler完全不需要更改。这是从对象的创建中抽象出来的好处。
此外,假设业务需求发生变化,需要组装一种新类型的计算机。与其直接修改ComputerAssembler类,不如修改ComputerFactory类以创建新计算机类的实例(假设此新类具有与Computer相同的接口)。此外,也可以通过创建一个新的工厂类(具有与ComputerFactory相同的接口),以创建新计算机类的实例(也是使用Computer相同的接口)。与之前一样,ComputerAssembler类中的任何内容都不需要更改,所有逻辑都继续像以前一样工作。这是将产品和工厂类型抽象为不变接口的好处。
第二段指出,创建者使我们能够创建新的工厂,以处理可能已经引入产品或其创建逻辑中的更改,迫使我们创建新的工厂。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接