Содержание
Принцип разделения интерфейсов (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 помогает уменьшить связь между классами. Это позволяет реализующим классам зависеть только от тех интерфейсов, которые они фактически используют, что минимизирует зависимости между компонентами.