了解使用单一责任原则的实际好处

时间:2013-11-19 13:37:08

标签: c# solid-principles single-responsibility-principle

我正在努力了解SRP,但是,虽然我理解了如何应用它的原因,但我并没有真正看到这样做的好处。考虑这个例子,取自Robert Martin的SRP PDF

interface IModem
{
    void Dial(string number);
    void Hangup();
    void Send(char c);
    char Recv();
}

他建议将其分为两个界面:

interface IModemConnection
{
    void Dial(string number);
    void Hangup();
}

interface IModemDataExchange
{
    void Send(char c);
    char Recv();
}

我也一直在阅读this article,这更进了一步:

interface IModemConnection : IDisposable
{
    IModemDataExchange Dial(string number);
}

interface IModemDataExchange
{
    void Send(char c);
    char Recv();
}

此时,我理解功能(Send / Recv)和非功能(Dial / Hangup)方面的含义,但我没有看到在此示例中将它们分开的好处。考虑到这个基本实现:

class ConcreteModem : IModemConnection
{
    public IModemDataExchange Dial(string number)
    {
        if (connection is successful)
        {
            return new ConcreteModemDataExchange();
        }

        return null;
    }

    public void Dispose()
    {
        // 
    }

    public bool IsConnected { get; private set; }
}

此时,让我再次引用罗伯特马丁,即使他正在谈论与PDF不同的例子:

  

其次,如果对GraphicalApplication的更改导致Rectangle由于某种原因发生更改,该更改可能会强制我们重建,重新测试和重新部署ComputationalGeometryApplication 。如果我们忘记这样做,该应用程序可能会以不可预测的方式破坏。

这是我不明白的。如果我必须创建IModemDataExchange的第二个实现,并且我想要使用它,我仍然需要更改Dial方法,这意味着该类也需要重新编译:

public IModemDataExchange Dial(string number)
{
    if (some condition is met)
    {
        return new ConcreteModemDataExchange();
    }
    else if (another condition is met)
    {
        return new AnotherConcreteModemDataExchange();
    }

    return null;
}

我无法看到这样做是为了减少改变对课程的影响。它仍然需要重新编译,那么有什么好处呢?你从这样做中获得了什么,这对于生成高质量的代码非常重要?

4 个答案:

答案 0 :(得分:10)

对我来说,上面的调制解调器示例似乎总是interface segregation principle而不是SRP的情况,但除了这一点之外。

在你关于Rectangle的部分中,我认为你只是误解了它。 Martin使用Rectangle作为共享库的示例。如果GraphicalApplication需要新方法或Rectangle类中的语义更改,则会影响ComputationalGeometryApplication,因为它们都“链接”到Rectangle库。他说它违反了SRP,因为它负责定义渲染边界和代数概念。想象一下,如果GraphicalApplication从DirectX更改为OpenGL,其中y坐标被反转。您可能希望更改Rectangle上的某些内容以促进此操作,但您可能会在ComputationalGeometryApplication中导致更改。

在我的工作中,我尝试遵循SOLID原则和TDD,并且我发现SRP使得为类编写测试变得简单并且还使课程集中。遵循SRP的类通常非常小,这降低了代码和依赖性的复杂性。在删除课程时,我会尝试确保课程要么“做一件事”,要么“协调两件(或更多件事)”。这使他们保持专注,并使他们改变的理由仅取决于他们所做的一件事,对我而言,这是SRP的重点。

答案 1 :(得分:3)

主要好处是显而易见的。通过拆分,您可以为您的模型提供更好的逻辑分组,从而使意图更清晰,维护更容易。

  

如果我必须创建IModemDataExchange的第二个实现,并且我想要使用它,我仍然需要更改Dial方法

是的必须,但那不是好处。一个好处是,当您对IModemDataExchange接口本身进行任何修改时,您只需要更改接口的具体实现,而不是ConcreteModem本身,这将使Dial方法的订阅者的维护更容易。另一个好处是,即使你必须编写一个额外的IModemDataExchange实现,然后在ConcreteModem类中需要的更改被最小化,没有直接耦合。 通过分离责任,您可以最大限度地减少修改的副作用。

不需要重新编译不是这里的本质。从严格意义上讲,如果其中一个接口在另一个项目中呢?它节省了一个项目的重新编译。压力在于不需要在很多地方改变代码。当然,任何更改都需要重新编译。

答案 2 :(得分:1)

如果您使用抽象工厂,则无需更改ConcreteModem。或者,如果您通过应在成功时创建的具体类型参数化通用Modem<TModemDataExchange>(或通用Dial<TModemDataExchange>()方法)。

这个想法是IModemConnection实现不依赖于有关IModeDataExchange实现的任何信息,除了它的名称。

继续前进,我会考虑采用以下方法:

interface IModemConnection : IDisposable
{
    void Dial(string number);
}

interface IModemDataExchange
{
    void Send(char c);
    char Recv();
}

class ConcreteModemDataExchange : IModemDataExchange
{
    ConcreteModemDataExchange(IModemDataExchange);
}

因此,要创建ConcreteModemDataExchange实例,您需要建立连接。仍然有可能断开连接的实例,但这是一个不同的故事。

作为一个副节点,我建议在失败时在Dial中抛出异常。

答案 3 :(得分:1)

我不太了解调制解调器的工作原理,所以我努力想出一个有意义的例子。但是,请考虑一下:

分离了拨号逻辑,现在如果程序的其他部分只需要拨号,我们只能传入一个IModemConnection。 即使在使用依赖注入的Modem类本身中,这也很有用:

public class Modem : IModemConnection, IModemDataExchange
{
    public IModemConnection Dialer {get; private set;}

    public Modem(IModemConnection Dialer)
    {
        this.Dialer=Dialer;
    }

    public void Dial(string number)
    {
        Dialer.Dial(number);
    }

    public void Hangup()
    {
        Dialer.Hangup();
    } 

    // .... implement IModemDataExchange
}

现在你可以:

public class DigitalDialer : IModemConnection
{
    public void Dial(string number)
    {
        Console.WriteLine("beep beep");
    }
    public void Hangup()
    {
        //hangup
    }
}

public class AnalogDialer : IModemConnection
{
    public void Dial(string number)
    {
        Console.WriteLine("do you even remember these?");
    }
    public void Hangup()
    {
        //hangup
    }
}

现在,如果您想要更改调制解调器工作方式的某些方面(在这种情况下拨打号码的方式),您的更改将在具有单一责任(拨号)的拨号程序类中进行本地化。