Интерфейс или абстрактный класс для Null object pattern?

216
26 марта 2022, 06:10

Разрабатываю библиотеку по работе со схемой, где доменная логика следующая:

  • есть схема;
  • схема может содержать либо таблицу или картинку, но не оба контента одновременно.

Если разработчик, который будет использовать мою либу попытаеться обратиться к таблице (Scheme.Table.Rows), а у схемы ее нет, то вместо NullReferenceException, я хочу, чтобы выбрасывалась более внятная ошибка. И для было решено реализовать паттерн Null Object.

Интерфейс

Сейчас это сделано через интерфейс и вот как это выглядит в проекте:

public class Scheme
{
    public ITable Table { get; }
    public IPicture Picture { get; }
    /// <summary>
    /// Creates a new <see cref="Scheme"/> instance with table content.
    /// </summary>
    public Scheme(ITable table)
    {
        Table = table;
        Picture = new NoPicture();
    }
    /// <summary>
    /// Creates a new <see cref="Scheme"/> instance with picture content.
    /// </summary>
    public Scheme(IPicture picture)
    {
        Table = new NoTable();
        Picture = picture;
    }
}
public interface ITable
{
    IEnumerable<Row> Rows { get; }
}
public class Table : ITable
{
    public IEnumerable<Row> Rows { get; }
}
public class NoTable : ITable
{
    public IEnumerable<Row> Rows => throw new Exception("Scheme does not contain a table.");
}
public interface IPicture
{
    Image Image { get; }
}
public class Picture : IPicture
{
    public Image Image { get; }
}
public class NoPicture : IPicture
{
    public Image Image => throw new Exception("Scheme does not contain a picture.");
}

Абстрактный класс

А вот как бы это выглядело на абстрактных классах:

public class Scheme
{
    public AbstractTable Table { get; }
    public AbstractPicture Picture { get; }
    /// <summary>
    /// Creates a new <see cref="Scheme"/> instance with table content.
    /// </summary>
    public Scheme(AbstractTable table)
    {
        Table = table;
        Picture = new NoPicture();
    }
    /// <summary>
    /// Creates a new <see cref="Scheme"/> instance with picture content.
    /// </summary>
    public Scheme(AbstractPicture picture)
    {
        Table = new NoTable();
        Picture = picture;
    }
}
public abstract class AbstractTable
{
    public abstract IEnumerable<Row> Rows { get; }
}
public class Table : AbstractTable
{
    public override IEnumerable<Row> Rows { get; }
}
public class NoTable : AbstractTable
{
    public override IEnumerable<Row> Rows => throw new Exception("Scheme does not contain a table.");
}
public abstract class AbstractPicture
{
    public abstract Image Image { get; }
}
public class Picture : AbstractPicture
{
    public override Image Image { get; }
}
public class NoPicture : AbstractPicture
{
    public override Image Image => throw new Exception("Scheme does not contain a picture.");
}

ВОПРОС

Это очередной случай, когда одно и тоже можно реализовать с помощью интерфейса или абcтрактного класса. Но с точки зрения логики и дизайна, что правильнее?

Я обычно использую абстрактный класс, чтобы вынести общий код в отдельное место и самое главное, чтобы отношение между сущностями было типа IS, но что-то затрудняюсь сказать, что NoTabel is AbstractTable.

Answer 1

Для решения этой задачи

Если разработчик, который будет использовать мою либу попытаеться обратиться к таблице (Scheme.Table.Rows), а у схемы ее нет, то вместо NullReferenceException, я хочу, чтобы выбрасывалась более внятная ошибка.

не нужно использовать паттерн Null Object. Достаточно бросать исключения в свойствах:

public class Scheme
{
    private readonly Table table;
    private readonly Picture picture;
    public Table Table => table ?? throw new Exception("Scheme does not contain a table.");
    public Picture Picture => picture ?? throw new Exception("Scheme does not contain a picture.");
    // со слов автора, такое свойство имеется
    public bool HasTable => table != null;
    public Scheme(Table table)
    {
        this.table = table ?? throw new ArgumentNullException(nameof(table));
    }
    public Scheme(Picture picture)
    {
        this.picture = picture ?? throw new ArgumentNullException(nameof(picture));
    }
}
public class Table
{
    public override IEnumerable<Row> Rows { get; }
}
public class Picture
{
    public override Image Image { get; }
}

Если нужна абстракция зависимостей от Table и Picture, добавляем интерфейсы:

public class Scheme
{
    ...
    public Scheme(ITable table) { ... }
    public Scheme(IPicture picture) { ... }
}
public interface ITable { ... }
public class Table : ITable { ... }
public interface IPicture { ... }
public class Picture : IPicture { ... }

Есть несколько реализаций интерфейсов с общим кодом? Добавляем абстрактный класс:

public abstract class AbstractTable : ITable { ... }
public class Table : AbstractTable { ... }
public class AbstractPicture : IPicture { ... }
public class Picture : AbstractPicture { ... }
Answer 2

Я думаю что интерфейсами будет правильнее и вот почему.

Интерфейсы изначально придумывались как промежуточный объект между двумя функциональными объектами.

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

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

READ ALSO
Совместить объекты на разных Canvas Unity UI

Совместить объекты на разных Canvas Unity UI

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

90
Загрузка картинок в базу данных на Code First

Загрузка картинок в базу данных на Code First

Я вот попытался сделать проект на Code First, позволяющий пользователю загружать картинки в базу данныхСоздал классы моделей и класс контекста...

101
Прокси и Firebase Xamarin

Прокси и Firebase Xamarin

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

85
Не выходит из цикла while C#

Не выходит из цикла while C#

Код до этого работал, перестал после обновления на VS 2019

109