Как я могу избежать нарушения LSP в этом примере? С#

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

public abstract class Message 
{
    protected int m_id;
    protected bool m_localized;
    protected string m_metaData;

    public int GetID() { return m_id; }
    public bool GetLocalized() { return m_localized; }
    public string GetMetadata() { return m_metaData; }
}

Затем у меня есть еще два класса, которые наследуются от Message, например:

public class ClassicMessage : Message
{
     private string m_title;
     private string m_content;

     public void SetTitle(string title) { m_title = title; }
     public void SetContent(string content) { m_content = content; }
     public string GetTitle() { return m_title; }
     public string GetContent() { return m_content; }
}

public class MessageWithCustomContent : Message
{
     private List<CustomContent> m_content;

     public MessageWithCustomContent() 
     {
          m_content = new List<CustomContent>();
     }

     public List<CustomContent> GetContent()
     {
          return m_content;
     }

     public CustomContent GetContentEntry(int id) 
     {
          return m_content.find(x => x.ID.Equals(id));
     }
}

public class CustomContent
{
     private int m_id;
     public int ID { get; set { m_id = value; } }
     private string m_body;
     public string Body { get { return m_body; } set { m_body = value; }
     private Image m_image; 
     public Image Image { get { return m_image; } set { m_image = value; } }
}

Как в таком случае унифицировать интерфейс приложения, если производные классы имеют похожие методы, но эти методы возвращают разные типы? (даже когда методы пытаются сделать то же самое)

Я знаю, что в примере, в котором я нарушаю принцип замещения Лискова и принцип открытого/закрытого, как лучше всего обойти это?

Спасибо за вашу помощь!

Изменить:

Для большей ясности я пытаюсь создать общий интерфейс для управления всеми возможными сообщениями в качестве базового «Сообщения», потому что я хочу избежать использования typeof в потребительском классе.

Например:

if(message is MessageWithCustomContent) 
{
         // do something with the contents.
}
else if(message is MessageWithCustomContent) 
{
       // do another thing with the contents.
}
etc...

person Camilo    schedule 05.02.2017    source источник


Ответы (3)


Вы можете изменить Message на общий, а буква T будет указывать тип возвращаемого содержимого. См. пример ниже.

Редактировать Вы можете использовать «IMessage» и «Message: IMessage» в качестве основы. Затем вы сможете создать список IMessage следующим образом:

var messages = new List<IMessage>
{
    new ClassicMessage(),
    new MessageWithCustomContent()
};
foreach (var message in messages)
{
    message.GetContent();
}

Ниже показано, как можно реализовать IMessage.

public interface IMessage
{
    int GetID();
    bool GetLocalized();
    string GetMetadata();
    object GetContent();
}

public abstract class Message<T> : IMessage
{
    protected int m_id;
    protected bool m_localized;
    protected string m_metaData;

    public int GetID() { return m_id; }
    public bool GetLocalized() { return m_localized; }
    public string GetMetadata() { return m_metaData; }
    object IMessage.GetContent()
    {
        return GetContent();
    }
    public abstract T GetContent();
}

public class ClassicMessage : Message<string>
{
    private string m_title;
    private string m_content;

    public void SetTitle(string title) { m_title = title; }
    public void SetContent(string content) { m_content = content; }
    public string GetTitle() { return m_title; }
    public override string GetContent()
    {
        return m_content;
    }
}

public class MessageWithCustomContent : Message<List<CustomContent>>
{
    private List<CustomContent> m_content;

    public MessageWithCustomContent()
    {
        m_content = new List<CustomContent>();
    }

    public CustomContent GetCustomContent(int id)
    {
        return null;
    }

    public override List<CustomContent> GetContent()
    {
        return m_content;
    }
}

public class CustomContent
{
    private int m_id;
    public int ID { get; set; }
    private string m_body;

    public string Body
    {
        get { return m_body; }
        set { m_body = value; }
    }
}
person Coolio    schedule 05.02.2017
comment
Это хорошая идея, но тогда я не могу создать единый список сообщений для управления ими, потому что мне нужно указать общий тип в объявлении. - person Camilo; 05.02.2017
comment
Хороший! я сам пришел к очень похожей реализации. Но похоже, что подход, который я использую для этой проблемы, неправильно разработан, потому что тогда нет способа обрабатывать содержимое, не проверяя, является ли это списком или строкой. Я все равно отмечу ваш ответ как правильный. Большое спасибо за Вашу помощь! - person Camilo; 06.02.2017

Ниже я объясню, как вы ломаете LSP, но до того, как я это сделаю, вы на самом деле не выполняете никакого наследования. Да, вы объявляете, что ваши классы наследуются, но вы на самом деле ничего не наследуете. Поэтому, прежде чем изучать LSP, возможно, вам нужно сначала разобраться с наследованием.


Как узнать, нарушаю ли я LSP?

Чтобы не сказать, что ваш Message класс был таким, обратите внимание на виртуальные и абстрактные методы:

public abstract class Message 
{
    protected int m_id;
    protected bool m_localized;
    protected string m_metaData;

    public virtual int GetID() { return m_id; }
    public virtual bool GetLocalized() { return m_localized; }
    public abstract string GetMetadata();
}

Создайте такой список:

var messages = new List<Message>();

Затем добавьте конкретные типы в этот список всех наследуемых типов. Затем сделайте следующее:

foreach(var thisMessage in messages)
{
    var id = thisMessage.GetID();
    var loc = GetLocalized();
    var meta = GetMetadata();
}

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

Причина, по которой этот принцип важен, заключается в том, что может существовать код, который использует Message, как показано в foreach выше, где он обрабатывает все типы полиморфно, и разработчик решает наследовать его следующим образом:

public abstract class BadMessage 
{    
    public override int GetID() 
    { 
        throw new InvalidOperationException
           ("This method is not needed for BadMessage and should not be called"); 
    }
    public override bool GetLocalized() { ... }
    public override string GetMetadata() { ... }
}

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

person CodingYoshi    schedule 05.02.2017

Ну, вам не хватает методов интерфейса в базовом классе. Абстрактные функции, которые реализуются в производных классах. Если вы получите сообщение, не зная, какого оно типа, как бы вы запросили его содержимое? Вы можете добавить в свою базу специфичные для производных методы, но вам придется реализовать исключение not_implemented в виртуальной реализации в базовом классе, чтобы компенсировать все производные, не реализующие его, и добавить обработку исключений. Но тогда вы должны спросить себя: «Действительно ли этот класс производный? Чего я хочу достичь».

person JHBonarius    schedule 05.02.2017