Принцип был сформулирован Бертраном Мейером в 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.