La situation présente
La configuration actuelle viole le principe de séparation d'interface (le I dans SOLID).
Référence
Selon Wikipedia, le principe de ségrégation d'interface (ISP) stipule qu'aucun client ne devrait être contraint de dépendre de méthodes qu'il n'utilise pas . Le principe de ségrégation d'interface a été formulé par Robert Martin au milieu des années 1990.
En d'autres termes, s'il s'agit de votre interface:
public interface IUserBackend
{
User getUser(int uid);
User createUser(int uid);
void deleteUser(int uid);
void setPassword(int uid, string password);
}
Ensuite, chaque classe qui implémente cette interface doit utiliser toutes les méthodes répertoriées de l'interface. Pas exception.
Imaginez s'il existe une méthode généralisée:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
backendService.deleteUser(user.Uid);
}
Si vous deviez réellement faire en sorte que seules certaines des classes d'implémentation soient réellement capables de supprimer un utilisateur, alors cette méthode vous explosera de temps en temps (ou ne fera rien du tout). Ce n'est pas un bon design.
Votre solution proposée
J'ai vu une solution où l'IUserInterface a une méthode implementActions qui renvoie un entier qui est le résultat des OR au niveau du bit des actions au niveau du bit ET avec les actions demandées.
Ce que vous voulez essentiellement faire, c'est:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
if(backendService.canDeleteUser())
backendService.deleteUser(user.Uid);
}
J'ignore comment exactement nous déterminons si une classe donnée est capable de supprimer un utilisateur. Qu'il s'agisse d'un booléen, d'un drapeau, ... n'a pas d'importance. Tout se résume à une réponse binaire: peut-il supprimer un utilisateur, oui ou non?
Cela résoudrait le problème, non? Eh bien, techniquement, c'est le cas. Mais maintenant, vous violez le principe de substitution de Liskov (le L dans SOLID).
Renonçant à l'explication plutôt complexe de Wikipédia, j'ai trouvé un exemple décent sur StackOverflow . Prenez note du "mauvais" exemple:
void MakeDuckSwim(IDuck duck)
{
if (duck is ElectricDuck)
((ElectricDuck)duck).TurnOn();
duck.Swim();
}
Je suppose que vous voyez la similitude ici. C'est une méthode qui est censée gérer un objet abstrait ( IDuck
, IUserBackend
), mais en raison d'une conception de classe compromise, elle est obligée de gérer d'abord des implémentations spécifiques ( ElectricDuck
, assurez-vous que ce n'est pas une IUserBackend
classe qui ne peut pas supprimer d'utilisateurs).
Cela va à l'encontre du but de développer une approche abstraite.
Remarque: L'exemple ici est plus facile à corriger que votre cas. Pour l'exemple, il suffit d'avoir le ElectricDuck
turn lui-même dans la Swim()
méthode. Les deux canards sont toujours capables de nager, donc le résultat fonctionnel est le même.
Vous voudrez peut-être faire quelque chose de similaire. Non . Vous ne pouvez pas simplement faire semblant de supprimer un utilisateur, mais en réalité, avoir un corps de méthode vide. Bien que cela fonctionne d'un point de vue technique, il est impossible de savoir si votre classe d'implémentation fera réellement quelque chose lorsqu'on lui demandera de faire quelque chose. C'est un terreau fertile pour un code impossible à maintenir.
Ma solution proposée
Mais vous avez dit qu'il est possible (et correct) pour une classe d'implémentation de gérer uniquement certaines de ces méthodes.
Par exemple, disons que pour chaque combinaison possible de ces méthodes, il existe une classe qui l'implémentera. Il couvre toutes nos bases.
La solution ici est de diviser l'interface .
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);
}
Notez que vous auriez pu voir cela venir au début de ma réponse. Le nom du principe de séparation des interfaces révèle déjà que ce principe est conçu pour vous faire séparer les interfaces à un degré suffisant.
Cela vous permet de mélanger et de faire correspondre les interfaces à votre guise:
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
}
Chaque classe peut décider ce qu'elle veut faire, sans pour autant rompre le contrat de son interface.
Cela signifie également que nous n'avons pas besoin de vérifier si une certaine classe est capable de supprimer un utilisateur. Chaque classe qui implémente l' IDeleteUserService
interface pourra supprimer un utilisateur = Aucune violation du principe de substitution Liskov .
public void HaveUserDeleted(IDeleteUserService backendService, User user)
{
backendService.deleteUser(user.Uid); //guaranteed to work
}
Si quelqu'un essaie de passer un objet qui ne l'implémente pas IDeleteUserService
, le programme refusera de compiler. C'est pourquoi nous aimons la sécurité des caractères.
HaveUserDeleted(new DoesEverythingService()); // No problem.
HaveUserDeleted(new UserDeleteService()); // No problem.
HaveUserDeleted(new UserRetrievalService()); // COMPILE ERROR
note de bas de page
J'ai pris l'exemple à l'extrême, en séparant l'interface en les plus petits morceaux possibles. Cependant, si votre situation est différente, vous pouvez vous en tirer avec de plus gros morceaux.
Par exemple, si chaque service qui peut créer un utilisateur est toujours capable de supprimer un utilisateur (et vice versa), vous pouvez conserver ces méthodes dans le cadre d'une interface unique:
public interface IManageUserService
{
User createUser(int uid);
void deleteUser(int uid);
}
Il n'y a aucun avantage technique à faire cela au lieu de se séparer des plus petits morceaux; mais cela rendra le développement un peu plus facile car il nécessite moins de passe-partout.
IUserBackend
ne doit pas du tout contenir ladeleteUser
méthode. Cela devrait faire partie deIUserDeleteBackend
(ou comme vous voulez l'appeler). Le code qui doit supprimer les utilisateurs aura des argumentsIUserDeleteBackend
, le code qui n'a pas besoin de cette fonctionnalité utiliseraIUserBackend
et n'aura aucun problème avec les méthodes non implémentées.