Параметры конструктора LSP C# и защитные предложения

Я читал о принципе замещения Лискова (LSP), и я немного смущен тем, как вы правильно его придерживаетесь. Особенно, когда используются интерфейсы и подклассы.

Например, если у меня есть базовый класс:

public abstract class AccountBase
{
    private string primaryAccountHolder;

    public string PrimaryAccountHolder 
    { 
        get { return this.primaryAccountHolder; } 
        set
        {
            if (value == null) throw ArgumentNullException("value");
            this.primaryAccountHolder = value;
        }
    }

    public string SecondaryAccountHolder { get; set; }

    protected AccountBase(string primary)
    {
        if (primary == null) throw new ArgumentNullException("primary");
        this.primaryAccountHolder = primary;
    }
}

Теперь предположим, что у меня есть две учетные записи, которые наследуются от базового класса. Тот, который ТРЕБУЕТ SecondaryAccountHolder. Добавление защиты null к подклассу является нарушением LSP, верно? Итак, как мне спроектировать свои классы таким образом, чтобы они не нарушали LSP, но для одного из моих подклассов требуется дополнительный владелец учетной записи, а для другого нет?

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

И у меня такой же вопрос с интерфейсами. Если у меня есть интерфейс:

public interface IPrintsSomething
{
    void PrintSomething(string text);
}

Не будет ли нарушением LSP добавление защитного предложения null для текста в любом классе, реализующем IPrintsSomething? Как вы защищаете свои инварианты? Это правильное слово, верно? :п


person Rabid Penguin    schedule 21.12.2016    source источник


Ответы (2)


Вам следует изучить принцип "говорить-не-спрашивать" и разделение команд/запросов. Вы можете начать здесь: https://pragprog.com/articles/tell-dont-ask

Вы должны стараться сообщать объектам, что вы от них хотите; не задавайте им вопросов об их состоянии, примите решение, а потом скажите, что делать.

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

Вместо того, чтобы спрашивать и принимать такие решения:

string holders = account.PrimaryAccountHolder;
if (accountHolder.SecondaryAccountHolder != null)
{
    holders += " " + accountHolder.SecondaryAccountHolder;
}

Скажи это:

string holders = account.ListAllHoldersAsAString();

В идеале вы бы сказали, что вы на самом деле хотите сделать с этой строкой:

account.MailMergeAllAccountHoldersNames(letterDocument);

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

Что касается LSP, хорошо, если есть официально (или неофициально) задокументированный контракт, в котором говорится, что клиенты должны с самого начала проверять наличие null у второго держателя, тогда это нормально. Это нехорошо, но любые исключения нулевого указателя будут ошибкой клиента из-за неправильного использования класса. (Обратите внимание, это неправда, что добавление логического свойства улучшает это, просто оно может быть немного более читабельным, т.е. проверяет ли кто-нибудь IList.IsReadOnly перед записью в него?!).

Однако если вы начали с учетной записи с двойным владельцем, а затем добавили условие, согласно которому второй владелец учетной записи может быть null позже для одиночной учетной записи, то вы изменили договор, и экземпляр одиночной может быть нарушен. существующий код. Если вы полностью контролируете все места, где используются учетные записи, то вам разрешено это делать, если вы меняете общедоступный API, это другое дело.

Но принцип «говори-не-спрашивай» в данном случае позволяет избежать всей проблемы.

person weston    schedule 24.12.2016

Итак, как мне спроектировать свои классы таким образом, чтобы они не нарушали LSP, но для одного из моих подклассов требуется дополнительный владелец учетной записи, а для другого нет?

Выход из этой проблемы состоит в том, чтобы вывести эту изменчивость на поверхность контракта базового класса. Это может выглядеть так (ненужные детали реализации опущены):

public abstract class AccountBase
{
    public string PrimaryAccountHolder 
    { 
        get { … } 
        set { … }
    }

    public string SecondaryAccountHolder
    { 
        get { … } 
        set 
        { 
            …  
            if (RequiresSecondaryAccountHolder && value == null) throw …;
            …  
        } 
    }

    public abstract bool RequiresSecondaryAccountHolder { get; }
}

Тогда вы не нарушаете LSP, потому что пользователь AccountBase может определить, должны они или нет предоставлять значение SecondaryAcccountHolder.

И у меня такой же вопрос с интерфейсами. … Не будет ли нарушением LSP добавление защитного предложения null для текста в любом классе, реализующем IPrintsSomething?

Сделайте проверку очевидной частью контракта интерфейса. Как? Задокументируйте, что разработчик должен проверить значение text для null.

person Ondrej Tucny    schedule 21.12.2016
comment
Для примера AccountBase, как тогда это работает с конструктором? Нарушает ли это LSP наличие конструктора в подклассе, который требует SecondaryAccountHolder, когда конструктор базового класса этого не делает? - person Rabid Penguin; 22.12.2016
comment
Ваш конструктор защищен, поэтому он не имеет значения в отношении публичного контракта класса. Это также кажется излишним, поскольку подкласс может напрямую использовать соответствующие свойства. Если вам действительно нужен конструктор, он может принимать как первичные, так и вторичные учетные записи, но оставить его СУХИМ — избегайте дублирования логики проверки и вместо этого назначайте свойства. - person Ondrej Tucny; 22.12.2016
comment
Но если вы назначаете свойства напрямую, вы не гарантируете, что ваш объект находится в допустимом состоянии. Например, если я просто помещу свойство RequiresSecondaryAccountHolder в базовый класс, это только гарантирует, что свойство не может быть установлено равным нулю, но это не гарантирует, что свойство действительно установлено. Так не потребует ли это использования конструктора в производном классе, чтобы сохранить объект в допустимом состоянии? Нарушает ли LSP наличие разных конструкторов в производных классах? - person Rabid Penguin; 22.12.2016