Текущая ситуация
Текущая настройка нарушает принцип разделения интерфейса (I в SOLID).
Ссылка
Согласно Википедии, принцип разделения интерфейса (ISP) гласит, что ни один клиент не должен зависеть от методов, которые он не использует . Принцип разделения интерфейса был сформулирован Робертом Мартином в середине 1990-х годов.
Другими словами, если это ваш интерфейс:
public interface IUserBackend
{
User getUser(int uid);
User createUser(int uid);
void deleteUser(int uid);
void setPassword(int uid, string password);
}
Затем каждый класс, который реализует этот интерфейс, должен использовать каждый из перечисленных методов интерфейса. Не исключение
Представьте, если есть обобщенный метод:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
backendService.deleteUser(user.Uid);
}
Если вы действительно должны были сделать так, чтобы только некоторые из реализующих классов действительно могли удалить пользователя, то этот метод иногда взорвется вам (или вообще ничего не сделает). Это не хороший дизайн.
Ваше предлагаемое решение
Я видел решение, в котором IUserInterface имеет метод ImplectedActions, который возвращает целое число, которое является результатом побитового ИЛИ действий побитового И с запрошенными действиями.
То, что вы по сути хотите сделать, это:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
if(backendService.canDeleteUser())
backendService.deleteUser(user.Uid);
}
Я игнорирую, как именно мы определяем, может ли данный класс удалить пользователя. Будь то логическое значение, битовый флаг ... не имеет значения. Все сводится к двоичному ответу: может ли он удалить пользователя, да или нет?
Это решит проблему, верно? Ну, технически это так. Но теперь вы нарушаете принцип подстановки Лискова (L в SOLID).
Отказавшись от довольно сложного объяснения в Википедии, я нашел достойный пример на StackOverflow . Обратите внимание на «плохой» пример:
void MakeDuckSwim(IDuck duck)
{
if (duck is ElectricDuck)
((ElectricDuck)duck).TurnOn();
duck.Swim();
}
Я полагаю, вы видите здесь сходство. Это метод, который должен обрабатывать абстрагированный объект ( IDuck, IUserBackend), но из-за скомпрометированной конструкции класса он вынужден сначала обрабатывать определенные реализации ( ElectricDuckубедитесь, что это не тот IUserBackendкласс, который не может удалять пользователей).
Это противоречит цели разработки абстрактного подхода.
Примечание: пример здесь легче исправить, чем ваш случай. Для примера, достаточно иметь ElectricDuckсам поворот на внутри в Swim()методе. Обе утки еще умеют плавать, поэтому функциональный результат одинаков.
Вы можете сделать что-то подобное. Не . Вы не можете просто притворяться, что удалили пользователя, но в действительности у вас есть пустое тело метода. Хотя это работает с технической точки зрения, невозможно определить, действительно ли ваш реализующий класс что-то сделает, когда его попросят что-то сделать. Это питательная среда для неуправляемого кода.
Мое предлагаемое решение
Но вы сказали, что реализующий класс может (и правильно) обрабатывать только некоторые из этих методов.
Для примера скажем, что для каждой возможной комбинации этих методов существует класс, который будет реализовывать его. Он охватывает все наши базы.
Решением здесь является разделение интерфейса .
public interface IGetUserService
{
User getUser(int uid);
}
public interface ICreateUserService
{
User createUser(int uid);
}
public interface IDeleteUserService
{
void deleteUser(int uid);
}
public interface ISetPasswordService
{
void setPassword(int uid, string password);
}
Обратите внимание, что вы могли видеть это в начале моего ответа. Название Принципа Разделения Интерфейса уже показывает, что этот принцип разработан, чтобы заставить вас разделить интерфейсы в достаточной степени.
Это позволяет вам смешивать и сочетать интерфейсы по своему усмотрению:
public class UserRetrievalService
: IGetUserService, ICreateUserService
{
//getUser and createUser methods implemented here
}
public class UserDeleteService
: IDeleteUserService
{
//deleteUser method implemented here
}
public class DoesEverythingService
: IGetUserService, ICreateUserService, IDeleteUserService, ISetPasswordService
{
//All methods implemented here
}
Каждый класс может решить, что он хочет делать, не нарушая контракт своего интерфейса.
Это также означает, что нам не нужно проверять, может ли определенный класс удалить пользователя. Каждый класс, реализующий IDeleteUserServiceинтерфейс, сможет удалить пользователя = Нет нарушения принципа подстановки Лискова .
public void HaveUserDeleted(IDeleteUserService backendService, User user)
{
backendService.deleteUser(user.Uid); //guaranteed to work
}
Если кто-то попытается передать объект, который не реализует IDeleteUserService, программа откажется от компиляции. Вот почему нам нравится иметь безопасность типов.
HaveUserDeleted(new DoesEverythingService()); // No problem.
HaveUserDeleted(new UserDeleteService()); // No problem.
HaveUserDeleted(new UserRetrievalService()); // COMPILE ERROR
сноска
Я довел пример до крайности, разделив интерфейс на наименьшие возможные куски. Однако, если ваша ситуация отличается, вы можете избежать больших кусков.
Например, если каждая служба, которая может создать пользователя, всегда способна удалить пользователя (и наоборот), вы можете оставить эти методы как часть единого интерфейса:
public interface IManageUserService
{
User createUser(int uid);
void deleteUser(int uid);
}
Нет технической выгоды делать это вместо разделения на более мелкие куски; но это немного облегчит разработку, поскольку требует меньшего количества компоновки.
IUserBackendне должен содержатьdeleteUserметод вообще. Это должно быть частьюIUserDeleteBackend(или как вы хотите это назвать). Код, который должен удалять пользователей, будет иметь аргументыIUserDeleteBackend, код, который не нуждается в этой функциональности, будет использоватьIUserBackendи не будет иметь проблем с нереализованными методами.