Не хватает очков в S.O.L.I.D Essentials?

Я прочитал так много статей об этом, но у меня есть 2 вопроса.

Вопрос № 1 - Относительно инверсии зависимостей:

В нем говорится, что классы высокого уровня не должны зависеть от классов низкого уровня. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Например :

public class BirthdayCalculator
{
    private readonly List<Birthday> _birthdays;

    public BirthdayCalculator()
    {
        _birthdays = new List<Birthday>();// <----- here is a dependency
    }
...

Исправление: поместите это в ctor.

public class BirthdayCalculator
{
    private readonly IList<Birthday> _birthdays;

    public BirthdayCalculator(IList<Birthday> birthdays) 
    {
        _birthdays = birthdays;
    }
  • Если он будет в ctor - мне придется отправлять его каждый раз, когда я использую класс. Так что мне придется оставить его при вызове класса BirthdayCalculator. это нормально делать это так?

  • Я могу утверждать, что после исправления, IList<Birthday> _birthdays не должно быть там (Birthday в IList), но это должно быть как IList<IBirthday>. Я прав ?

Вопрос № 2 - Относительно Замены Лискова:

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

или точнее:

Пусть q (x) - свойство, доказуемое для объектов x типа T. Тогда q (y) должно быть истинным для объектов y типа S, где S - подтип T.

(уже прочитал это)

пример :

public abstract class Account
{
     public abstract void Deposit(double amount);
}

У меня есть класс:

public class CheckingAccount : Account
{
     public override void Deposit(double amount)
    {    ...
        _currentBalance += amount;
    }
}

а банк хочет открыть ипотечный счет - итак:

public class MortgageAccount : Account
{

    public override void Deposit(double amount)
    {
        _currentBalance -= amount; //<-----notice the minus
    }
}

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

public class Bank
{
    public void ReceiveMoney(Account account, double amount)
    {
        double oldBalance = account.CurrentBalance;
        account.Deposit(amount); //oopssss?????
    }
}

так что здесь он нарушает LSP.

Но я не понимаю.

каждый переопределенный метод при переопределении будет выполнять другой код, поэтому он никогда не будет 100% заменяемым!

в определении НЕ говорилось о том, что «логика должна продолжаться, как в базовом классе (всегда вносить (добавлять) положительные числа)»

пример:

Что, если и CheckingAccount класс, и MortgageAccount класс вносили положительные числа, но MortgageAccount также записывались в базу данных? он все еще ломает LSP? Какая граница для LSP обрывается / не тормозит?

определение должно определять, что это за граница. и это ничего не говорит об этом.

что мне не хватает?


comment
Я думаю, что пример LSP не работает по двум причинам. 1) Базового функционала нет, так как он абстрактный. Checking и Mortgage являются отдельными ветвями в иерархии и не дают гарантии, что они реализуют то же самое для Deposit, они просто гарантируют, что реализуют Deposit. Checking и Mortgage не заменяют друг друга, но могут заменять Account. 2) Фактический учет за Mortgage, вероятно, использует +=, и еще один компонент просто преобразует отрицательный баланс в учетной записи в положительное число при отображении в пользовательском интерфейсе.   -  person David    schedule 17.12.2012


Ответы (6)


LSP говорит, что любые обещания, которые дает базовый класс, должны выполнять и подклассы. В случае с Учетной записью, да, это означает, что Deposit вычитание из баланса ипотечного кредита довольно запутано. Один из способов решения этой проблемы - если вы считаете, что баланс - это разница между суммой, которую должен вам клиент, и тем, сколько вы ему должны. (Я примерно на 54% уверен, что это первоначальное значение слова «баланс».) Положительный баланс может означать, что у клиента есть деньги, а отрицательный - может означать, что он должен деньги. Если вы не можете решить эту проблему, чтобы можно было обращаться с двумя учетными записями одинаково, тогда они не должны быть связаны - или, по крайней мере, метод «Депозит» не должен быть определен в базовом классе.

