Лучшие практики кодирования — принцип разделения интерфейсов

задняя часть

Принцип разделения интерфейсов

В объектно-ориентированном программировании интерфейсы — очень важное оружие. Интерфейс выражает границу между требованиями клиентского кода и конкретной реализацией требований. Принцип разделения интерфейса утверждает, что интерфейс должен быть достаточно маленьким, а большой и всеобъемлющий контракт (интерфейс) не имеет смысла.

Причины разделения интерфейса

Причины для разделения большого интерфейса на несколько меньших интерфейсов:

① Необходимо изменить интерфейс отдельно

②Клиенту нужно

③ Требования к архитектуре

Нужно отдельно украшать интерфейс

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

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

public interface ICreateReadUpdateDelete<TEntity>
{
    void Create(TEntity entity);
    TEntity ReadOne(Guid identity);
    IEnumerable<TEntity> ReadAll();
    void Update(TEntity entity);
    void Delete(TEntity entity);
}

ICreateReadUpdateDelete — это универсальный интерфейс, который может принимать различные типы сущностей. Клиент должен сначала объявить TEntity, от которого он зависит. Каждая операция в CRUD выполняется соответствующей реализацией интерфейса ICreateReadUpdateDelete, включая реализации декораторов.

Некоторые декораторы применяются ко всем методам, например декоратор ведения журнала. Конечно, декоратор журналов является сквозной задачей.Чтобы избежать повторной реализации в нескольких интерфейсах, вы также можете использовать Аспектно-ориентированное программирование (АОП) для декорирования всех реализаций интерфейса.

public class CrudLogging<TEntity> : ICreateReadUpdateDelete<TEntity>
{
    private readonly ICreateReadUpdateDelete<TEntity> decoratedCrud;
    private readonly ILog log;
    public CrudLogging(ICreateReadUpdateDelete<TEntity> decoratedCrud,
         ILog log)
    {
        this.decoratedCrud = decoratedCrud;
        this.log = log;
    }

    public void Create(TEntity entity)
    {
        log.InfoFormat("Create entity of type {0}", typeof(TEntity).Name);
        decoratedCrud.Create(entity);
    }

    public void Delete(TEntity entity)
    {
        log.InfoFormat("Delete entity of type {0}", typeof(TEntity).Name);
        decoratedCrud.Delete(entity);
    }

    public IEnumerable<TEntity> ReadAll()
    {
        log.InfoFormat("Reading all entities of type {0}", typeof(TEntity).Name);
        return decoratedCrud.ReadAll();
    }

    public TEntity ReadOne(Guid identity)
    {
        log.InfoFormat("Reading  entity of type {0}", typeof(TEntity).Name);
        return decoratedCrud.ReadOne(identity);
    }

    public void Update(TEntity entity)
    {
        log.InfoFormat("Update  entity of type {0}", typeof(TEntity).Name);
        decoratedCrud.Update(entity);
    }
}

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

 public class DeleteConfirm<TEntity> : ICreateReadUpdateDelete<TEntity>
 {
     private readonly ICreateReadUpdateDelete<TEntity> decoratedCrud;
     public DeleteConfirm(ICreateReadUpdateDelete<TEntity> decoratedCrud)
     {
         this.decoratedCrud = decoratedCrud;
     }
     public void Create(TEntity entity)
     {
         decoratedCrud.Create(entity);
     }

     public IEnumerable<TEntity> ReadAll()
     {
         return decoratedCrud.ReadAll();
     }

     public TEntity ReadOne(Guid identity)
     {
         return decoratedCrud.ReadOne(identity);
     }

     public void Update(TEntity entity)
     {
         decoratedCrud.Update(entity);
     }

     public void Delete(TEntity entity)
     {
         Console.WriteLine("Are you sure you want to delete the entity ? [y/n]");
         var keyInfo = Console.ReadKey();
         if(keyInfo.Key == ConsoleKey.Y)
         {
             decoratedCrud.Delete(entity);
         }
     }
 }

