Dev Blog

Принцип открытости/закрытости (Open-Closed Principle; OCP)

Принцип был сформулирован Бертраном Мейером в 1988 году:
Программные сущности должны быть открыты для расширения и закрыты для изменения.1

Иными словами, должна иметься возможность расширять поведение программных сущностей без их изменения1.

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

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

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

После такого рефакторинга добавлять новые источники и тестировать их будет гораздо легче, ведь старый код не изменялся, а только добавились новые реализации.

 

Код до рефакторинга:

        public async Task<ResponseModel> LoadData(string source, CancellationToken cancellationToken)
        {
            ResponseModel? responseModel = null;

            _logger.LogInformation(1084061059, "source is {source}", source);

            if (source == "source1")
            {
                var response = await _source1.LoadData1(cancellationToken);

                responseModel = _mapper.Map<Source1Response, ResponseModel>(response);
            }
            else if (source == "source2")
            {
                var response = await _source2.LoadData2(cancellationToken);

                responseModel = _mapper.Map<Source2Response, ResponseModel>(response);
            }
            else if (source == "source3")
            {
                var response = await _source3.LoadData3(cancellationToken);

                responseModel = _mapper.Map<Source3Response, ResponseModel>(response);
            }

            //logic

            return responseModel ?? throw new NotImplementedException(source);
        }

Рефакторинг через стратегию

Основной механизм:

        public async Task<ResponseModel> LoadData(ISource source, CancellationToken cancellationToken)
        {
            _logger.LogInformation(1084061059, "source is {source}", source);

            var responseModel = await source.LoadData(cancellationToken);
            
            //logic

            return responseModel;
        }

Стратегия:

    public interface ISource
    {
        Task<ResponseModel> LoadData(CancellationToken cancellationToken);
    }

Одна из реализаций источника:

    public class Source1 : ISource
    {
        public Task<ResponseModel> LoadData(CancellationToken cancellationToken) => //load logic;
    }

Фабрика:

        public ISource SourceFactory(string source) =>
            source switch
            {
                "source1" => new Source1(),
                "source2" => new Source2(),
                "source3" => new Source3(),
                _ => throw new ArgumentOutOfRangeException(nameof(source), source, null)
            };

Использование:

            var source = SourceFactory("source1");
            var response = await LoadData(source, CancellationToken.None);

Рефакторинг через шаблонный метод

Основной механизм в абстрактном классе:

        public abstract class Source
        {
            public abstract string Name { get; }
            protected abstract Task<ResponseModel> LoadDataFromSource(CancellationToken cancellationToken);

            public async Task<ResponseModel> LoadData(CancellationToken cancellationToken)
            {
                var responseModel = await LoadDataFromSource(cancellationToken);

                //logic
                
                return responseModel;
            }
        }

Источники реализующие абстрактный Name и LoadDataFromSource :

        public class SourceClass1 : Source
        {
            public override string Name => "source1";

            protected override Task<ResponseModel> LoadDataFromSource(CancellationToken cancellationToken)
            {
                //load logic           
            }
        }

        public class SourceClass2 : Source
        {
            public override string Name => "source2";

            protected override Task<ResponseModel> LoadDataFromSource(CancellationToken cancellationToken)
            {
                //load logic             
            }
        }

Фабрика:

        private List<Source> _sources = new()
        {
            new SourceClass1(),
            new SourceClass2(),
            new SourceClass3()
        };

        public Source SourceFactoryMethod2(string source) =>
            _sources.FirstOrDefault(a => a.Name == source) ??
            throw new ArgumentOutOfRangeException(nameof(source), source, null);

Использование:

            var source = SourceFactoryMethod2("source1");
            var response = await source.LoadData(CancellationToken.None);

 

Примечания

1. Мартин Р. Чистая архитектура. Искусство разработки программного обеспечения. — СПб.: Питер, 2018.