Что касается DIP, то на самом деле он не упоминает, что он не предназначен для применения ко всем строчкам кода. В конце концов у вас есть где-нибудь есть конкретный класс. Дело в том, чтобы ограничить зависимость от специфики этих конкретных классов разделами кода, которые абсолютно должны знать о них, и сохранить как можно меньшее количество и размер этих разделов. Это означает использование как можно более универсального интерфейса, насколько это возможно. Например, если вам не нужны все гарантии, которые дает List (порядок перечисления, наличие дубликатов, нули и т. Д.), Вы можете вместо этого объявить _birthdays как IEnumerable. Если конструктор - единственное, что знает, что ваш IEnumerable на самом деле является списком, то вы более или менее придерживаетесь этого принципа.

(Однако будьте осторожны: это не означает, что вы можете придерживаться DIP, просто объявив все как Object и понизив при необходимости. был предоставлен; вы пытаетесь получить более конкретный.)

person cHao    schedule 17.12.2012
comment
Как насчет ситуации, когда они оба будут вносить положительные числа, но один также войдет в БД. это тоже ломает LSP? при переопределении всегда запускается другой код! пожалуйста, объясните этот момент. - person Royi Namir; 17.12.2012
comment
@RoyiNamir: На самом деле это не проблема. LSP на самом деле ничего не говорит о поведении, которое базовый класс не демонстрирует; при отсутствии обещания не предоставлять их подкласс может обеспечивать дополнительное поведение. Например, если по какой-то тупой причине указано, что Deposit никогда ничего не регистрирует, тогда вы нарушили бы LSP, если бы он регистрировал. Но при отсутствии этой спецификации вы, вероятно, будете в порядке, если войдете в дополнение ко всем прочим вещам, которые делает Deposit базового класса. Если вы можете обращаться с производным точно так же, как с базой, без каких-либо странностей, LSP, вероятно, будет удовлетворен. - person cHao; 17.12.2012
comment
так что LSP касается замены, пока остается предварительно определенный-обязательный список .... что это такое? Вам решать. правильно ? - person Royi Namir; 17.12.2012
comment
@RoyiNamir: В основном. Вы решаете в огромной степени, когда определяете контракт базового класса. Но если существуют правила, которые применяются как следствие определенного вами поведения, или если базовый класс имеет свой собственный базовый класс со своими собственными правилами, они также применяются. - person cHao; 18.12.2012

Если он будет в ctor - мне придется отправлять его каждый раз, когда я использую класс. Так что мне придется оставить его при вызове класса BirthdayCalculator. это нормально делать это так?

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

Я могу утверждать, что после исправления все еще - IList<Birthday> _дни рождения не должно быть там (день рождения в IList) - но должно быть как IList<IBirthday>. Я прав ?

Это зависит. День рождения - это просто объект данных? Есть ли в этом какая-то логика? Если это просто набор свойств / полей, то интерфейс излишний.

Если у дня рождения есть логика, я бы выбрал IEnumerable<IBirthday>. List<Birthday> нельзя присвоить IList<IBirthday>, поэтому у вас могут возникнуть проблемы. Если вам не нужно манипулировать им, придерживайтесь IEnumerable.


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

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

Изменить

Другие отметили, что вы можете сделать _currentBalance в MortageAccount отрицательным значением, а затем прибавить к нему. Если вам это легче понять, тогда дерзайте. Однако внутреннее устройство того, как MortageAccount управляет своим балансом, не важно, если GetBalance или какой-либо другой метод, который вы используете для его проверки / отображения, обрабатывает его правильно.

person cadrell0    schedule 17.12.2012

№1

  • Да, вам придется вызывать это каждый раз, когда вы создаете объект. Здесь фабрики и Инверсия управления пригодится, чтобы удалить всю необходимую сантехнику.

  • Да, вам следует использовать интерфейс IBirthday.

# 2

Ваша логика здесь ошибочна. Когда вы вносите деньги на счет, вы добавляете на него деньги. На ипотечном счете нет денег (положительный баланс), и вы не снимаете деньги с него путем внесения депозита. На ипотечном счете отрицательное сальдо, и при внесении на счет сальдо уменьшается (добавляется к отрицательному сальдо).

person Greg B    schedule 17.12.2012
comment
"Yes, you should be using an IBirthday interface" - Возможно, в этом нет необходимости. Birthday похоже, что это скорее тип значения, а не сущность. Это зависит от того, что Birthday на самом деле в этой области, какие функции он может иметь и т. Д. Даже если это полноценная модель сущности, часто нет ничего плохого в зависимости от моделей предметной области. В конце концов, они лежат в основе. Пока все стрелки зависимостей указывают внутрь к ядру, а не наружу к деталям, все в порядке. - person David; 17.12.2012

