【问题标题】:Factory class is not supporting SOLID principle工厂类不支持 SOLID 原理
【发布时间】:2017-01-22 03:03:02
【问题描述】:

我有如下代码

 public interface ICar
{
    void Created();
}

public class BigCar : ICar
{
    public void Created()
    {

    }
}

public class SmallCar : ICar
{
    public void Created()
    {

    }
}


public class LuxaryCar : ICar
{
    public void Created()
    {

    }
}

public class CarFactory
{
    public ICar CreateCar(int carType)
    {
        switch (carType)
        {
            case 0:
                return new BigCar();
            case 1:
                return new SmallCar();
            case 2:
                return new LuxaryCar();
            default:
                break;
        }
        return null;
    }
}

在这段代码中,我有一个返回具体实例的工厂。但是每次我需要有一个新的 ICar 接口实现时,我都必须更改 CarFactory 的 CreateCar() 方法。看来我不支持 SOLID 原则的开放封闭原则。请建议是否有更好的方法来处理这种情况。

【问题讨论】:

  • 您不应该将carType 传递给CreateCar 方法。相反,工厂本身将决定它创建的汽车类型。您将为每种想要支持的汽车类型创建不同的工厂。
  • @Vlad 这意味着 switch 语句将移动到您决定使用哪个工厂的某个地方。恕我直言,OP 正在询问如何完全消除 switch 语句。即使您在调用站点创建工厂,您也需要以某种方式将 carType 转换为工厂。
  • 您应该使用一些包含类型名称和程序集的配置文件来查找要创建的与键关联的类型。然后您可以在您的工厂中使用 Activator.CreateInstance 来表示该类型。
  • @HimBromBeere,这无济于事,因为对于每个新的 ICar,都必须有一个新密钥,对于每个新密钥,工厂中都必须有一个新的 case。你又回到了第一格。
  • @GoodNightNerdPride 不完全是。在这种情况下,您不需要任何开关,您所做的只是调用Activator.CreateInstance(Type.GetType(typeFromConfig)),也许您必须将程序集添加到加载上下文中,但这就是一切。 switch 没用。

标签: c# oop design-patterns solid-principles


【解决方案1】:

您可能希望使其可配置,如下所示:

void Main()
{
    // configurable array
    var factories = new ICarFactory[] { new BigCarFactory() };

    // create factory
    var realfactory = new CarFactory(factories);

    // create car
    var car = realfactory.CreateCar(0);
}

public class CarFactory : ICarFactory
{
    private ICarFactory[] _factories;

    public CarFactory (ICarFactory[] factories)
    {
        _factories = factories;

    }   
    public ICar CreateCar(int carType)
    {
        return _factories.Where(x=>x.SupportCar(carType)).First().CreateCar(carType);
    }

    public bool SupportCar(int type) => _factories.Any(x=>x.SupportCar(type));
}

public interface ICarFactory
{
    ICar CreateCar(int type);
    bool SupportCar(int type);
}

public class BigCarFactory : ICarFactory
{
    public ICar CreateCar(int carType)
    {
        if(carType != 0) throw new NotSupportedException();
        return new BigCar();
    }

    public bool SupportCar(int type) => type == 0;
}


public interface ICar
{
    void Created();
}

public class BigCar : ICar
{
    public void Created()
    {

    }
}

public class SmallCar : ICar
{
    public void Created()
    {

    }
}


public class LuxaryCar : ICar
{
    public void Created()
    {

    }
}

【讨论】:

  • 非常感谢您解决了我的问题。它帮助很大。
  • @Vikram,我不明白这如何解决任何问题。 switch 语句现在分散在各处,即它涉及数组 ICarFactory[] factories、Linq 过滤器 Where() 和许多 SupportCar(int) 实现。问题刚刚被转移到另一个地方(实际上是多个地方)。
  • @GoodNightNerdPride 是的,我同意代码分散在许多地方。有很多课。但是所有的关注点都是分开的。所有的班级都在做他们应该做的事情。这里没有我必须一次又一次改变的课程。
  • @Vikram,除了有一个。您必须为每种新车类型更新factories 数组。那么更新switch 语句的区别在哪里呢?
  • ICarFactory[]composition root 的一部分,通常预计会发生变化。这种设计是测试者-执行者模式的一种变体,是创建可扩展抽象的好方法。 Here's another useful link 这个设计值得考虑。
