Содержание
Принцип разделения интерфейсов (ISP) — это один из пяти принципов SOLID, сформулированный следующим образом: клиенты не должны реализовывать методы, которые они не используют. Что означает ISP и как он реализуется в C# — рассмотрим в этой части.
Принцип разделения интерфейсов (ISP) в C#
Ключевая идея ISP: вместо одного большого интерфейса предпочтительны многочисленные маленькие интерфейсы, основанные на группах методов
Чтобы разобраться в принципе разделения интерфейсов, разберем его формулировку не две части:
- Во-первых, ни один класс не должен быть вынужден реализовывать какие-либо методы интерфейса, которые он не использует.
- Во-вторых, вместо создания больших или, можно сказать, громоздких интерфейсов создавайте несколько небольших интерфейсов, чтобы клиенты думали только о методах, которые они хотят реализовать.
Рассмотрим применение принципа разделения интерфейсов в C# на примере.
Пример использования принципа разделения интерфейсов (ISP) в C#
Пример без использования принципа разделения интерфейсов (ISP) в C#
Допустим, мы разрабатываем интерфейс для работы с данными в БД. В результате мы получаем вот такой интерфейс:
public interface ICrudService<T>
{
T Create(T entity);
T Read(int id);
T Update(T entity);
void Delete(int id);
void Validate(T entity);
void SendNotification(T entity, string message);
}
Теперь реализуем этот интерфейс в нескольких классах:
public class TasksRepository<T> : ICrudService<T>
{
public T Create(T entity)
{
//код создания задачи
}
public void Delete(int id)
{
//код удаления задачи
}
public T Read(int id)
{
//код получения задачи
}
public void SendNotification(T entity, string message)
{
//код отправки уведомления
}
public T Update(T entity)
{
//код обновления задачи
}
public void Validate(T entity)
{
//код проверки задачи
}
}
public class DataRepository<T> : ICrudService<T>
{
public T Create(T entity)
{
//код создания данных
}
public void Delete(int id)
{
//код удаления данных
}
public T Read(int id)
{
//код чтения данных
}
public void SendNotification(T entity, string message)
{
//этот метод нам не нужен
throw new NotImplementedException();
}
public T Update(T entity)
{
//этот метод нам не нужен
throw new NotImplementedException();
}
public void Validatw(T entity)
{
throw new NotImplementedException();
}
}
public class MessageRepository<T> : ICrudService<T>
{
public T Create(T entity)
{
//код создания данных
}
public void Delete(int id)
{
//этот метод нам не нужен
throw new NotImplementedException();
}
public T Read(int id)
{
//этот метод нам не нужен
throw new NotImplementedException();
}
public void SendNotification(T entity, string message)
{
//код отправки уведомления
}
public T Update(T entity)
{
//этот метод нам не нужен
throw new NotImplementedException();
}
public void Validate(T entity)
{
//этот метод нам не нужен
throw new NotImplementedException();
}
}
Как можно видеть по представленному выше примеру, каждый класс реализует свои методы:
Это наглядная демонстрация нарушения принципа разделения интерфейсов — наш интерфейс ICrudService получился «и швец, и жнец, и на дуде игрец» — делает всё. В результате, наши классы должны реализовывать те методы, которые им вообще не требуются. Следовательно, чтобы соответствовать ISP, нам необходимо разбить этот интерфейс на несколько более мелких.
Пример c использованием принципа разделения интерфейсов (ISP) в C#
Попробуем разделить наш «супер-интерфейс» так, чтобы каждый класс реализовывал только те методы, которые ему действительно требуются:
public interface ICreateble<T>
{
T Create(T entity);
}
public interface IReadable<T>
{
T Read(int id);
}
public interface IUpdatable<T>
{
T Update(T entity);
}
public interface IDeletable<T>
{
void Delete(int id);
}
public interface IValidable<T>
{
void Validate(T entity);
}
public interface INotifiable<T>
{
void SendNotification(T entity, string message);
}
public class TasksRepository<T> : ICreateble<T>, IReadable<T>, IUpdatable<T>, INotifiable<T>, IValidable<T>, IDeletable<T>
{
public T Create(T entity)
{
//код создания задачи
}
public void Delete(int id)
{
//код удаления задачи
}
public T Read(int id)
{
//код получения задачи
}
public void SendNotification(T entity, string message)
{
//код отправки уведомления
}
public T Update(T entity)
{
//код обновления задачи
}
public void Validate(T entity)
{
//код проверки задачи
}
}
public class DataRepository<T> : ICreateble<T>, IDeletable<T>, IReadable<T>
{
public T Create(T entity)
{
//код создания данных
}
public void Delete(int id)
{
//код удаления данных
}
public T Read(int id)
{
//код чтения данных
}
}
public class MessageRepository<T> : ICreateble<T>, INotifiable<T>
{
public T Create(T entity)
{
//код создания данных
}
public void SendNotification(T entity, string message)
{
//код отправки уведомления
}
}
В данном случае, мы разделили наш интерфейс на более мелки, в каждом из которых мы определили по одному методу. Стоит ещё раз особенно подчеркнуть — такое разделение не всегда требуется и не является обязательным. Вполне возможно, что при анализе вашего кода, вам будет достаточно сосредоточить все операции CRUD в одном интерфейсе, а проверку и отправку уведомлений — в двух других интерфейсах. Всё зависит от конкретной ситуации.
Преимущества принципа разделения интерфейсов в C#
Ниже перечислены некоторые из ключевых преимуществ применения принципа разделения интерфейсов в C#:
- Уменьшение побочных эффектов: благодаря разделению интерфейсов изменения в одной части системы с меньшей вероятностью повлияют на компоненты, реализующие эти интерфейсы. Клиенты будут знать только о тех методах, которые имеют к ним отношение, поэтому изменения в несвязанных методах не повлияют на них.
- Повышенная ясность и сосредоточенность: небольшие, четко определенные интерфейсы легче понять и реализовать. Каждый интерфейс фокусируется на определенном аспекте, что помогает разработчикам быстро понять, что требуется, не тратя время на изучение ненужных функций.
- Упрощенное обслуживание и расширение: благодаря небольшим разделенным интерфейсам обслуживание и расширение программного обеспечения становятся более простыми. Разработчики могут добавлять новые функции, не изменяя существующие интерфейсы, просто создавая новые интерфейсы и реализуя классы с несколькими интерфейсами по мере необходимости.
- Повышенная возможность повторного использования: Разделенные интерфейсы повышают возможность повторного использования. Поскольку каждый интерфейс меньше по размеру и более специфичен, его можно с большей легкостью использовать в разных частях системы или даже в разных проектах без лишних зависимостей.
- Способствует разделению: ISP помогает уменьшить связь между классами. Это позволяет реализующим классам зависеть только от тех интерфейсов, которые они фактически используют, что минимизирует зависимости между компонентами.