J'ai travaillé sur la refactorisation de code, et je pense que j'ai peut-être fait le premier pas dans le terrier du lapin. J'écris l'exemple en Java, mais je suppose que cela pourrait être agnostique.
J'ai une interface Foo
définie comme
public interface Foo {
int getX();
int getY();
int getZ();
}
Et une mise en œuvre comme
public final class DefaultFoo implements Foo {
public DefaultFoo(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getZ() {
return z;
}
private final int x;
private final int y;
private final int z;
}
J'ai également une interface MutableFoo
qui fournit des mutateurs correspondants
/**
* This class extends Foo, because a 'write-only' instance should not
* be possible and a bit counter-intuitive.
*/
public interface MutableFoo extends Foo {
void setX(int newX);
void setY(int newY);
void setZ(int newZ);
}
Il existe quelques implémentations MutableFoo
qui pourraient exister (je ne les ai pas encore implémentées). L'un d'eux est
public final class DefaultMutableFoo implements MutableFoo {
/**
* A DefaultMutableFoo is not conceptually constructed
* without all values being set.
*/
public DefaultMutableFoo(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
public int getX() {
return x;
}
public void setX(int newX) {
this.x = newX;
}
public int getY() {
return y;
}
public void setY(int newY) {
this.y = newY;
}
public int getZ() {
return z;
}
public void setZ(int newZ) {
this.z = newZ;
}
private int x;
private int y;
private int z;
}
La raison pour laquelle je les ai divisés est qu'il est également probable que chacun soit utilisé. Autrement dit, il est tout aussi probable que quelqu'un utilisant ces classes veuille une instance immuable, comme il le voudra une instance mutable.
Le principal cas d'utilisation que j'ai est une interface appelée StatSet
qui représente certains détails de combat pour un jeu (points de vie, attaque, défense). Cependant, les statistiques "effectives", ou les statistiques réelles, sont le résultat des statistiques de base, qui ne peuvent jamais être modifiées, et des statistiques entraînées, qui peuvent être augmentées. Ces deux sont liés par
/**
* The EffectiveStats can never be modified independently of either the baseStats
* or trained stats. As such, this StatSet must never provide mutators.
*/
public StatSet calculateEffectiveStats() {
int effectiveHitpoints =
baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
int effectiveAttack =
baseStats.getAttack() + (trainedStats.getAttack() / 4);
int effectiveDefense =
baseStats.getDefense() + (trainedStats.getDefense() / 4);
return StatSetFactory.createImmutableStatSet(effectiveHitpoints, effectiveAttack, effectiveDefense);
}
les Stats formés sont augmentés après chaque bataille comme
public void grantExperience() {
int hitpointsReward = 0;
int attackReward = 0;
int defenseReward = 0;
final StatSet enemyStats = enemy.getEffectiveStats();
final StatSet currentStats = player.getEffectiveStats();
if (enemyStats.getHitpoints() >= currentStats.getHitpoints()) {
hitpointsReward++;
}
if (enemyStats.getAttack() >= currentStats.getAttack()) {
attackReward++;
}
if (enemyStats.getDefense() >= currentStats.getDefense()) {
defenseReward++;
}
final MutableStatSet trainedStats = player.getTrainedStats();
trainedStats.increaseHitpoints(hitpointsReward);
trainedStats.increaseAttack(attackReward);
trainedStats.increaseDefense(defenseReward);
}
mais ils ne sont pas augmentés juste après la bataille. L'utilisation de certains objets, l'emploi de certaines tactiques, une utilisation intelligente du champ de bataille peuvent tous offrir une expérience différente.
Maintenant pour mes questions:
- Existe-t-il un nom pour diviser les interfaces par les accesseurs et les mutateurs en interfaces distinctes?
- Est-ce que les séparer de cette manière est la «bonne» approche s'ils sont également susceptibles d'être utilisés, ou existe-t-il un modèle différent et plus accepté que je devrais utiliser à la place (par exemple,
Foo foo = FooFactory.createImmutableFoo();
qui pourrait revenirDefaultFoo
ou quiDefaultMutableFoo
est caché parce qu'ilcreateImmutableFoo
revientFoo
)? - Y a-t-il des inconvénients immédiatement prévisibles à utiliser ce modèle, sauf pour compliquer la hiérarchie de l'interface?
La raison pour laquelle j'ai commencé à la concevoir de cette façon est parce que je pense que tous les implémenteurs d'une interface devraient adhérer à l'interface la plus simple possible et ne rien fournir de plus. En ajoutant des setters à l'interface, les statistiques effectives peuvent désormais être modifiées indépendamment de ses parties.
Créer une nouvelle classe pour le EffectiveStatSet
n'a pas beaucoup de sens car nous n'étendons pas la fonctionnalité en aucune façon. Nous pourrions changer la mise en œuvre, et faire EffectiveStatSet
un composite de deux différents StatSets
, mais je pense que ce n'est pas la bonne solution;
public class EffectiveStatSet implements StatSet {
public EffectiveStatSet(StatSet baseStats, StatSet trainedStats) {
// ...
}
public int getHitpoints() {
return baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
}
}
Réponses:
Il me semble que vous avez une solution à la recherche d'un problème.
Cela peut être un peu provocant, mais en fait, je l'appellerais «choses trop conçues» ou «choses trop compliquées». En offrant une variante mutable et immuable de la même classe, vous offrez deux solutions fonctionnellement équivalentes pour le même problème, qui ne diffèrent que par des aspects non fonctionnels comme le comportement de performance, l'API et la sécurité contre les effets secondaires. Je suppose que c'est parce que vous avez peur de prendre une décision que vous préférez, ou parce que vous essayez d'implémenter la fonctionnalité "const" de C ++ en C #. Je suppose que dans 99% des cas, cela ne fera pas une grande différence si l'utilisateur choisit la variante mutable ou immuable, il peut résoudre ses problèmes avec l'un ou l'autre. Ainsi, "probabilité d'une classe à utiliser"
L'exception est lorsque vous concevez un nouveau langage de programmation ou un cadre polyvalent qui sera utilisé par une dizaine de milliers de programmeurs ou plus. Ensuite, il peut effectivement évoluer mieux lorsque vous proposez des variantes immuables et modifiables de types de données à usage général. Mais c'est une situation où vous aurez des milliers de scénarios d'utilisation différents - ce qui n'est probablement pas le problème auquel vous êtes confronté, je suppose?
Le "modèle plus accepté" est appelé KISS - restez simple et stupide. Prenez une décision pour ou contre la mutabilité de la classe / interface spécifique dans votre bibliothèque. Par exemple, si votre "StatSet" a une douzaine d'attributs ou plus, et qu'ils sont pour la plupart modifiés individuellement, je préférerais la variante mutable et je ne modifierais pas les statistiques de base là où elles ne devraient pas être modifiées. Pour quelque chose comme une
Foo
classe avec des attributs X, Y, Z (un vecteur tridimensionnel), je préférerais probablement la variante immuable.Les conceptions trop complexes rendent les logiciels plus difficiles à tester, plus difficiles à maintenir, plus difficiles à faire évoluer.
la source
Il pourrait y avoir un nom pour cela si cette séparation est utile et fournit un avantage que je ne vois pas . Si les séparations n'apportent aucun avantage, les deux autres questions n'ont aucun sens.
Pouvez-vous nous dire tout cas d'utilisation commerciale où les deux interfaces séparées nous offrent des avantages ou cette question est-elle un problème académique (YAGNI)?
Je pense à une boutique avec un chariot modifiable (vous pouvez y mettre plus d'articles) qui peut devenir une commande où les articles ne peuvent plus être modifiés par le client. Le statut de la commande est toujours modifiable.
Les implémentations que j'ai vues ne séparent pas les interfaces
La version ReadOnly utilise le "WritableInterface" et lève une exception si une méthode d'écriture est utilisée
la source
La séparation d'une interface de collecte mutable et d'une interface de collecte en lecture seule est un exemple de principe de ségrégation d'interface. Je ne pense pas qu'il y ait de noms spéciaux pour chaque application de principe.
Notez quelques mots ici: "contrat en lecture seule" et "collecte".
Un contrat en lecture seule signifie que la classe
A
donne à la classeB
un accès en lecture seule, mais n'implique pas que l'objet de collection sous-jacent est réellement immuable. Immuable signifie qu'il ne changera jamais à l'avenir, quel que soit l'agent. Un contrat en lecture seule indique uniquement que le destinataire n'est pas autorisé à le modifier; quelqu'un d'autre (en particulier, la classeA
) est autorisé à le modifier.Pour rendre un objet immuable, il doit être vraiment immuable - il doit refuser les tentatives de modification de ses données quel que soit l'agent qui le demande.
Le motif est très probablement observé sur des objets qui représentent une collection de données - listes, séquences, fichiers (flux), etc.
Le mot «immuable» devient une mode, mais le concept n'est pas nouveau. Et il existe de nombreuses façons d'utiliser l'immuabilité, ainsi que de nombreuses façons d'obtenir une meilleure conception en utilisant autre chose (c'est-à-dire ses concurrents).
Voici mon approche (non basée sur l'immuabilité).
hitpoints
,attack
,defense
.A
doit passer un DTO à la classeB
, il en fait une copie et passe la copie à la place. Ainsi, vousB
pouvez utiliser le DTO comme bon vous semble (en y écrivant), sans affecter le DTO qui s'y accrocheA
.grantExperience
fonction à décomposer en deux:calculateNewStats
increaseStats
calculateNewStats
prendra les entrées de deux DTO, l'un représentant les statistiques du joueur et l'autre représentant les statistiques de l'ennemi, et effectuera les calculs.hitpoints
,attack
,defense
) stocke le montant à incrémentée pour cette capacité.increaseStats
est une méthode sur le joueur (pas sur le DTO) qui prend un DTO "montant à incrémenter", et applique cet incrément sur le DTO qui appartient au joueur et représente le DTO entraînable du joueur.Dans le cas où il
calculateNewStats
s'avère ne dépendre d'aucun autre joueur ou information ennemie (au-delà des valeurs dans les deux entrées DTO), cette méthode peut potentiellement être localisée n'importe où dans le projet.S'il
calculateNewStats
s'avère que le produit dépend entièrement du joueur et des objets ennemis (c'est-à-dire que les futurs joueurs et objets ennemis peuvent avoir de nouvelles propriétés etcalculateNewStats
doivent être mis à jour pour consommer autant de leurs nouvelles propriétés que possible), ilscalculateNewStats
doivent alors accepter ces deux objets, pas seulement le DTO. Cependant, son résultat de calcul sera toujours l'incrément DTO, ou tout autre objet de transfert de données simple qui transporte les informations utilisées pour effectuer une incrémentation / mise à niveau.la source
Il y a un gros problème ici: ce n'est pas parce qu'une structure de données est immuable que nous n'en avons pas besoin de versions modifiées . La vraie la version immuable d'une structure de données ne fournit
setX
,setY
et dessetZ
méthodes - ils reviennent juste une nouvelle structure de lieu de modifier l'objet que vous les ai appelés.Alors, comment donnez-vous à une partie de votre système la possibilité de la changer tout en restreignant d'autres parties? Avec un objet mutable contenant une référence à une instance de la structure immuable. Fondamentalement, au lieu que votre classe de joueur puisse muter son objet de statistiques tout en donnant à chaque autre classe une vue immuable de ses statistiques, ses statistiques sont immuables et le joueur est mutable. Au lieu de:
Vous auriez:
Regarde la différence? L'objet stats est complètement immuable, mais les statistiques actuelles du joueur sont une référence mutable à l'objet immuable. Pas besoin de créer deux interfaces - il suffit de rendre la structure de données complètement immuable et de gérer une référence mutable à l'endroit où vous l'utilisez pour stocker l'état.
Cela signifie que les autres objets de votre système ne peuvent pas s'appuyer sur une référence à l'objet stats - l'objet stats qu'ils ont ne sera pas le même objet stats que le joueur a une fois que le joueur a mis à jour leurs statistiques.
Cela a plus de sens de toute façon, car conceptuellement, ce ne sont pas vraiment les statistiques qui changent, ce sont les statistiques actuelles du joueur . Pas l'objet statistiques. La référence à l'objet stats de l'objet joueur. Ainsi, d'autres parties de votre système en fonction de cette référence devraient toutes faire explicitement référence
player.currentStats()
ou certaines autres au lieu de saisir l'objet de statistiques sous-jacent du joueur, de le stocker quelque part et de compter sur sa mise à jour via des mutations.la source
Wow ... cela me ramène vraiment. J'ai essayé plusieurs fois cette même idée. J'étais trop têtu pour y renoncer parce que je pensais qu'il y aurait quelque chose de bon à apprendre. J'ai vu d'autres essayer cela dans le code aussi. La plupart des implémentations que j'ai vues appelaient les interfaces en lecture seule comme la vôtre
FooViewer
ouReadOnlyFoo
et l'interface en écriture seule laFooEditor
ouWriteableFoo
ou en Java, je pense avoir vuFooMutator
une fois. Je ne suis pas sûr qu'il existe un vocabulaire officiel ou même commun pour faire les choses de cette façon.Cela n'a jamais rien fait d'utile pour moi dans les endroits où je l'ai essayé. Je l'éviterais complètement. Je ferais ce que d'autres suggèrent et je prendrais du recul et je me demanderais si vous avez vraiment besoin de cette notion dans votre idée plus large. Je ne suis pas sûr qu'il existe une bonne façon de le faire, car je n'ai jamais gardé le code que j'ai produit en essayant. Chaque fois, j'ai reculé après des efforts considérables et des choses simplifiées. Et par cela, je veux dire quelque chose dans le sens de ce que les autres ont dit à propos de YAGNI et KISS et DRY.
Un inconvénient possible: la répétition. Vous devrez créer ces interfaces pour de nombreuses classes potentiellement et même pour une seule classe, vous devrez nommer chaque méthode et décrire chaque signature au moins deux fois. De toutes les choses qui me brûlent dans le codage, devoir changer plusieurs fichiers de cette manière me pose le plus de problèmes. Je finis par oublier de faire les changements à un endroit ou à un autre. Une fois que vous avez une classe appelée Foo et des interfaces appelées FooViewer et FooEditor, si vous décidez que ce serait mieux si vous l'appeliez Bar à la place, vous devez refactoriser-> renommer trois fois sauf si vous avez un IDE vraiment incroyablement intelligent. J'ai même trouvé que c'était le cas lorsque j'avais une quantité considérable de code provenant d'un générateur de code.
Personnellement, je n'aime pas créer des interfaces pour des choses qui n'ont qu'une seule implémentation. Cela signifie que lorsque je voyage dans le code, je ne peux pas simplement passer directement à la seule implémentation, je dois passer à l'interface, puis à l'implémentation de l'interface ou au moins appuyer sur quelques touches supplémentaires pour y arriver. Ces choses s'additionnent.
Et puis il y a la complication que vous avez mentionnée. Je doute que je recommencerai jamais avec ce code. Même pour l'endroit où cela correspondait le mieux à mon plan global pour le code (un ORM que j'ai construit), je l'ai retiré et remplacé par quelque chose de plus facile à coder.
Je réfléchirais vraiment à la composition que vous avez mentionnée. Je suis curieux de savoir pourquoi vous pensez que ce serait une mauvaise idée. Je me serais attendu à avoir de la
EffectiveStats
composition et immuableStats
et quelque chose comme çaStatModifiers
qui composerait davantage un ensemble deStatModifier
s qui représentent tout ce qui pourrait modifier les statistiques (effets temporaires, objets, emplacement dans une zone d'amélioration, fatigue) mais que vousEffectiveStats
n'auriez pas besoin de comprendre parceStatModifiers
que gérer ce que ces choses étaient et quel effet et quel genre elles auraient sur quelles statistiques. leStatModifier
serait une interface pour les différentes choses et pourrait savoir des choses comme "suis-je dans la zone", "quand le médicament disparaît-il", etc ... mais n'aurait même pas besoin de faire savoir à d'autres objets de telles choses. Il lui suffirait de dire quelle statistique et comment elle était modifiée en ce moment. Mieux encoreStatModifier
pourrait simplement exposer une méthode de production d'une nouvelle immuableStats
basée sur une autreStats
qui serait différente car elle a été modifiée de manière appropriée. Vous pourriez alors faire quelque chose commecurrentStats = statModifier2.modify(statModifier1.modify(baseStats))
et tout celaStats
peut être immuable. Je ne coderais même pas cela directement, je parcourrais probablement tous les modificateurs et les appliquerais au résultat des modificateurs précédents commençant parbaseStats
.la source