【解决方案2】:

confusion around the open/closed principle, and what it should mean to your code很多。

事实就在这里,你很好。移动到配置文件只是将代码与数据分开并隐藏意图,这会导致问题。

答案?这取决于。务实。 “只是他妈的硬编码它”。通过保留你的 switch 语句来明确工厂的意图。保持数据和意图紧密结合,就像这里一样。确保首先为缺少的 switch 案例编写测试,并始终包含 default 案例(即使它只是引发异常)。

你的工厂只有一个改变的理由,而且只做一项工作。你现在的设计就可以了。如果您发现自己经常回来添加新案例,那么也许您会想考虑其他方法(配置文件不是答案,它只是将编辑从代码文件移至配置文件)并重构您的解决方案。不断增加的 switch 语句并不好,但与其想办法以不同的方式做到这一点,不如花精力从一开始就解决这个问题。请参阅下面的附录:

附录

另一种解决方法是不要让多个类实现相同的接口。想想其他 SOLID 原则。也许还有另一个一致性或功能边界而不是“汽车类型”?也许有一些功能组可以在更多、特定、细粒度的接口中指定,然后也许你使用 DI 在你需要的地方注入实现,而不是一开始就必须使用工厂(其中,让我们面对现实吧,反正只是一个捏造的构造函数)。另请参阅:“偏好组合胜于继承”。

【讨论】:

  • 程序员有时会忘记设计模式、范式和原则并不是硬科学。很多时候,这是一种权衡,诀窍是使用最适合 您的 域的模式。 A discussion of the same question 在另一个 stackexchange 网站上得出了类似的结论。
【解决方案3】:

看来我的cmets被误解了,所以我们开始吧:

假设我们有一些简单的配置文件:

<config>
  <MyType key="1" type="My.Namespace.Ferrari"/>
  <MyType key="2" type="My.Namespace.Fiat"/>
</config>

现在您可以从该配置中读取并根据类型信息创建您的实例:

ICar CreateCar(int carType)
{
    foreach(var typeInfo in myConfig)
    {
        if(typeInfo.Key == carType) return Activator.CreateInstance(Type.GetType(typeInfo.type));
    }
}

现在只需调用myFactory.CreateCare(2) 即可获取Fiat 的实例。

这肯定假设您已经反序列化了您的配置(例如,使用 Linq2Xml 或使用 XmlSerializer)并且包含这些类型的程序集已经在运行时加载。如果这不适用,您应该先致电Assembly.Load

现在您的工厂仍然需要做的就是读取配置文件,最终加载程序集并构建一个缓存键和类型的字典以稍微提高性能。

依赖外部文件的好处是您不必重新发布整个应用程序,而只需重新发布那些实际更改的部分。

编辑:如果您的类型没有默认构造函数并且您必须提供参数,事情可能会变得复杂。然而,为了简单起见,我们假设我们一个默认构造函数。

【讨论】:

  • 所以您已将switch 移动到xml 文件中。我无法想象有人在 6 个月内维护此代码会多么高兴。特别是如果您没有非常明确地评论或记录它
  • @slawekwin 无论哪种情况,您都必须记录,这不仅限于这种方法。无论如何,请参阅我回答的最后一句话。
  • 我发表此评论的目的是强调彻底评论此类解决方案的重要性,因此将来不会混淆。它可能太强大了,抱歉:)
  • 如果您的类型没有默认构造函数会怎样?你不能把它放在你的界面合同中,这使得这种方法非常脆弱。
  • 这还有一个问题,就是将类型引用放入配置中很容易破坏重构。这是一个低风险区域,但当类名和命名空间更改时可能会导致运行时错误。根据经验,您不应该以这种方式引用类,除非它们是您知道永远不会改变的独立版本库。
【解决方案4】:

我认为 Vlad 在 cmets 中的建议是更好的方法,我在后续评论中对此进行了更具体的说明,但我将在下面详细说明。与tym32167's answer 不同,此方法不会通过将type 参数从应用程序本身发送到ICarFactory 类型,将您的库与使用它的特定应用程序耦合。

换句话说,我认为 tym32167 的回答不会让你的类在每次需要添加新实现时都“关闭以供修改”。


汽车

public interface ICar {
    string Name { get; }
}

class BigCar : ICar {
    string Name {
        get { return "Big Car"; }
    }
}

class SmallCar : ICar {
    string Name {
        get { return "Small Car"; }
    }
}

工厂

public interface ICarFactory {
    ICar CreateCar();
}

public class BigCarFactory : ICarFactory {
    ICar CreateCar() { return new BigCar (); }
}

public class SmallCarFactory : ICarFactory {
    ICar CreateCar() { return new SmallCar (); }
}

请注意,它们不会受到“控制耦合”的影响,即它们不依赖外部“魔法”值来确定工厂的内部控制流程。换句话说,它们不是ICar CreateCar(int type);,工厂只是ICar CreateCar();。工厂不应通过 type 参数与您的特定应用程序耦合,该参数具有特定于应用程序的含义。

把它们放在一起

现在,在您的客户端应用程序中的一些任意方法中,您拥有汽车创建逻辑,即 在您的库之外(例如读取您的配置文件或其他内容),您可以执行类似的操作:

public List<ICar> CreateCars() {
    // organize your factories based on whatever value you want for 'type'
    ICarFactory[] factories = { new BigCarFactory(), new SmallCarFactory() };
    List<ICar> carsList = new List<>();

    while (/* more config to read */ ) {
        int type = // read type from config, etc.
        carsList.Add(factories[type].CreateCar());
    }

    return carsList;
}

现在,每次添加新的ICar 实现时,都会创建一个关联的ICarFactory 实现,这显然很容易做到,并且在任何汽车@987654333 中的ICarFactory[] factories 数组中添加一个新工厂@你想要的位置,type 知识保留在你的特定应用程序中,与你的汽车/工厂库分离。


与其他回复相比,为什么我的提案SOLID

  1. S:这些工厂有 1 个职责:创建他们的汽车实现;其他提案增加了弄清楚type 含义的责任
  2. O:您的ICarICarFactory 可以通过添加新的实现来扩展,但在这样做时不需要修改现有的。
  3. L:我们已经在这样做了,因为我们正在返回 ICar 对象,所以无论实现是 BigCar 还是 SmallCar 或其他什么都没有关系。李>
  4. 我:我们在这里通过保持接口和类简单来做到这一点;其他建议通过 type 参数将您的库与您的应用耦合,该参数具有应用特定的含义
  5. D:在这种特殊情况下,库类并不完全需要依赖注入的外部信息。

【讨论】:

  • 关于“2.O”:您必须为每种新车类型修改现有方法CreateCars()。所以仍然违反了OCP。附带说明:SOLID 中的 D 代表 Dpendency Inversion Principle,而不是 Design-by-Contract。
  • @GoodNightNerdPride,CreateCars 方法专用于客户端应用程序,而不是遵循 OCP 的 car/car-factory 库类的一部分。与其他提案不同,您不必为了创建新型汽车而修改汽车/工厂实现;他们只是得到扩展。
  • @GoodNightNerdPride,D 部分很好。我显然在考虑按合同设计。不知道我为什么这样做,但我已经更新了一些东西,包括那个。我还试图更清楚地说明CreateCars 方法是在客户端应用程序上,而不是在汽车/工厂库方面。
  • 这与@tym32167 的答案完全相同,只是没有错误检查,也没有“将它们放在一起”的类。我更喜欢他的,尽管他写的有点杂乱
  • @slawekwin 我认为错误检查逻辑超出了我的帖子的范围,因为问题不在于如何执行错误检查。我认为我们做了类似的事情,但并不完全相同,尽管这可能只是一个清晰的问题。我的印象是 tym32167 的响应是将库与应用程序耦合,这是我试图解决的一件事,基于我对该帖子的理解。
猜你喜欢
  • 2013-06-04
  • 1970-01-01
  • 2017-09-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多