SOLID - 单一职责原则和开闭原则是否互斥?

5
单一职责原则指出:

一个类应该只有一个引起它变化的原因。

开闭原则指出:

你应该能够扩展一个类的行为,而不需要修改它。

如果一个类应该只有一个变化原因,但是又不能被修改,开发人员如何遵循这两个原则呢? 示例 工厂模式是一个很好的例子,它具有单一职责,但可能会违反开闭原则:
public abstract class Product
{
}

public class FooProduct : Product
{
}

public class BarProduct : Product
{
}

public class ProductFactory
{
    public Product GetProduct(string type)
    {
        switch(type)
        {
            case "foo":
                return new FooProduct();
            case "bar":
                return new BarProduct();
            default:
                throw new ArgumentException(...);
        }
    }
}

如果我在以后需要将ZenProduct添加到工厂中,会发生什么?

  • 这是否违反了开放/封闭原则?
  • 我们如何可以避免这种违规情况?

你能概述一下矛盾吗?举个例子可能会很有帮助。 - 0b101010
@0b101010 请查看更新。 - Matthew Layton
我认为这里的关键词是“应该”。因此,举个现实的例子,如果这是您自己的代码,那么就去更改它。但是,如果您将其提供给另一个团队/客户,则他们可能需要具有修改此行为的能力,而无需请求您(提供者)进行更改,在这种特定情况下,可以通过使方法虚拟来实现。在我看来,OCP非常有争议,如果您正在编写自己的完整代码集,则它是SOLID原则中最不有趣的一个。 - LordWilmore
这感觉像是关于“扩展类行为”的语义讨论。将新类型添加到工厂中是修改现有行为,而不是扩展行为,因为我们没有改变工厂所做的一件事情。我们可能需要扩展工厂,但我们并没有扩展它的行为。扩展行为意味着引入新行为,更接近于每次创建类型实例时引发事件或授权工厂的调用者 - 这些示例都扩展(引入新的)行为。 - qujck
@qujck 引入新的行为不违反 SRP 原则吗? - Matthew Layton
显示剩余4条评论
4个回答

3
这感觉像是关于“扩展类的行为”的语义讨论。将新类型添加到工厂中是修改现有行为,而不是扩展行为,因为我们没有改变工厂所做的唯一事情。我们可能需要扩展工厂,但我们并没有扩展其行为。扩展行为意味着引入新行为,更接近于每次创建类型实例时引发事件或授权工厂的调用者-这两个示例都扩展(引入新的)行为。

一个类应该只有一个改变的原因。

问题中的示例是用于创建Product实例的工厂,它的唯一有效更改原因是更改它创建的Product实例的某些内容,例如添加新的ZenProduct。

你应该能够扩展一个类的行为,而无需修改它。

通过使用装饰器Decorator,可以非常简单地实现这一点。

装饰器模式通常对遵循单一职责原则很有用,因为它允许在具有独特关注领域的类之间分配功能。

public interface IProductFactory
{
    Product GetProduct(string type);
}

public class ProductFactory : IProductFactory
{
    public Product GetProduct(string type)
    {
        \\ find and return the type
    }
}

public class ProductFactoryAuth : IProductFactory
{
    IProductFactory decorated;
    public ProductFactoryAuth(IProductFactory decorated)
    {
        this.decorated = decorated;
    }

    public Product GetProduct(string type)
    {
        \\ authenticate the caller
        return this.decorated.GetProduct(type);
    }
}

装饰器模式是应用SOLID原则时强大的一种模式。在上述示例中,我们向ProductFactory添加了身份验证,而不改变ProductFactory本身。

1
真巧啊,我现在正在观看 Pluralsight 上有关装饰器模式的教程! - Matthew Layton

2

一个类应该有且仅有一个改变的原因。

这基本上意味着,您的类应该代表单一职责,并且不应该被修改以适应新功能。

例如,如果您有一个类,它负责以pdf格式打印报告。后来,您想要添加新功能以支持在其他格式中打印报告。然后,您应该扩展它以支持其他格式,这也意味着扩展类的行为,而不修改它


我同意这一点,但仍有许多例子违背了这些原则。 - Matthew Layton
@series0ne 可能吧,但我现在想不起来有任何例子是它们会互相矛盾的。 :-) - Ravi
请看一下更新。我已经添加了一个例子。 - Matthew Layton

1
我认为这取决于您对SRP的解释。这些东西总是有点主观的。请100个人定义“单一职责”,您可能会得到100个不同的答案。
使用 Ravi's answer中的场景,一个典型的解决方案可能是拥有一个ReportGenerator类,该类公开一个GeneratePdf方法。如果需要,稍后可以通过添加其他GenerateWord方法来扩展它。但像你一样,我认为这有点不妥。
我可能会将GeneratePdf方法重构为PdfReportGenerator类,然后通过ReportGenerator公开它。这样,ReportGenerator只有一个单一职责;即公开各种报告生成机制(但不包含其逻辑)。然后可以扩展它而不扩展该职责。
我想说,如果您发现冲突,那可能是一种架构上的问题,需要快速检查以查看是否可以以更好的方式完成。

0

我有一个名为StudentOrganiser的类,它需要依赖于IStudentRepository。由IStudentRepository公开的接口是GetStudent(int studentId)

该类遵循SRP原则,因为它没有与管理与存储库源的连接相关的任何逻辑。

该类遵循OCP原则,因为如果我们想要将存储库源从SQL更改为XML,则无需对StudentOrganiser进行任何更改=>开放扩展但关闭修改。

考虑一下,如果StudentOrganiser被设计为不依赖于IStudentRepository,那么类内部的方法必须负责实例化new StudentSqlRepository()。如果以后的要求还需要基于某些运行时条件支持StudentXMLRepository,则您的方法将以某种case switch范式结束,从而违反SRP,因为方法也涉及实际存储库决策因素。通过注入存储库依赖项,我们将该责任从类中移除。现在,StudentOrganiser类可以扩展以支持StudentXMLRepository而无需进行任何修改。


我认为你可能有点困惑。你并没有通过注入不同的IStudentRepository实现来扩展StudentOrganiser - 0b101010
@0b101010:我明白了 :) 我更新了我的答案来解释这一点。 - rahulaga-msft

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