Как и в приведенном выше коде, DeleteConfirm изменяет только метод Delete, а остальные методыМетод прямой поддержки(без какого-либо оформления, как прямой вызов декорированного метода интерфейса). Хотя эти сквозные методы ничего не делают, вам все равно нужно реализовывать их один за другим, а также нужно писать тестовые методы для проверки правильности поведения метода, что гораздо более хлопотно, чем метод разделения интерфейса.

Мы можем отделить метод Delete от интерфейса ICreateReadUpdateDelete, что приводит к двум интерфейсам:

 public interface ICreateReadUpdate<TEntity>
 {
     void Create(TEntity entity);
     TEntity ReadOne(Guid identity);
     IEnumerable<TEntity> ReadAll();
     void Update(TEntity entity);
 }

 public interface IDelete<TEntity>
 {
     void Delete(TEntity entity);
 }

Затем просто предоставьте реализацию декоратора подтверждения для интерфейса IDelete:

public class DeleteConfirm<TEntity> : IDelete<TEntity>
{
    private readonly IDelete<TEntity> decoratedDelete;
    public DeleteConfirm(IDelete<TEntity> decoratedDelete)
    {
        this.decoratedDelete = decoratedDelete;
    }

    public void Delete(TEntity entity)
    {
        Console.WriteLine("Are you sure you want to delete the entity ? [y/n]");
        var keyInfo = Console.ReadKey();
        if(keyInfo.Key == ConsoleKey.Y)
        {
            decoratedDelete.Delete(entity);
        }
    }
}

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

Потребности клиента

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

Теперь есть сценарий доступа к текущей теме программы через пользовательский интерфейс настройки, который реализован следующим образом:

public interface IUserSettings
{
    string Theme
    {
        get;
        set;
    }
}
public class UserSettingsConfig : IUserSettings
    {
        private const string ThemeSetting = "Theme";
        private readonly Configuration config;
        public UserSettingsConfig()
        {
            config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
        }

        public string Theme
        {
            get
            {
                return config.AppSettingd[ThemeSetting].value;
            }
            set
            {
                config.AppSettingd[ThemeSetting].value = value;
                config.Save();
                ConfigurationManager.RefreshSection("appSettings");
            }
        }
    }

Клиенты с разными интерфейсами используют одно и то же свойство для разных целей:

public class ReadingController
{
    private readonly IUserSettings userSettings;
    public ReadingController(IUserSettings userSettings)
    {
        this.userSettings = userSettings;
    }

    public string GetTheme()
    {
        return userSettings.Theme;
    }
}

public class WritingController
{
    private readonly IUserSettings userSettings;
    public WritingController(IUserSettings userSettings)
    {
        this.userSettings = userSettings;
    }

    public void SetTheme(string theme)
    {
        userSettings.Theme = theme;
    }
}

В то время как класс ReadingController теперь использует только средство чтения свойства Theme, класс WritingController использует только средство установки свойства Theme. Но из-за отсутствия разделения интерфейсов мы не можем запретить классу WriteController получать данные темы, а классу ReadingController изменять данные темы, что является большой проблемой, особенно последнее.

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

public interface IUserSettingsReader
{
    string Theme
    {
        get;
    }
}
public interface IUserSettingsWriter
{
    string Theme
    {
        set;
    }
}

Классы реализации UserSettingsConfig теперь реализуют интерфейсы IUserSettingsReader и IUserSettingsWriter соответственно.

public class UserSettingsConfig : IUserSettings

=>

public class UserSettingsConfig:IUserSettingsReader,IUserSettingsWriter

Клиенты теперь зависят только от тех интерфейсов, которые им действительно нужны, соответственно:

public class ReadingController
{
    private readonly IUserSettingsReader userSettings;
    public ReadingController(IUserSettingsReader userSettings)
    {
        this.userSettings = userSettings;
    }

    public string GetTheme()
    {
        return userSettings.Theme;
    }
}

public class WritingController
{
    private readonly IUserSettingsWriter userSettings;
    public WritingController(IUserSettingsWriter userSettings)
    {
        this.userSettings = userSettings;
    }

