Является ли «композиция над наследством» нарушением «сухого принципа»?

36

Например, предположим, у меня есть класс для расширения других классов:

public class LoginPage {
    public String userId;
    public String session;
    public boolean checkSessionValid() {
    }
}

и некоторые подклассы:

public class HomePage extends LoginPage {

}

public class EditInfoPage extends LoginPage {

}

На самом деле, у подкласса нет никаких методов для переопределения, также я бы не стал обращаться к HomePage в общем виде, то есть: я бы не делал что-то вроде:

for (int i = 0; i < loginPages.length; i++) {
    loginPages[i].doSomething();
}

Я просто хочу повторно использовать страницу входа. Но согласно https://stackoverflow.com/a/53354 , я бы предпочел композицию здесь, потому что мне не нужен интерфейс LoginPage, поэтому я не использую здесь наследование:

public class HomePage {
    public LoginPage loginPage;
}

public class EditInfoPage {
    public LoginPage loginPage;
}

но проблема возникает здесь, в новой версии, код:

public LoginPage loginPage;

дублируется при добавлении нового класса. И если LoginPage нужны сеттер и геттер, нужно скопировать больше кодов:

public LoginPage loginPage;

private LoginPage getLoginPage() {
    return this.loginPage;
}
private void setLoginPage(LoginPage loginPage) {
    this.loginPage = loginPage;
}

Таким образом, мой вопрос заключается в том, нарушает ли «композиция через наследование» «сухой принцип»?

ocomfd
источник
13
Иногда вам не нужно ни наследования, ни композиции, то есть иногда один класс с несколькими объектами выполняет свою работу.
Эрик Эйдт
23
Почему вы хотите, чтобы все ваши страницы были страницами входа? Может быть, просто иметь страницу входа.
Мистер Кочезе
29
Но с наследованием у вас есть extends LoginPageповсюду дубликаты . МАТ!
el.pescado
2
Если у вас часто возникает проблема с последним фрагментом кода, возможно, вы злоупотребляете геттерами и сеттерами. Они имеют тенденцию нарушать инкапсуляцию.
Брайан МакКатчон
4
Итак, сделайте так, чтобы ваша страница могла быть оформлена, а ваш LoginPage- декоратором. Нет больше дублирования, просто page = new LoginPage(new EditInfoPage())и все готово. Или вы используете принцип open-closed для создания модуля аутентификации, который можно динамически добавлять на любую страницу. Существует множество способов справиться с дублированием кода, в основном с помощью поиска новой абстракции. LoginPageскорее всего, это плохое имя, и вы хотите убедиться, что пользователь проходит аутентификацию при просмотре этой страницы, перенаправляется на LoginPageили показывает правильное сообщение об ошибке, если это не так.
Полигном,

Ответы:

46

Э-э, подождите, вы обеспокоены тем, что повторение

public LoginPage loginPage;

в двух местах нарушает СУХОЙ? По этой логике

int x;

теперь может существовать только в одном объекте во всей базе кода. BLEH.

СУХОЙ это хорошая вещь, чтобы иметь в виду, но давай. Кроме

... extends LoginPage

дублируется в вашей альтернативе, так что даже анальное отношение к СУХОЙ не будет иметь смысла в этом.

Действительные проблемы с СУХОЙ имеют тенденцию фокусироваться на идентичном поведении, необходимом в нескольких местах, определяемых в нескольких местах, так что необходимость изменить это поведение в конечном итоге требует изменения в нескольких местах. Принимайте решения в одном месте, и вам нужно будет только изменить их в одном месте. Это не значит, что только один объект может содержать ссылку на ваш LoginPage.

СУХОЙ не следует слепо следовать. Если вы дублируете, потому что копировать и вставлять проще, чем придумывать хороший метод или имя класса, то вы, вероятно, ошибаетесь.

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

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

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

Но да, это очень много дополнительной клавиатуры. Пока я могу предотвратить проблему йо-йо, связанную с отскоком вверх и вниз по стеку наследования, пока я читаю код, он того стоит.

Теперь я не отказываюсь использовать наследство. Одно из моих любимых применений - давать исключениям новые имена:

public class MyLoginPageWasNull extends NullPointerException{}
candied_orange
источник
int x;может существовать не более нуля раз в кодовой базе
К. Алан Бейтс
@ K.AlanBates извините ?
candied_orange
... я знал, что ты собираешься возразить с классом Point, лол. Никто не заботится о классе Point. Если я зайду к вашей базе кода и увижу, int a; int b; int x;я буду настаивать на том, чтобы удалить вас.
К. Алан Бейтс
@ K.AlanBates Вау. грустно, что вы думаете, что такое поведение сделает вас хорошим программистом. Лучший кодер не тот, который может найти большинство вещей о других, чтобы ссориться. Это то, что делает отдых лучше.
candied_orange
Использование бессмысленных имен не является началом и, вероятно, делает разработчика этого кода скорее пассивом, чем активом.
К. Алан Бейтс
127

Распространенное недоразумение с принципом DRY состоит в том, что оно каким-то образом связано с неповторяющимися строками кода. Принцип СУХОГО: «Каждое знание должно иметь одно, однозначное, авторитетное представление в системе» . Речь идет о знаниях, а не о коде.

LoginPageзнает, как нарисовать страницу для входа в систему. Если бы EditInfoPageзнал, как это сделать, то это было бы нарушением. Включение LoginPageчерез композицию ни в коем случае не является нарушением принципа СУХОЙ.

Принцип СУХОЙ, пожалуй, является наиболее неправильно используемым принципом в разработке программного обеспечения, и его всегда следует рассматривать не как принцип, не дублирующий код, а как принцип, не дублирующий знание абстрактной области. На самом деле, во многих случаях, если вы правильно примените DRY, вы будете дублировать код , и это не обязательно плохо.

wasatz
источник
27
«Принцип единой ответственности» используется гораздо чаще. «Не повторяйся» может легко стать
вторым
28
«СУХОЙ» - это удобная аббревиатура от «Не повторяй себя», которая является ключевой фразой, но не названием принципа, фактическое название принципа - «Один раз и только один раз», что, в свою очередь, является броским названием для принципа, который требует пару абзацев для описания. Важно понимать пару абзацев, а не запоминать три буквы D, R и Y.
Jörg W Mittag
4
@ gnasher729 Вы знаете, я на самом деле согласен с вами, что SRP, вероятно, используется гораздо чаще. Хотя, чтобы быть справедливым, во многих случаях я думаю, что они часто неправильно используются вместе. Моя теория заключается в том, что программистам вообще не следует доверять использованию аббревиатур с «простыми именами».
wasatz
17
ИМХО это хороший ответ. Однако, чтобы быть справедливым: по моему опыту, наиболее частая форма нарушений DRY вызвана программированием копирования-вставки и просто дублированием кода. Кто-то сказал мне однажды: «всякий раз, когда вы собираетесь скопировать и вставить некоторый код, подумайте дважды, если дублированную часть нельзя извлечь в общую функцию», что было ИМХО очень хорошим советом.
Док Браун
9
Злоупотребление копировальной пастой, безусловно, является движущей силой нарушения СУХОЙ, но простое запрещение копировальной пасты - плохое руководство для следования СУХОЙ. Я бы не испытывал сожаления по поводу наличия двух методов с одинаковым кодом, когда эти методы представляют разные части знаний. Они выполняют две разные обязанности. Просто у них сегодня одинаковый код. Они должны быть свободны, чтобы измениться независимо. Да, знания, а не код. Хорошо сказано. Я написал один из других ответов, но я кланюсь этому. +1.
candied_orange
12

Краткий ответ: да, в некоторой степени приемлемо.

На первый взгляд, наследование иногда может сэкономить вам несколько строк кода, так как оно дает эффект «мой класс повторного использования будет содержать все открытые методы и атрибуты в формате 1: 1». Поэтому, если в компоненте есть список из 10 методов, нет необходимости повторять их в коде унаследованного класса. Когда в композиционном сценарии 9 из этих 10 методов должны быть открыты для публичного использования через повторно используемый компонент, то необходимо записать 9 делегирующих вызовов и выпустить оставшийся, и нет никакого способа обойти это.

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

Суть принципа СУХОЙ состоит в том, чтобы в коде не было двух мест, где кодируются одни и те же логические правила - потому что когда эти логические правила изменяются, в не-СУХОМ коде легко адаптировать одно из этих мест и забыть о другом, который вводит ошибку.

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

Примечание к вашему примеру: я не знаю, как вы HomePageи ваши EditInfoPageвыглядите, но если у них есть функция входа в систему, а HomePage(или EditInfoPage) - это LoginPage , то наследование может быть правильным инструментом здесь. Менее спорный пример, где композиция будет лучшим инструментом в более очевидной форме, вероятно, прояснит ситуацию.

Предполагая, что ни то, HomePageни другое не EditInfoPageявляется LoginPage, и кто-то хочет повторно использовать последнее, как вы написали, то, скорее всего, вам потребуются только некоторые части LoginPage, а не все. Если это так, лучшим подходом, чем использование композиции указанным способом, может быть

  • извлекать повторно используемую часть LoginPageв свой компонент

  • повторно использовать этот компонент HomePageи так EditInfoPageже, как он используется сейчас внутриLoginPage

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

Док Браун
источник