Подтвердить что ты не робот

Понимание практических преимуществ использования принципа единой ответственности

Я пытаюсь понять SRP, но, хотя я понимаю аргументы в пользу того, как его применять, я не вижу в этом преимущества. Рассмотрим этот пример, взятый у Роберта Мартина 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();
}

Я также читал эту статью, которая делает следующий шаг:

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;
}

Я не вижу, что это сделало, чтобы уменьшить влияние изменений на класс. Его все еще нужно перекомпилировать, так что польза? Что вы получаете от этого, что так важно для создания качественного кода?

4b9b3361

Ответ 1

Мне модемный пример выше всегда казался примером для принципа сегрегации интерфейса, а не для SRP, но кроме того.

В той части, которую вы вызвали в отношении Rectangle, я думаю, вы просто неправильно интерпретируете ее. Мартин использует Rectangle в качестве примера общей библиотеки. Если для параметра GraphicalApplication требуется новый метод или изменение семантики в классе Rectangle, это влияет на ComputationalGeometryApplication, поскольку они оба "ссылаются" на библиотеку Rectangle. Он говорит, что это нарушает SRP, потому что он отвечает за определение границ рендеринга, а также за алгебраическое понятие. Представьте себе, изменился ли GraphicalApplication с DirectX на OpenGL, где y-координата инвертирована. Вы можете изменить некоторые вещи на Rectangle, чтобы облегчить это, но затем вы потенциально вызываете изменения в ComputationalGeometryApplication.

В своей работе я стараюсь следовать принципам SOLID и TDD, и я обнаружил, что SRP делает простые тесты для простых классов, а также поддерживает классы сосредоточены. Классы, которые следуют за SRP, как правило, очень малы, и это снижает сложность как кода, так и зависимостей. Когда вы занимаетесь занятиями, я стараюсь, чтобы класс "делал одну вещь" или "координировал две (или более) вещи". Это позволяет им сфокусироваться и делает свои причины для изменений только зависимыми от того, что они делают, что для меня является точкой SRP.

Ответ 2

Основное преимущество совершенно очевидно. Разделившись, вы даете вашей модели лучшую логическую группировку, которая, в свою очередь, упрощает задачу и упрощает техническое обслуживание.

Если мне пришлось создать вторую реализацию IModemDataExchange, и я хотел бы использовать ее, мне все равно пришлось бы изменить метод набора

Да, придется, но в этом нет пользы. Одно из преимуществ заключается в том, что, когда у вас есть какие-либо изменения для интерфейса IModemDataExchange, вам нужно только изменить конкретные реализации интерфейса, а не ConcreteModem, что облегчит обслуживание подписчиков Dial. Другое преимущество заключается в том, что теперь, даже если вам нужно написать дополнительную реализацию IModemDataExchange, тогда изменения, которые потребуются в классе ConcreteModem, минимизированы, нет прямой связи. Отделяя обязанности, вы минимизируете побочные эффекты модификаций.

Не нужно перекомпилировать, это не суть. И в строгом смысле, что, если один из этих интерфейсов находится в другом проекте? Это экономит перекомпиляцию одного проекта. Усилие не требует изменения кода во многих местах. Конечно, для любого изменения потребуется перекомпиляция.

Ответ 3

Вам не нужно менять ConcreteModem, если вы используете абстрактный factory. Или если вы параметризуете общий 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, вам нужно иметь соединение. Есть еще возможность отключить экземпляр соединения, но это другая история.

Как сторона node, я бы рекомендовал исключить исключение в Dial при ошибке.

Ответ 4

Я не знаю много о том, как работают модемы, поэтому я немного попытался придумать осмысленный пример. Однако учтите следующее:

Отделив логику набора номера, теперь, если некоторые другие части программы нужно только набрать, мы можем передать только IModemConnection. Это может быть полезно даже в самом модемом, используя инъекцию зависимостей:

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
    }
}

теперь, если вы хотите изменить некоторые аспекты работы вашего модема (способ, которым он набирает номер в этом случае), ваши изменения будут локализованы в классе Dialer, у которого есть отдельная ответственность (набор номера).