Нинішня ситуація
Поточна установка порушує Принцип розділення інтерфейсу (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 має метод implemenActions, який повертає ціле число, яке є результатом побітових АБО дій, розрядних ANDed із запитаними діями.
По суті, ви хочете зробити це:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
if(backendService.canDeleteUser())
backendService.deleteUser(user.Uid);
}
Я ігнорую, як саме ми визначаємо, чи здатний даний клас видалити користувача. Будь це булевий, трохи прапор, ... не має значення. Все зводиться до двійкової відповіді: чи може він видалити користувача, так чи ні?
Це вирішило б проблему, правда? Ну, технічно це роблять. Але тепер ви порушуєте Принцип заміщення Ліскова (L у твердій формі).
Проводячи досить складне пояснення Вікіпедії, я знайшов гідний приклад на 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і не матиме проблем із невтіленими методами.