Scénario
Une application Web définit une interface utilisateur dorsale IUserBackend
avec les méthodes
- getUser (uid)
- createUser (uid)
- deleteUser (uid)
- setPassword (uid, mot de passe)
- ...
Différents backends utilisateurs (par exemple LDAP, SQL, ...) implémentent cette interface mais tous les backends ne peuvent pas tout faire. Par exemple, un serveur LDAP concret ne permet pas à cette application Web de supprimer des utilisateurs. La LdapUserBackend
classe qui implémente IUserBackend
ne l'implémentera donc pas deleteUser(uid)
.
La classe concrète doit communiquer à l'application web ce que l'application web est autorisée à faire avec les utilisateurs du backend.
Solution connue
J'ai vu une solution où le IUserInterface
a une implementedActions
méthode qui retourne 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:
function implementedActions(requestedActions) {
return (bool)(
ACTION_GET_USER
| ACTION_CREATE_USER
| ACTION_DELTE_USER
| ACTION_SET_PASSWORD
) & requestedActions)
}
Où
- ACTION_GET_USER = 1
- ACTION_CREATE_USER = 2
- ACTION_DELETE_USER = 4
- ACTION_SET_PASSWORD = 8
- .... = 16
- .... = 32
etc.
Ainsi, l'application Web définit un masque de bits avec ce dont elle a besoin et implementedActions()
répond par un booléen si elle les prend en charge.
Opinion
Ces opérations de bits me ressemblent à des reliques de l'ère C, pas nécessairement faciles à comprendre en termes de code propre.
Question
Qu'est-ce qu'un modèle moderne (meilleur?) Pour que la classe communique le sous-ensemble des méthodes d'interface qu'elle implémente? Ou la "méthode d'opération de bits" ci-dessus est-elle toujours la meilleure pratique?
( Au cas où cela compte: PHP, bien que je recherche une solution générale pour les langages OO )
la source
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.Réponses:
De manière générale, il existe deux approches que vous pouvez adopter ici: test & throw ou composition via le polymorphisme.
Test & lancer
C'est l'approche que vous décrivez déjà. Par certains moyens, vous indiquez à l'utilisateur de la classe si certaines autres méthodes sont implémentées ou non. Cela peut être fait avec une seule méthode et une énumération au niveau du bit (comme vous le décrivez), ou via une série de
supportsDelete()
méthodes, etc.Ensuite, s'il
supportsDelete()
retournefalse
, l'appeldeleteUser()
peut entraîner unNotImplementedExeption
rejet ou la méthode ne fait rien.C'est une solution populaire parmi certains, car c'est simple. Cependant, beaucoup - moi y compris - soutiennent que c'est une violation du principe de substitution de Liskov (le L dans SOLID) et n'est donc pas une bonne solution.
Composition via le polymorphisme
L'approche ici consiste à voir
IUserBackend
un instrument beaucoup trop brutal. Si les classes ne peuvent pas toujours implémenter toutes les méthodes de cette interface, divisez l'interface en parties plus ciblées. Donc, vous pourriez avoir:IGeneralUser IDeletableUser IRenamableUser ...
En d'autres termes, toutes les méthodes que tous vos backends peuvent implémenter entrentIGeneralUser
et vous créez une interface distincte pour chacune des actions que seuls certains peuvent effectuer.De cette façon,
LdapUserBackend
n'implémente pasIDeletableUser
et vous testez cela en utilisant un test tel que (en utilisant la syntaxe C #):(Je ne suis pas sûr du mécanisme en PHP pour déterminer si une instance implémente une interface et comment vous transformez ensuite cette interface, mais je suis sûr qu'il y a un équivalent dans ce langage)
L'avantage de cette méthode est qu'elle fait bon usage du polymorphisme pour permettre à votre code de se conformer aux principes SOLIDES et est juste beaucoup plus élégante à mon avis.
L'inconvénient est qu'il peut devenir trop lourd. Si, par exemple, vous finissez par devoir implémenter des dizaines d'interfaces car chaque backend concret a des capacités légèrement différentes, alors ce n'est pas une bonne solution. Je vous conseillerais donc simplement d'utiliser votre jugement pour savoir si cette approche est pratique pour vous à cette occasion et de l'utiliser, si elle l'est.
la source
if (backend instanceof IDelatableUser) {...}
Divide(float,float)
méthode. La valeur d'entrée est variable et l'exception couvre un petit sous-ensemble d'exécutions possibles. Mais si vous lancez en fonction de votre type d'implémentation, son incapacité à exécuter est un fait donné. L'exception couvre toutes les entrées possibles , pas seulement un sous-ensemble d'entre elles. C'est comme mettre un panneau "sol mouillé" sur chaque sol mouillé dans un monde où chaque sol est toujours mouillé.NotImplementedException
. Cette exception est destinée aux pannes temporaires , c'est-à-dire au code qui n'est pas encore développé mais qui sera développé. Ce n'est pas la même chose que de décider définitivement qu'une classe donnée ne fera jamais rien avec une méthode donnée, même une fois le développement terminé.La situation présente
La configuration actuelle viole le principe de séparation d'interface (le I dans SOLID).
Référence
En d'autres termes, s'il s'agit de votre interface:
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:
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
Ce que vous voulez essentiellement faire, c'est:
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:
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 uneIUserBackend
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 laSwim()
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 .
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:
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 .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.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:
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.
la source
TryDeleteUser
pour refléter cela); ou vous avez la méthode de lever délibérément une exception si c'est une situation possible mais problématique. L'utilisation d'une approcheCanDoThing()
et d' uneDoThing()
méthode fonctionne, mais cela nécessiterait que vos appelants externes utilisent deux appels (et soient punis pour ne pas le faire), ce qui est moins intuitif et moins élégant.Si vous souhaitez utiliser des types de niveau supérieur, vous pouvez choisir le type défini dans la langue de votre choix. Espérons que cela fournisse du sucre syntaxique pour faire des intersections d'ensembles et la détermination de sous-ensembles.
C'est essentiellement ce que Java fait avec EnumSet (moins le sucre de syntaxe, mais bon, c'est Java)
la source
Dans le monde .NET, vous pouvez décorer des méthodes et des classes avec des attributs personnalisés. Cela peut ne pas être pertinent pour votre cas.
Il me semble cependant que le problème que vous rencontrez peut être davantage lié à un niveau supérieur de conception.
S'il s'agit d'une fonctionnalité d'interface utilisateur, telle qu'une page ou un composant d'édition d'utilisateurs, comment les différentes capacités sont-elles masquées? Dans ce cas, «tester et lancer» sera une approche assez inefficace à cet effet. Il suppose qu'avant de charger chaque page, vous exécutez un appel simulé à chaque fonction pour déterminer si le widget ou l'élément doit être masqué ou présenté différemment. Alternativement, vous avez une page Web qui oblige essentiellement l'utilisateur à découvrir ce qui est disponible par `` test et lancement manuel '', quelle que soit la route de codage que vous prenez, car l'utilisateur ne découvre pas que quelque chose n'est pas disponible jusqu'à ce qu'un avertissement contextuel s'affiche.
Donc, pour une interface utilisateur, vous voudrez peut-être examiner comment vous effectuez la gestion des fonctionnalités et lier le choix des implémentations disponibles à cela, plutôt que de demander aux implémentations sélectionnées de déterminer quelles fonctionnalités peuvent être gérées. Vous souhaiterez peut-être examiner les cadres de composition des dépendances des fonctionnalités et définir explicitement les capacités en tant qu'entités dans votre modèle de domaine. Cela pourrait même être lié à une autorisation. Essentiellement, décider si une capacité est disponible ou non en fonction du niveau d'autorisation peut être étendu pour décider si une capacité est réellement implémentée, puis les `` fonctionnalités '' de l'interface utilisateur de haut niveau peuvent avoir des mappages explicites avec les ensembles de capacités.
S'il s'agit d'une API Web, le choix de conception global peut être compliqué en prenant en charge plusieurs versions publiques de l'API 'Manage User' ou de la ressource REST 'User' à mesure que les capacités s'étendent au fil du temps.
Donc, pour résumer, dans le monde .NET, vous pouvez exploiter diverses manières de réflexion / attribut pour déterminer à l'avance quelles classes implémentent quoi, mais en tout cas, il semble que les vrais problèmes vont être dans ce que vous faites avec ces informations.
la source