Принцип замещения Лисков

29 октября 2009 г.

Формулировка №1: eсли для каждого объекта o1 типа S существует объект o2 типа T, который для всех программ P определен в терминах T, то поведение P не изменится, если o2 заменить на o1 при условии, что S является подтипом T.

Формулировка №2: Функции, которые используют ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом.

Короткая версия: Derived classes must be substitutable for their base classes

Примеры

Проверка абстракции на тип

Я уже приводил код проверки абстракции на тип на примере нарушения принципа открытости/закрытости. Теперь мы видим, что класс Repository нарушает еще и принцип замещения Лисков. Дело в том, что внутри класса Repository мы оперируем не только абстрактной сущностью AbstractEntity, но и унаследованными типами. А это значит, что в данном случае подтипы AccountEntity и RoleEntity не могут быть заменены типом, от которого они унаследованы. По определению имеем нарушение.

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

Ошибочное наследование

Проблема

Мы хотим реализовать свой список с интерфейсом IList. Его особенностью будет то, что все записи в нем дублируются.

public class DoubleList<T> : IList<T>
{
    private readonly IList<T> innerList = new List<T>();
 
    public void Add(T item)
    {
        innerList.Add(item);
        innerList.Add(item);
    }
 
    ...  

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

[Fact]
public void CheckBehaviourForRegularList()
{
    IList<int> list = new List<int>();
 
    list.Add(1);
 
    Assert.Equal(1, list.Count);
}
 
[Fact]
public void CheckBehaviourForDoubleList()
{
    IList<int> list = new DoubleList<int>();
 
    list.Add(1);
 
    Assert.Equal(1, list.Count); // fail
}

Поведение списка DoubleList отличается от типичных реализаций IList. Получается, что наш DoubleList не может быть заменен базовым типом. Это и есть нарушение принципа замещения Лисков.

Проблема заключается в том, что теперь клиенту необходимо знать о конкретном типе объекта, реализующем интерфейс IList. В качестве такого объекта могут передать и DoubleList, а для него придется выполнять дополнительную логику и проверки.

Решение

Правильным решением будет использовать свой собственный интерфейс, например, IDoubleList. Этот интерфейс будет объявлять для пользователей поведение, при котором добавляемые элементы удваиваются. Более подробно об этом можно прочитать в дополнении к LSP.

Проектирование по контракту

Есть формальный способ понять, что наследование является ошибочным. Это можно сделать с помощью проектирования по контракту. Бернард Мейер, его автор, сформулировал следующий принцип:

Наследуемый объект может заменить родительское пред-условие на такое же или более слабое и родительское пост-условие на такое же или более сильное. (перефразировано)

Рассмотрим пред- и пост-условия для интерфейса IList. Для функции Add:

  • пред-условие: item != null
  • пост-условие: count = oldCount + 1

Для нашего DoubleList и его функции Add:

  • пред-условие: item != null
  • пост-условие: count = oldCount + 2

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

Другими словами, когда мы используем интерфейс IList, то как пользователи этого базового класса знаем только его пред- и пост-условия. Нарушая принцип проектирования по контракту мы меняем поведение унаследованного объекта.

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

Этот пост входит в серию Принципы проектирования классов (S.O.L.I.D.):


Ссылки

R. Martin: The Liskov Substitution Principle

LosTechies: The Liskov Substitution Principle

Wikipedia: Liskov substitution principle

CodingEfficiency: SOLID – L: Liskov Substitution Principle

DimeCasts: # 92 - Creating SOLID Code: Liskov Substitution Principle

53 комментария:

