Давайте сначала рассмотрим принцип подстановки Лискова , а затем поговорим об ООП и наследовании.
Сначала давайте поговорим о Абстрактные типы данных .В своей статье она использует концепцию объектов из типов.
Abstact Data Type (ADT) - это описание типа со всеми его операциями и поведения .Все клиенты ADT должны знать, чего ожидать при его использовании.
Вот пример:
Давайте определим Stack
как ADT
Операции: push
, pop
, topElement
, size
, isEmpty
Поведения:
push
: всегда добавляет элемент в начало стека! size
:вернуть количество элементов в стеке pop
: удаляет и элемент с вершины стека.ошибка, если стек пуст topElement
: вернуть верхний элемент в стеке.ошибка, если стек пуст isEmpty
: верните true, если стек пуст, в противном случае ложь
В этот момент мы описали, что такое Stack
с точки зрения егооперации и как это должно вести себя.Мы не говорим ни о правилах, ни о конкретных реализациях.Это делает абстрактный тип данных .
Теперь давайте создадим иерархию типов.В C # и интерфейсы, и классы являются типами.Они отличаются, так как интерфейсы определяют только операции, поэтому в некотором смысле они являются контрактом.Они определяют операции ADT.Обычно люди предполагают, что только классы, которые наследуются друг от друга, определяют иерархию типов.Это правда, что классы, которые наследуются друг от друга, называются Суперкласс или Базовый класс и Подкласс , но с точки зрения Типы у нас есть Супертип и Подтип для обоих интерфейсов и классов, так как они оба определяют типы.
ПРИМЕЧАНИЕ. Для простоты я пропущу проверку ошибок в реализациях методов
// interfaces are types. they define a contract so we can say that
// they define the operations of an ADT
public interface IStack<T> {
T Top();
int Size();
void Push(T element);
void Pop();
bool IsEmpty();
}
// the correct term here for C# whould be 'implements interface' but from
// point of view of ADTs and *Types* ListBasedStack is a *Subtype*
public class ListBasedStack<T> : IStack<T> {
private List<T> mElements;
public int Size() { return mElements.Count; }
public T Top() { mElements(mElements.Count - 1); }
public void Push(T element) { mElements.Add(element); }
public void Pop() { mElements.Remove(mElements.Count - 1); }
public bool IsEmpty() { return mElements.Count > 0; }
}
public class SetBasedStack<T> : IStack<T> {
private Set<T> mElements;
public int Size() { return mElements.Count; }
public T Top() { mElements.Last(); }
public void Push(T element) { mElements.Add(element); }
public void Pop() { mElements.RemoveLast(); }
public bool IsEmpty() { return mElements.Count > 0; }
}
Обратите внимание, что у нас есть два подтипа изто же самое ADT .Теперь давайте рассмотрим тестовый пример.
public class Tests {
public void TestListBasedStackPush() {
EnsureUniqueElementsArePushesToAStack(new ListBasedStack<int>());
}
public void TestSetBasedStackPush() {
EnsureUniqueElementsArePushesToAStack(new SetBasedStack<int>());
}
public void EnsureUniqueElementsArePushesToAStack(IStack<int> stack) {
stack.Push(1);
stack.Push(1);
Assert.IsTrue(stack.Size() == 2);
}
}
И результаты:
TestListBasedStackPush
: Pass TestSetBasedStackPush
: FAIL!
SetBasedStack
нарушает правила для push
: всегда добавляет элемент в начало стека! , поскольку набор может содержать только уникальные элементы, аsecond stack.Push(1)
не будет добавлять новый элемент в стек.
Это нарушение LSP.
Теперь о примерах и иерархиях типов, таких как IAnimal
и Dog
.Когда вы находитесь на правом уровне абстракции , тип должен вести себя так, как ему полагается.Если вам нужен Dog
, используйте Dog
.Если вам нужен IAnimal
, используйте IAnimal
.
Как получить доступ к Bark
, если у вас есть IAnimal
? Вы НЕ !! .Вы находитесь на неправильном уровне абстракции.Если вам нужен Dog
, используйте Dog
.В ролях, если вам нужно.
public class Veterenerian {
public void ClipDogNails(IAnimal animal) { } // NO!
public void ClipDogNails(Dog dog) { } // YES!
}
private Veterenerian mOnDutyVeterenerian;
private List<IAnimal> mAnimals;
public ClipAllDogsNails() {
// Yes
foreach(var dog in mAnimals.OffType<Dog>()) {
mOnDutyVeterenerian.ClipDogNails(dog);
}
// NO
foreach(var animal in mAnimals) {
mOnDutyVeterenerian.ClipDogNails(animal);
}
}
Вам нужно разыграть?Иногда да.Если лучше не делать этого?Да, большую часть времени.
Как вы решаете вышеуказанную проблему?Вы можете сделать Собачий клип своими ногтями.Вы делаете, чтобы добавить метод ClipNails
к IAnimal
и сделать так, чтобы только животные с ногтями реализовали это, а другие подклассы животных оставили этот метод пустым?НЕТ!Поскольку это не имеет смысла на уровне абстракции IAnimal
и , это также нарушает LSP.Также, если вы сделаете это, вы можете позвонить animal.ClipNails()
, и это будет хорошо, но если у вас есть расписание, в котором говорится, что собаки должны подстригать гвозди в пятницу, другие животные в понедельник снова застряли, поскольку вы можете заставить всех животных обрезать свои гвозди, а нетолько собаки.
Иногда объект одного типа должен использоваться объектами другого типа .Некоторые операции не имеют смысла в типе.Этот пример иллюстрирует, как собака не может обрезать ногти.Это должно быть сделано Veterenerial
.
И все же нам нужно работать на уровне IAnimal
абстракции.Все вещи в Veterenerian Clinic
являются животными.Но иногда некоторые operations
должны выполняться на определенных типах из животных , в данном случае Dog
, поэтому нам необходимо отфильтровать животных по их Типу .
Но это проблема, совершенно отличная от приведенного выше примера с Stack
.
Вотпример того, когда приведение не должно использоваться, а код клиента не должен касаться конкретной реализации:
public abstract class Serializer {
public abstract byte[] Serialize(object o);
}
public class JSONSerializer : Serializer {
public override byte[] Serialize(object o) { ... }
}
public class BinarySerializer : Serializer {
public override byte[] Serialize(object o) { ... }
}
public void DoSomeSerialization(Serializer serializer, Event e) {
EventStore.Store(serializer.Serialize(e));
}
DoSomeSerialization
метод не должен заботиться оСериализатор, который передается на него.Вы можете передать любой Serializer
, который соответствует спецификации Serializer
, он должен работать.В этом смысл абстракции с несколькими реализациями.DoSomeSerialization
работает на уровне Serializer
.Мы можем определить Serializer
как ADT.Все классы, которые являются производными от Serializer
, должны соответствовать спецификации ADT, и система работает просто отлично.Здесь нет кастинга, здесь нет необходимости кастовать, так как проблема в другом.