SOLID - являются ли принцип единой ответственности и принцип открытости / закрытости взаимоисключающими?

Принцип единой ответственности гласит:

У класса должна быть одна и только одна причина для изменения.

Принцип открытости / закрытости гласит:

Вы должны иметь возможность расширять поведение классов, не изменяя его.

Как разработчик может уважать оба принципа, если у класса должна быть только одна причина для изменения, но его нельзя изменять?

Пример

Шаблон factory является здесь хорошим примером чего-то, что несет единственную ответственность, но может нарушать принцип открытия / закрытия:

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 на завод на более позднем этапе?

  • Неужто это нарушает принцип открытого / закрытого?
  • Как мы можем предотвратить это нарушение?

person Matthew Layton    schedule 09.03.2018    source источник
comment
@Ravi butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod от дяди Боба, известного среди Сообщество разработчиков.   -  person Matthew Layton    schedule 09.03.2018
comment
Можете обозначить противоречие? Пример может быть полезен.   -  person 0b101010    schedule 09.03.2018
comment
@ 0b101010 см. Апдейт.   -  person Matthew Layton    schedule 15.03.2018
comment
В вашем примере я не понимаю, как добавление ZenProduct будет нарушением любого принципа, поскольку вы просто выполняете плановое обслуживание / улучшение - ни один из принципов не предполагает, что после того, как класс написан, его нельзя изменить, и действительно, такого рода изменений - одна из основных причин использовать фабрику в первую очередь. Однако, если вы считаете, что это так, вам следует сделать GetProduct () виртуальным методом, тем самым удовлетворяя обоим принципам.   -  person LordWilmore    schedule 15.03.2018
comment
@LordWilmore OCP заявляет, что класс A следует закрыть для модификации. Поэтому изменение его на добавление ZenProduct нарушает принцип. Я согласен с тем, что расширение фабрики предотвратит это нарушение, однако это связано с большими накладными расходами, плюс вам также придется нарушить OCP в другом месте, потому что вам придется изменить класс, чтобы использовать новую фабрику.   -  person Matthew Layton    schedule 15.03.2018
comment
Я считаю, что ключевое слово здесь - «должен». Так что, как пример из реальной жизни, если это ваш собственный код, то войдите и измените его. Но если вы предоставляете это другой команде / клиенту, тогда им может потребоваться возможность изменить это поведение, не прося вас, поставщика, внести это изменение, и в этом конкретном случае это может быть достигнуто путем внесения метод виртуальный. ИМХО, OCP очень спорный, и если вы работаете над своим собственным полным набором кода, то это наименее интересный из принципов SOLID.   -  person LordWilmore    schedule 15.03.2018
comment
Это похоже на обсуждение семантики «расширения поведения классов». Добавление нового типа в фабрику - это изменение существующего поведения, это не расширение поведения, потому что мы не изменили единственное, что делает фабрика. Нам может потребоваться расширить фабрику, но мы не расширили ее поведение. Расширение поведения означает введение нового поведения и будет больше похоже на событие каждый раз, когда создается экземпляр типа или авторизует вызывающий объект фабрики - оба этих примера расширяют (вводят новое) поведение.   -  person qujck    schedule 15.03.2018
comment
@qujck не вводит новое поведение в класс, нарушая SRP?   -  person Matthew Layton    schedule 15.03.2018
comment
@ series0ne да, но ваш пример не добавляет нового поведения   -  person qujck    schedule 15.03.2018


Ответы (4)


Это похоже на обсуждение семантики «расширения поведения классов». Добавление нового типа в фабрику изменяет существующее поведение, а не расширяет поведение, потому что мы не изменили единственное, что делает фабрика. Нам может потребоваться расширить фабрику, но мы не расширили ее поведение. Расширение поведения означает введение нового поведения и будет больше похоже на событие каждый раз, когда создается экземпляр типа или авторизует вызывающий объект фабрики - оба этих примера расширяют (вводят новое) поведение.

У класса должна быть одна и только одна причина для изменения.

Пример в вопросе - это фабрика для создания 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.

person qujck    schedule 16.03.2018
comment
Какое совпадение. Я сейчас смотрю учебник по шаблону декоратора во множественном числе! - person Matthew Layton; 16.03.2018

У класса должна быть одна и только одна причина для изменения.

По сути, это означает, что ваш класс должен представлять единоличную ответственность и не должен впоследствии изменяться для включения новой функции.

Например, если у вас есть класс, который отвечает за печать отчета в формате pdf. Позже вы захотели добавить новую функцию для поддержки печати отчетов в других форматах. Затем вместо изменения существующего кода вы должны расширить его для поддержки другого формата, что также подразумевает расширение поведения классов без его изменения.

person Ravi    schedule 09.03.2018
comment
Я согласен с этим, но есть еще МНОГО примеров, когда принципы противоречат друг другу. - person Matthew Layton; 09.03.2018
comment
@ series0ne Возможно, но я не мог сейчас вспомнить ни одного примера, где бы они противоречили друг другу. :-) - person Ravi; 09.03.2018
comment
Взгляните, пожалуйста, на обновление. Я добавил пример. - person Matthew Layton; 15.03.2018

Я думаю, это зависит от вашей интерпретации SRP. Это всегда несколько субъективно. Попросите 100 человек дать определение «единоличной ответственности», и вы, вероятно, получите 100 разных ответов.

Используя сценарий из ответа Рави, типичным решением может быть класс ReportGenerator, который предоставляет GeneratePdf метод. Позднее он может быть расширен дополнительным GenerateWord методом, если потребуется. Хотя, как и вы, я думаю, что в этом есть привкус.

Я бы, вероятно, реорганизовал метод GeneratePdf в класс PdfReportGenerator, а затем представил бы его через ReportGenerator. Таким образом, ReportGenerator несет только одну ответственность; который должен раскрыть различные механизмы генерации отчетов (но не содержать их логики). Затем его можно было бы продлить, не расширяя эту ответственность.

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

person 0b101010    schedule 09.03.2018

У меня есть класс StudentOrganiser, который принимает IStudentRepository зависимости. Интерфейсы, представленные IStudentRepository, говорят GetStudent(int studentId)

Класс подчиняется SRP, потому что у него нет никакой логики, связанной с управлением соединением с источником репозитория.

Класс подчиняется OCP, потому что, если мы хотим изменить источник репозитория с SQL на XML, StudentOrganiser не нужно претерпевать никаких изменений => открыт для расширения, но закрыт для модификации.

Подумайте, если StudentOrganiser был разработан так, чтобы не зависеть от IStudentRepository, тогда метод внутри самого класса должен заботиться о создании экземпляра new StudentSqlRepository() Если позже возникнет потребность также поддерживать StudentXMLRepository на основе определенного условия времени выполнения, ваш метод закончился бы с некоторая case switch парадигма и, таким образом, нарушение SRP как метода также является решающим фактором фактического хранилища. Внедряя зависимость репозитория, мы сняли эту ответственность с класса. Теперь класс StudentOrganiser можно расширить для поддержки StudentXMLRepository без каких-либо изменений.

person rahulaga_dev    schedule 09.03.2018
comment
Думаю, вы здесь немного запутались. Вы не расширяете StudentOrganiser путем внедрения другой IStudentRepository реализации. - person 0b101010; 09.03.2018
comment
@ 0b101010: Понятно :) обновил свой ответ, чтобы объяснить суть - person rahulaga_dev; 10.03.2018