  1. Я думаю, что под словом "программа" в 1-й формулировке принципа следует понимать не программу как некий законченый программный модуль, а функцию. Иначе это сбивает с толку и затрудняет понимание, не так ли?
    В целом хочу сказать спасибо за популяризацию этих принципов. Оказалось, многие из них я применял интуитивно, не зная об их существовании, но теперь как пелена с глаз :)

    ОтветитьУдалить
  2. Объясни пожалуйста, что значит ослабление или усиление условий в проектировании по контракту?

    ОтветитьУдалить
  3. @efix
    > Я думаю, что под словом "программа" в 1-й формулировке принципа следует понимать не программу как некий законченый программный модуль, а функцию

    Ага, так лучше.

    > Объясни пожалуйста, что значит ослабление или усиление условий в проектировании по контракту?

    Если для функции Add родительского класса справедливо:
    * пред-условие: item != null
    * пост-условие: count = oldCount + 1

    То для дочернего можно использовать:
    * пред-условие: -
    * пост-условие: count = oldCount + 1 && is_changed = true

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

    ОтветитьУдалить
  4. Здравствуйте Александр!!!

    У меня возник один вопрос, возможно не к месту и, возможно, глупый, но все же,не могли бы вы объяснить связь между проектированием по контракту и модульным тестированием , или же это просто два совершенно разных подхода к проектированию классов!
    я недавно разбирался с Проектированием по контракту по книге Бертрана Мейера там ничего о тестировании не говориться!

    ОтветитьУдалить
  5. @pArTiZaN

    Привет.

    Это два подхода к разработке (если под "модульным тестированием" ты подразумеваешь TDD). Их можно применять вместе. Видимо только это их и связывает.

    ОтветитьУдалить
  6. >>то поведение P не измениться

    Исправь, пожалуйста

    Некоторое время назад я обнаружил принципиальное отличие в подходе к сериализации в .NET и Java. Заключается оно в том, что в Java для пометки класса сериализуемым необходимо отнаследовать его от маркерного (пустого) интерфейса (есть и другие подходы к сериализации, но сейчас остановимся именно на этом), поэтому дочерние классы автоматически становятся сериализуемыми. В .NET для этих целей есть атрибут Serializable, который не наследуется в дочерних классах.

    Получается, с одной стороны, Java ведет себя правильнее по Лискоу, с другой, - сериализуемость, доставшаяся по наследству, может принести массу проблем. Так кто прав: .NET или Java? Этот вопрос я попытался прояснить в этом топике на rsdn, но не очень успешно.

    ОтветитьУдалить
  7. @Idsa
    Я думаю, что аттрибут - это более правильный подход. Вообще не рекомендую использование пустых интерфейсов для "пометки" объекта.

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

    ОтветитьУдалить
  8. >>Я думаю, что аттрибут - это более правильный подход.

    Здесь суть не в выборе атрибут/интерфейс. Ведь в .NET есть возможность сделать атрибут наследуемым, но разработчики .NET почему-то сделали "не как в Java", пойдя наперекор принципу Лискоу (ты считаешь, что принцип Лискоу здесь нарушается?). Если развивать эту логику, получается, что любые ненаследуемые метаданные нарушают принцип Лискоу.

    >>Вообще не рекомендую использование пустых интерфейсов для "пометки" объекта.

    Мне тоже не очень нравится этот подход, да и в .NET я его ни разу не видел. Хотя в том же топике я пытался заодно узнать, используют ли маркерные интерфейсы после появления в Java аннотаций, и там привели интересный юзкейс, в котором аннотации (как и наши родные атрибуты) бессильны: "можно делать методы someMethod(MarkerInterface o) — с аннотациями такое не прокатит". Это может пригодиться, если нам нужно вынести зависимость класса от объекта с некоторыми метаданными.

    ОтветитьУдалить
  9. @Idsa
    > ты считаешь, что принцип Лискоу здесь нарушается?
    Если я не могу использовать вместо наследуемого класса интерфейс ISerializable, то по определению нарушается :)

    > Если развивать эту логику, получается, что любые ненаследуемые метаданные нарушают принцип Лискоу

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

    > можно делать методы someMethod(MarkerInterface o) — с аннотациями такое не прокатит

    Не понял этот пример, можешь подробнее?

    ОтветитьУдалить
  10. >>Если я не могу использовать вместо наследуемого класса интерфейс ISerializable, то по определению нарушается :)

    Слушай, точно :) Я в этом направлении не размышлял. Выходит, атрибут C# нарушает принцип замещения Лискоу всегда (хотя тут можно спорить...), а поведение сериализации в Java не определено: наличие маркерного интерфейса не гарантирует сериализацию.

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

    Они и передаются, если в AttributeUsage задан Inherited = true. Я склоняюсь к мысли, что любой ненаследуемый атрибут нарушает принцип замещения Лискоу.

    >>Не понял этот пример, можешь подробнее?

    Допустим, у тебя зависимость "любой объект, поддающийся бинарной сериализации". В Java для этого достаточно получить объект, реализующий интерфейс ISerializable, а в C# - никак.

    ОтветитьУдалить
  11. @Idsa
    > Я склоняюсь к мысли, что любой ненаследуемый атрибут нарушает принцип замещения Лискоу
    Это не так, потому что ты не можешь использовать аттрибут вместо класса. Но ты можешь использовать интерфейс вместо класса. Т.е. аттрибут вообще никак не влияет на нарушение или не нарушение этого принципа.

    > В Java для этого достаточно получить объект, реализующий интерфейс ISerializable, а в C# - никак
    Можно пример кода?

    ОтветитьУдалить
  12. >>Т.е. аттрибут вообще никак не влияет на нарушение или не нарушение этого принципа.

    Я тоже так сначала думал, но ребята с rsdn меня переубедили. Пример:

    ClassA classInstance = new ClassA();
    ClassA derivedClassInstance = new ClassB();

    ClassA помечен атрибутом Serializable, а ClassB - нет.

    Пытаемся сериализовать derivedClassInstance и получаем исключение. Это ли не нарушение принципа замещения Лискоу?

    >>Можно пример кода?

    public class SerializableEntityManager
    {
    private ISerializable _serializableEntity;

    /*Зависимость должна быть бинарно-сериализуемой. В Java это выглядело бы так, а в С# - хз. Ну и в общем подобный подход - аргумент за маркерные интерфейсы как средство задания метаданных*/
    public Serializator(ISerializable serializableEntity)
    {
    _serializableEntity = serializableEntity;
    }
    }

    ОтветитьУдалить
  13. @Idsa
    Если ты реализуешь ISerializable у класса, то любой его наследник будет сериализуем. Т.е. по принципу: вместо наследников мы можем использовать их родителей (в данном случае абстракцию - интерфейс ISerializable).

    Речь не идет о метаданных. Метаданные не участвуют во взаимодействии объектов. Мы оперируем в коде не аттрибутами или названиями методов, а классами, абстрактными классами или интерфейсами.

    ОтветитьУдалить
  14. >>Если ты реализуешь ISerializable у класса, то любой его наследник будет сериализуем. Т.е. по принципу: вместо наследников мы можем использовать их родителей (в данном случае абстракцию - интерфейс ISerializable).

    Погоди, погоди. Это ты сейчас насчет чего? Я же вверху про C# и атрибуты говорил, а не про Java и ISerializable. И то, что ты говоришь, не совсем правда: стандартный механизм "сделать класс несериализуемым, если у него сериализуемый родитель" - выкидывать исключение в каком-то там методе (не помню уже). Так что один из потомков может оказаться несериализуемым (я выше писал про "вероятностную сериализацию" в Java :) ).

    >>Речь не идет о метаданных. Метаданные не участвуют во взаимодействии объектов. Мы оперируем в коде не аттрибутами или названиями методов, а классами, абстрактными классами или интерфейсами.

    Метаданные не участвуют во взаимодействии объектов? Хм... А то, что BinaryFormatter при взаимодействии с сериализуемым объектом проверяет наличие у него атрибута Serializable - это ли не взаимодействие?

    ОтветитьУдалить
  15. @Idsa
    Ты путаешь теплое с мягким. Мы говорим про принцип проектирования, который влияет на дизайн системы. Аттрибуты здесь никакую роль не играют.

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

    Более того, если мысленно объединить эти примеры, и представить логику "Count возвращает 0, если для класса не задан атрибут [ReturnNotZeroCount]", разница между примерами еще более размывается.

    ОтветитьУдалить
  17. Формулировка №2: подтипы должны быть заменяемы базовыми типами.
    По моему наоборот.

    ОтветитьУдалить
  18. И пример с листом мне кажется не корректным. Два класса реализуют свое поведение, наследуясь от интерфейса. Где здесь нарушение? Вот если б DoubleList наследовался от конкретного List, тогда да, а так по-моему все нормально.

    ОтветитьУдалить
  19. @Андрей

    > Формулировка №2: подтипы должны быть заменяемы базовыми типами
    > По моему наоборот

    Забавно.

    > ...по-моему все нормально

    Происходит нарушения контракта IList, этому не нормально.

    ОтветитьУдалить
  20. А где описан контракт IList и в частности его поведение "Count должен увеличиваться на 1 при однократном успешном вызове Add и уменьшаться на 1 при однократном успешном вызове Remove) ?
    Count - кол-во элементов в коллекции. Делать какие либо предположения о нём глупо(как и вообще делать наивные предположения). Нужно узнать кол-во - дёрните св-ва и узнаете. И для DoubleList там будет 2 после одного вызова.

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

    ОтветитьУдалить
  21. @Григорий Перепечко
    В том, то и дело, что нигде не описано, разве что может быть в документации.

    А как бы вы отнеслись к .NET фреймворку, если бы в нем был объект SetList унаследованный от IList, который добавлял в себя элементы два раза?

    Думаю, что после первого знакомства с этой его "особенностью" вы бы поостереглись им пользоваться.

    Но самое главное здесь в другом. Посмотрите код:
    public void Method1(IList myList)
    {
    ///
    }

    Метод принимает на вход IList, и вы ждете от объекта myList его "нормального" поведения, другими словами выполнения контракта интерфейса IList. Если этот контракт будет нарушен одним из наследников этот интерфейса и этого наследника передадут в метод Method1, то вам придется внутри Method1 делать даункаст, чтобы проверить подтип.

    ОтветитьУдалить
  22. > Формулировка №2: подтипы должны быть заменяемы базовыми типами
    > По моему наоборот

    Андрея смутила русская википедия. Там действительно пурга написана:
    SOLID (объектно-ориентированное программирование)
    Принцип подстановки Лисков

    ОтветитьУдалить
  23. Все хорошо, но уберите лишний мягкий знак в "поведение P не измениться"

    ОтветитьУдалить
  24. Спасибо, поправил.

    ОтветитьУдалить
  25. Дмитрий Полянин11 марта 2014 г. в 00:32

    Что-то я не пойму.
    Допустим у нас есть два класса.
    1. Button
    2. ImageButton
    и второй это наследник первого


    и в програме используется везде ImageButton


    И если я заменю ImageButton везде на Button
    то программа явно перестанет работать или будет работать по другому, так как у Button-a нету иконки. то есть как минимум пропадут все иконки с кнопок, либо вообще перестанет компилироваться в тех местах где мы указываем иконки в кнопке.

    ОтветитьУдалить
  26. Дмитрий, у Button может быть еще 10-20 наследников.

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

    Дело в том, что чем более абстрактный класс вы используете, тем меньше система знает обо всех наследниках класса Button. Почему нам надо скрывать всех наследников?

    Дело в том, что:

    1. наследники появляются и исчезают, из-за этого приходится менять код. Если информации о наследниках нет (в методы передаются только Button), то добавить/удалить наследника ничего не стоит.

    2. допустим у вас есть метод IsButtonVisible(ImageButton button), который принимает ImageButton и проверяет видима кнопка или нет. Для проверки видимости используются только свойства базового класса. У вас появляется второй наследник класса Button, например, 3dButton, которой нужна такая же функция проверки на видимость. Что делать? Можно создать еще один метод IsButtonVisible(3dButton button). С другой стороны можно сделать универстальный метод IsButtonVisible(Button button), он будет принимать _любого_ наследника класса Button и корректно работать.

    ОтветитьУдалить
  27. Бред сивой кобылы тут написан. Автор не понимает LSP.

    ОтветитьУдалить
  28. >> Формулировка №2: подтипы должны быть заменяемы базовыми типами
    Не понимаю, как такое может быть, ведь у поддтипов могут быть методы, которых нет в базовом типе. В этом случае при замене более специфичного типа базовым получим ошибку компиляции, если такой метод использовался в коде программы.

    А вот наоборот уже получается логично. Если есть использование объекта базового класса, то при замене его объектом подкласса, все должно попрежнему работать корректно.

    ОтветитьУдалить
  29. Илья, это действительно так.

    Рассмотрим ситуацию, когда у нас есть Volatile Dependency, которая закрыта абстракцией. Я думаю тут очевидно, что надо использовать абстракцию, а не реализацию.



    Другая ситуация, у нас есть доменный объект и у него несколько наследников. В любом случае будет код, который использует конкретных наследников, но весь остальной код проекта не должен про них знать. Этот пример можно увидеть, если вы передадите в метод List list и внутри метода будете использовать параметр list только в foreach. Тогда Resharper предложит вам заменить List на IEnumerable, чтобы уйти как можно дальше от конкретной реализации в сторону приемлемой абстракции.

    ОтветитьУдалить
  30. Александр, прошу прощения, я не очень понял ваш первый пример, т.к. работаю в основном с java.


    Что касается второго примера, то вы привели конкретный случай того, как решарпер предлагает делать рефакторинг. Допустим, что метод, в который мы передали List, использует специфический метод этого List, которого нет в IEnumerable. Тогда очевидно, что его нельзя заменить на IEnumerable, т.к. будет ошибка компиляции. Правильно я понимаю, что этот участок кода противоречит обсуждаемому принципу Лисков?

    ОтветитьУдалить
  31. Дмитрий Дорощук15 января 2015 г. в 04:41

    Некорректная 2я формулировка: подтипы должны быть заменяемы базовыми типами.
    Должно быть наоборот: базовые типы должны быть заменяемы подтипами без любых последствий для модуля(программы)

    ОтветитьУдалить
  32. Это как? Просто интересно на пример посмотреть и суть такой подстановки.

    ОтветитьУдалить
  33. Дмитрий Дорощук16 января 2015 г. в 19:35

    Перечитал первое определение. Считаю его тоже некорректным.

    https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B8%D0%BD%D1%86%D0%B8%D0%BF_%D0%BF%D0%BE%D0%B4%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B8_%D0%91%D0%B0%D1%80%D0%B1%D0%B0%D1%80%D1%8B_%D0%9B%D0%B8%D1%81%D0%BA%D0%BE%D0%B2 :
    В последующей статье[2] Лисков кратко сформулировала свой принцип следующим образом:
    Пусть q(x) является свойством, верным относительно объектов x некоторого типа T . Тогда также должно быть верным q(y) для объектов y типа S , где S является подтипом типа T.

    Роберт С. Мартин определил[3] этот принцип так:
    Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.

    Если мы заменим экземпляром класса Object экземпляр класса String, то -logInFile(object) выпадет в креше.

    Вообще сам по себе принцип не более чем руководство к наследованию: если Вы наследуетесь от "НаземныеТранспортныеСредства" не получится ли у Вас при вызове функции "поехали" вылететь в стратосферу

    ОтветитьУдалить
  34. Забавно, но у этого принципа есть строгое математическое доказательсво, к сожалению, вы его не поняли. Видимо статья не достаточно показывает применимость. Попробуйте поискать в других источниках.

    Если вам так нравятся определения, то посмотрите http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod на "Derived classes must be substitutable for their base classes"

    ОтветитьУдалить
  35. Дмитрий Дорощук16 января 2015 г. в 23:11

    Александр, если ссылка, которую Вы кинули является для Вас авторитетной, то, пожалуйста, перечитайте еще раз. Будь там by предлог, то вы бы перевели верно, однако там for и суть меняется. Если перейти по ссылке и далее по ссылке LSP, то откроется пдфка и уже на второй странице будет тезис

    ОтветитьУдалить
  36. Дмитрий Дорощук16 января 2015 г. в 23:16

    Илья, посмотрите мое сообщение ниже, Александр просто некорректно перевел статью.

    ОтветитьУдалить
  37. Дмитрий, спасибо большое, что аргументировано поправили Александра. И спасибо, что меня упомянули, а то я уже думал, что с ума начинаю сходить :)

    ОтветитьУдалить
  38. Дмитрий Дорощук17 января 2015 г. в 13:07

    Илья, без проблем =) Странно, что раньше никто в более оживленную полемику не вступил...

    ОтветитьУдалить
  39. > Странно, что раньше никто в более оживленную полемику не вступил
    Может потому что в этом нет ошибки и вы ее надумали?

    ОтветитьУдалить
  40. > Вы кинули является для Вас авторитетной

    Ну это Боб Мартин, который все эти принципы и систематизировал. Т.е. да, для меня эта ссылка авторитетна :)

    Дмитрий, на самом деле мой перевод правильный. На всякий случай уточнил у лингвиста, не сошел ли я с ума, но не суть.

    Самое главное в этом принципе, что при наследовании должны соблюдаться контракты. Более подробно рекомендую посмотреть в презентации http://www.slideshare.net/etyumentcev/solid-42457729

    ОтветитьУдалить
  41. Дмитрий Дорощук17 января 2015 г. в 13:24

    Александр, это уклонение от моего прежнего поста, пожалуйста, прокоментируйте: FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES WITHOUT KNOWING IT.

    ОтветитьУдалить
  42. Это тоже самое вид сбоку, не вижу противоречий. Если бы так не было, то полиморфизм невозможно было корректно использовать.

    ОтветитьУдалить
  43. Дмитрий Дорощук17 января 2015 г. в 13:31

    Думаю люди со знанием английского сделают свои выводы... На этом думаю нет смысла продолжать полемику. Каждый останется при своем мнении. Удачи.

    ОтветитьУдалить
  44. Дмитрий, мне кажется я понял про что вы мне хотите сказать. На счет определение про "FUNCTIONS THAT USE POINTERS OR REFERENCES..." это действительно так, потому что в противном случае контракт будет нарушен и использовать такую подстановку будут небезопасно.
    Я уверен, что мы с вами правильно понимаем суть этого принципа и его практическое приминение. Вся проблема в его "короткой" формулировке. Я понял, что она может взрывать мозг без контекста и пониматься двояко.


    Пожалуй добавлю более длинное для начала, а корокое оставлю в английской формуляровке.


    Спасибо, что обратили на это внимание, надеюсь, что никто не попал в заблуждение из-за такой интерпретации :)

    ОтветитьУдалить
  45. Дмитрий Дорощук17 января 2015 г. в 14:26

    Александр, рад что мы пришли хоть к какому-то консенсусу =)
    Прошу Вас обратить внимание также на первое определение: "если o1 заменить на o2" не корректно. Для соответствия определения принципу замещения Барбары Лисков, нужно поменять местами: "если о2 заменить на о1". Просьба, если не согласны, прочитайте свое определение (новое второе) пару раз: я уверен полемика не потребуется =)

    ОтветитьУдалить
  46. Да, действительно. Я сам эти определения как 5 лет назад написал, так и не вдавался в них сильно. Сам читал про это всё только на английском и в голове видимо сложилось правильно, а по-русски сформулировать не так. Сейчас поправил.

    ОтветитьУдалить
  47. Дмитрий Дорощук17 января 2015 г. в 14:36

    Александр, рад, что донес все же информацию) Жалко, что долго пришлось дискутировать, если сейчас прочитаете мое первое и второе сообщение, то увидите, что дальнейшая беседа не имела смысла =)

    ОтветитьУдалить
  48. Судя по результату имела :) Не знаю куда на вас сослаться в интернете, поэтому написал просто имя https://twitter.com/alexanderbyndyu/status/556383999208484864 (фамилия же не склоняется?)

    ОтветитьУдалить
  49. Дмитрий Дорощук17 января 2015 г. в 14:47

    =) ну я в твиттере не сижу) если ссылку в комменте вставите https://vk.com/tcdiamond , буду признателен) может когда предложение об интересной вакансии поступит благодаря вашему посту =)

    ОтветитьУдалить
  50. Хочу отметить, что на конструкторы данное ограничение не распространяется. Подробнее - здесь:
    http://stackoverflow.com/questions/5490824/should-constructors-comply-with-the-liskov-substitution-principle

    ОтветитьУдалить
  51. Спасибо за ссылку, посмотрел что там пишут.

    > на конструкторы данное ограничение не распространяется



    О каком ограничении идет речь?

    ОтветитьУдалить
  52. Ну, собственно о принципе замещения Лисков. Сигнатуры конструкторов могут быть абсолютно разными.

    ОтветитьУдалить

Моя книга «Антихрупкость в IT»

Как достигать результатов в IT-проектах в условиях неопределённости. Подробнее...