    public void SetTheme(string theme)
    {
        userSettings.Theme = theme;
    }
}

Потребности архитектуры

Другой движущей силой разделения интерфейсов является архитектурный дизайн. В асимметричной архитектуре, например.Режим разделения ответственности команд и запросов(Разделение чтения-записи), цель состоит в том, чтобы помочь вам выполнить некоторые действия по разделению интерфейса.

Сама структура базы данных (таблицы) ориентирована на данные и коллекции, и теперь все основные языки программирования имеют объектно-ориентированную сторону. Ориентированные на данные (сбор) и объектно-ориентированные по своей сути конфликтуют, но в современных системах база данных является неотъемлемой частью. Чтобы решить этуДисбаланс импеданса, ORM (объектно-реляционное отображение). Полностью изолирует базу данных, позволяя нам манипулировать базой данных, как если бы она была объектом. Теперь общепринятой практикой является использование ORM для операций добавления, удаления и модификации и собственного SQL для запросов. Для запросов лучше всего подходят более простые и эффективные (эффективность разработки и эффективность выполнения).

Схематическая диаграмма выглядит следующим образом:

mark

Сборка клиента

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

Несколько реализаций, несколько экземпляров

Предполагая, что интерфейсы IRead, ISave и IDelete имеют свои собственные классы реализации, клиент должен ввести эти три интерфейса одновременно. Это также наиболее часто используемый метод в нашей обычной разработке.На основе комбинированной реализации, какой бы интерфейс ни был необходим, вводится соответствующий интерфейс, что похоже на разработку подключаемых компонентов.

public class OrderController
{
    private readonly IRead<Order> reader;
    private readonly ISave<Order> saver;
    private readonly IDelete<Order> deleter;

    public OrderController(IRead<Order> reader,
        ISave<Order> saver,
        IDelete<Order> deleter)
    {
        this.reader = reader;
        this.saver = saver;
        this.deleter = deleter;
    }

    public void CreateOrder(Order order)
    {
        saver.Save(order);
    }

    public Order GetOrder(Guid orderID)
    {
        return reader.ReadOne(orderID);
    }

    public void UpdateOrder(Order order)
    {
        saver.Save(order);
    }

    public void DeleteOrder(Order order)
    {
        deleter.Delete(order);
    }
}

Единая реализация, единственный экземпляр

Этот путьодин классНаследование и реализация нескольких отдельных интерфейсов в одной реализации может показаться аномальным (цель разделения интерфейсов не в том, чтобы снова унифицировать их в одной реализации). Часто используются для листовых классов реализации интерфейсов, то есть классов реализации, которые не являются ни декораторами, ни адаптерами, а классами реализации, которые выполняют работу. Применение этого метода к листовому классу реализации связано с тем, чтоКонтекст всех реализаций в листовом классе одинаков.. Этот подход часто применяется к классам, которые имеют дело непосредственно с платформами сохраняемости, такими как Entity Framework.

public class CreateReadUpdateDelete<TEntity>:
    IRead<TEntity>,ISave<TEntity>,IDelete<TEntity>
{
    public void Save(TEntity entity)
    {
       
    }
    public IEnumerable<TEntity> ReadAll()
    {
        return new List<TEntity>();
    }
    public void Delete(TEntity entity)
    {
        
    }
}

public OrderController CreateSingleService()
{
    var crud = new CreateReadUpdateDelete<Order>();
    return new OrderController(crud,crud,crud);
}

Анти-шаблон суперинтерфейса

Интерфейсы, полученные путем разделения всех интерфейсов, объединяются в один и тот же интерфейс.распространенные ошибки, эти интерфейсы объединяются в «суперинтерфейс», что сводит на нет преимущества разделения интерфейсов.

public interface CreateReadUpdateDelete<TEntity>:
    IRead<TEntity>,ISave<TEntity>,IDelete<TEntity>
{
    
}

Суммировать

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

Ссылаться на

«Практика гибкой разработки на C#»

автор:CoderFocus
Публичный аккаунт WeChat: