Композиция или наследование
Возникли у нас как-то с коллегой разногласия по поводу реализации одной функциональности. Лучшие практики объектно-ориентированного проектирования советуют нам предпочитать композицию наследованию. Но всегда ли это верно?
Коротко о домене. Есть у нас сервис поиска поставщиков электронных деталей, и сайт, на котором его можно осуществлять по подписке. А ещё есть сервис, который мы предоставляем производителям этих деталей. Заключается он в том, что мы даём им доступ к результатам нашего поиска «на их сайте». Чтобы на их сайте покупатель мог легко найти, какие поставщики их деталей есть рядом. Да, в наше время уже 99,9% читателей подумали бы, что мы предоставляем API, но нет. В смысле, API тоже есть, но поскольку сервису уже почти 20 лет и не у каждого производителя есть отдел разработки, который мог бы прикрутить этот API к их сайту, есть у нас и другая опция. Мы создаём страницы с функциональностью (поиск, результаты, возможность заказа и т.п.) и подгоняем их под внешний вид сайта производителя.
К сути. Конечно, каждый наш клиент хочет, чтобы функциональность максимально соответствовала его сайту. И стандартная реализация не всегда подходит. Кто-то хочет разную логику поиска: «с начала», «содержит», «равно». Кто-то разные регионы: по умолчанию результаты делятся на Америка, Европа и Азия, но некоторые просят поделить по-другому. И так далее, там настроек выше крыши. Большинство из них легко сохраняется в JSON, но не все. Например, кто-то хочет, чтоб на странице результатов поиска повторялась форма поиска, а кто-то, чтоб результаты определённого поставщика показывались сверху. Тут уже не избежать кастомной бизнес-логики.
Мы сошлись на том, что должен быть базовый класс со стандартным поведением, реализующий интерфейс. Вот его упрощённый код. Только методы, каждый из которых реализует нужный кусок функциональности (допустим, форматирует вывод):
puclic class SearchResults : ISearchResults
{
public Header GetHeader(…) { … }
public Rows GetRows(…) { … }
public Footer GetFooter(…) { … }
}
Кроме того, мы сошлись на том, что в случае, когда нужно что-то поменять, мы просто создаём отдельный класс для определённого производителя и переопределяем там нужную логику. Потом в DI-контейнере для каждого производителя внедряется либо класс по умолчанию, либо его специализированный (если он есть).
Так вот, моя реализация предполагала, что спец-класс просто наследовал бы от базового и переопределял нужную функциональность:
puclic class CustomSearchResults
: SearchResults, ISearchResults
{
public override Header GetHeader(…)
{
// кастомная логика
}
}
Остальные методы автоматически наследовались бы. Коллега настаивал на предпочтении композиции наследованию, и что мы должны делать декоратор базового класса. Как-то так:
puclic class CustomSearchResults
: ISearchResults
{
private SearchResults _base;
public CustomSearchResults(SearchResults sr)
{
_base = sr;
}
public Header GetHeader(…)
{
// кастомная логика
}
public Caption GetRows(…)
=> _base.GetRows(…);
public Caption GetFooter(…)
=> _base.GetFooter(…);
}
По мне так в декораторе куча лишней логики. Мы обязаны реализовать все методы, даже если мы просто вызываем метод декорируемого класса. С другой стороны – это «правильная» композиция вместо «неправильного» наследования.
Так мы к согласию и не пришли. А вы что скажете? Может есть вообще какой-то третий, «более лучший», вариант?