Что касается LSP: на мой взгляд, LSP - это в основном (публичный) контракт иерархии. У вас есть счет (объект), вы можете зачислить / списать на него деньги (договор). Для ипотечного счета Кредит приведет к вычету, а Дебет - к добавлению. Для чекового счета все наоборот.

Важно то, что для данной учетной записи вы всегда можете попросить ее выполнить действия по кредиту / дебетованию и что все подклассы объекта учетной записи будут поддерживать эти операции.

person Evgeny Lukashevich    schedule 17.12.2012

Инверсия зависимостей:

Если он будет в ctor - мне придется отправлять его каждый раз, когда я использую класс. Так что мне придется оставить его при вызове класса BirthdayCalculator. это нормально делать это так?

Он нужен не при использовании объекта, а при конструировании объекта BirthdayCalendar. Перемещение IList<Birthday> в интерфейс построения гарантирует, что все зависимости BirthdayCalendar будут такими же, как и зависимости вызывающего объекта (т. Е. Никаких новых), и именно этого мы пытаемся достичь.

Я могу утверждать, что после исправления все еще - IList<Birthday> _birthdays не должно быть (Birthday в IList) - но должно быть как IList<IBirthday>. Я прав ?

Это не связано напрямую с принципом инверсии зависимостей. Вы передали зависимость вызывающей стороне и теперь спрашиваете о деталях этой зависимости. Было бы неплохо использовать IBirthday для доступа к внутренним компонентам вашего списка вместо Birthday, но это вопрос ваших интерфейсов, не связанных с инверсией зависимостей.

каждый переопределенный метод при переопределении будет выполнять другой код, поэтому его никогда нельзя будет заменить на 100%! в определении НЕ говорилось о том, что «логика должна продолжаться, как в базовом классе (всегда вносить (добавлять) положительные числа)»

Пример: Что, если и CheckingAccount класс, и MortgageAccount класс вносили положительные числа, но MortgageAccount также записывались в базу данных? он все еще ломается LSP? Какая граница для LSP обрывается / не тормозит? определение должно определять, что это за граница. и это ничего не говорит об этом.

Принцип подстановки Лискова применим к отношению базовый класс к производному классу. В вашем примере речь идет о двух классах, унаследованных от общей базы, поэтому ваше отношение является производным классом1 и производным классом2. Это полная противоположность тому, о чем говорит LSP, именно по указанным вами причинам - логика различна в обоих производных классах. LSP - это мера действительности установленного вами наследования. Если вы можете ответить положительно на вопрос можно ли прозрачно использовать объект моего производного класса везде, где используется мой объект базового класса?, вы не нарушаете LSP. В противном случае вы его нарушите.

person SomeWittyUsername    schedule 17.12.2012

Что касается первого пункта, вполне нормально использовать конкретный низкоуровневый класс в качестве частной детали реализации другого класса; код мог бы быть больше SOLID, если бы кто-то сказал IList<Birthday> _birthdays = new List<Birthday>;, но производительность, вероятно, была бы немного лучше с List<Birthday> _birthdays = new List<Birthday>. Если вы хотите использовать IBirthday, а не Birthday, это тоже хорошо, если вы используете IBirthday каждый раз, вы будете склонны использовать Birthday, кроме случаев вызова конструкторов или статических методов. Обратите внимание, что вы должны быть последовательны в том, используете ли вы Birthday против IBirthday и List<T> против IList<T>.

Обратите внимание, что частные члены не обязаны придерживаться тех же правил, что и публичные члены. Если бы тип принял параметр типа List<T>, это заставило бы внешний код использовать этот тип. Переход с List<T> на IList<T>, вероятно, потребует внесения множества изменений во множество различных сборок и сделает старые версии сборок несовместимыми с новыми. Напротив, если у класса есть частный член типа List<T>, который никогда не используется совместно с внешним кодом, последствия его изменения для использования IList<T> будут ограничены внутри самого класса и не окажут никакого влияния на потребителей этого класса.

person supercat    schedule 17.12.2012