Как избежать приведения производных типов - нарушение принципа Лискова - PullRequest
0 голосов
/ 11 июля 2019

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

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

public abstract class Animal : IAnimal
{
    public void Move()
    {        
    }
}

public interface IAnimal
{
     void Move();
}

public interface IDog:IAnimal
{
    void bark();
}

public class Dog : IDog
{
    public void Move()
    {

    }

    public void bark()
    {

    }
}

static void Main(string[] args)
{
    Animal animal = null;
    IDog dog = animal as IDog;

    dog.bark(); // can access specialized method

    IAnimal puppy = new Dog();
    puppy.Move(); // can only access generic functions    
}

Как я могу изменить дизайн классов, чтобы получить доступ к методу "лая" без приведения?

Ответы [ 2 ]

5 голосов
/ 11 июля 2019

Краткий ответ: вы не можете и не должны быть в состоянии.

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

Однако, если вы настаиваете на Bark() на IDog, вы не ожидаете, что IDuck сможет получить к нему доступ - у него должен быть метод Quack(). Ни один из них не будет доступен для объектов, пониженных до IAnimal, потому что как вы можете догадаться, является ли это Duck или Dog?


Я опубликую немного более «реальный» пример того, почему вам может понадобиться наследование в программировании, потому что приведенный вами пример является своего рода «книжным примером», и поэтому он также неясен и расплывчат.

using System.Collections.Generic;

namespace ConsoleApp1
{
    public static class DocumentHandling
    {
        public static List<IAccountable> Documents;
        public static dynamic InternalService { get; set; }
        public static dynamic IRS { get; set; }

        public static void HandleDocuments()
        {
            foreach (var document in Documents)
            {
                document.Account();
            }
        }
    }

    public interface IAccountable
    {
        void Account();
    }

    public abstract class Document
    {
        public int DatabaseId { get; set; }
        public string Title { get; set; }

    }

    public abstract class DocumentWithPositions : Document
    {
        public int[] PositionsIds { get; set; }
    }

    public class Invoice : DocumentWithPositions, IAccountable
    {
        public void Account()
        {
            var positions = DocumentHandling.InternalService.PreparePositions(this.PositionsIds);
            DocumentHandling.IRS.RegisterInvoice(positions);
        }
    }

    public class Receipt : DocumentWithPositions, IAccountable
    {
        public void Account()
        {
            Invoice invoice = DocumentHandling.InternalService.ConvertToReceipt(this);
            invoice.Account();
        }
    }
}

Посмотрите, как я могу собрать как Invoice, так и Receipt документы в одном списке (потому что они уменьшены до IAccountable)? Теперь я могу учесть их всех сразу, хотя их конкретные реализации по-разному управляют процессом учета.

0 голосов
/ 20 июля 2019

Давайте сначала рассмотрим принцип подстановки Лискова , а затем поговорим об ООП и наследовании.

Сначала давайте поговорим о Абстрактные типы данных .В своей статье она использует концепцию объектов из типов.

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, и система работает просто отлично.Здесь нет кастинга, здесь нет необходимости кастовать, так как проблема в другом.

...