J'ai une méthode qui crée un fichier de données après avoir parlé à une carte numérique:
CreateDataFile(IFileAccess boardFileAccess, IMeasurer boardMeasurer)
Ici, boardFileAccess
et boardMeasurer
sont la même instance d'un objet Board
qui implémente à la fois IFileAccess
et IMeasurer
. IMeasurer
est utilisé dans ce cas pour une seule méthode qui mettra une broche sur la carte active pour effectuer une mesure simple. Les données de cette mesure sont ensuite stockées localement sur la carte à l'aide de IFileAccess
. Board
est situé dans un projet distinct.
Je suis arrivé à la conclusion que CreateDataFile
fait une chose en effectuant une mesure rapide puis en stockant les données, et faire les deux dans la même méthode est plus intuitif pour quelqu'un d'autre utilisant ce code, puis devant faire un mesure et écrire dans un fichier en tant qu'appels de méthode distincts.
Il me semble gênant de passer deux fois le même objet à une méthode. J'ai envisagé de créer une interface locale IDataFileCreator
qui étendra IFileAccess
et IMeasurer
, puis j'ai une implémentation contenant une instance Board
qui appellera simplement le requis Méthodes Board
. Étant donné que le même objet de carte serait toujours utilisé pour la mesure et l'écriture de fichier, est-ce une mauvaise pratique de passer deux fois le même objet à une méthode? Si oui, l'utilisation d'une interface locale et de la mise en œuvre est-elle une solution appropriée?
Non, c'est parfaitement bien. Cela signifie simplement que l'API est sur-conçue en ce qui concerne votre application actuelle.
Mais cela ne prouve pas qu'il y aura jamais un cas d'utilisation dans lequel la source de données et le mesureur sont différents. L'intérêt d'une API est d'offrir au programmeur d'applications des possibilités qui ne seront pas toutes utilisées. Vous ne devez pas restreindre artificiellement ce que les utilisateurs d'API peuvent faire à moins que cela ne complique l'API afin que la compréhensibilité nette diminue.
D'accord avec @ la réponse de KilianFoth que c'est parfaitement bien.
Néanmoins, si vous le souhaitez, vous pouvez créer une méthode qui prend un seul objet qui implémente les deux interfaces:
public object CreateDataFile<T_BoardInterface>(
T_BoardInterface boardInterface
)
where T_BoardInterface : IFileAccess, IMeasurer
{
return CreateDataFile(
boardInterface
, boardInterface
);
}
Il n'y a aucune raison générale pour laquelle les arguments doivent être des objets différents, et si une méthode nécessitait des arguments différents, ce serait une exigence spéciale que son contrat devrait clarifier.
Je suis arrivé à la conclusion que
CreateDataFile
fait une chose en effectuant une mesure rapide puis en stockant les données, et faire les deux dans la même méthode est plus intuitif pour quelqu'un d'autre utilisant ce code puis devant faire un mesure et écrire dans un fichier en tant qu'appels de méthode distincts.
Je pense que c'est votre problème, en fait. La méthode ( ne fait pas une seule chose. Il effectue deux opérations distinctes qui impliquent des E/S vers différents périphériques , les deux étant déchargées vers d'autres objets:
Il s'agit de deux opérations d'E/S différentes. En particulier, le premier ne modifie en rien le système de fichiers.
En fait, nous devons noter qu'il existe une étape intermédiaire implicite:
Votre API doit fournir chacun de ces éléments séparément sous une forme ou une autre. Comment savez-vous qu'un appelant ne voudra pas prendre une mesure sans la stocker n'importe où? Comment savez-vous qu'ils ne voudront pas obtenir une mesure d'une autre source? Comment savez-vous qu'ils ne voudront pas le stocker ailleurs que sur l'appareil? Il y a de bonnes raisons de découpler les opérations. Au minimum nu , chaque pièce individuelle doit être disponible pour tout appelant. Je ne devrais pas être obligé d'écrire la mesure dans un fichier si mon cas d'utilisation ne l'exige pas.
Par exemple, vous pouvez séparer les opérations comme celle-ci.
IMeasurer
a un moyen de récupérer la mesure:
public interface IMeasurer
{
IMeasurement Measure(int someInput);
}
Votre type de mesure pourrait être quelque chose de simple, comme un string
ou decimal
. Je n'insiste pas pour que vous ayez besoin d'une interface ou d'une classe, mais cela rend l'exemple ici plus général.
IFileAccess
a une méthode pour enregistrer des fichiers:
interface IFileAccess
{
void SaveFile(string fileContents);
}
Ensuite, vous avez besoin d'un moyen de sérialiser une mesure. Construisez-le dans la classe ou l'interface représentant une mesure, ou utilisez une méthode utilitaire:
interface IMeasurement
{
// As part of the type
string Serialize();
}
// Utility method. Makes more sense if the measurement is not a custom type.
public static string SerializeMeasurement(IMeasurement m)
{
return ...
}
On ne sait pas encore si cette opération de sérialisation est encore séparée.
Ce type de séparation améliore votre API. Il laisse l'appelant décider de ce dont il a besoin et quand, plutôt que de forcer vos idées préconçues sur ce que les E/S doivent effectuer. Les appelants devraient avoir le contrôle pour effectuer toute opération valide , que vous pensiez que cela soit utile ou non.
Une fois que vous avez des implémentations distinctes pour chaque opération, votre méthode CreateDataFile
devient juste un raccourci pour
fileAccess.SaveFile(SerializeMeasurement(measurer.Measure()));
Notamment, votre méthode ajoute très peu de valeur une fois que vous avez fait tout cela. La ligne de code ci-dessus n'est pas difficile à utiliser directement par vos appelants, et votre méthode est purement pratique tout au plus. Il devrait être et est quelque chose facultatif . Et c'est la bonne façon pour l'API de se comporter.
Une fois que toutes les parties pertinentes ont été prises en compte et que nous avons reconnu que la méthode n'est qu'une commodité, nous devons reformuler votre question:
Quel serait le cas d'utilisation le plus courant pour vos appelants?
Si le but est de rendre le cas d'utilisation typique de mesure et d'écriture sur le même tableau un peu plus pratique, il est parfaitement logique de le rendre disponible directement sur la classe Board
:
public class Board : IMeasurer, IFileAccess
{
// Interface methods...
/// <summary>
/// Convenience method to measure and immediate record measurement in
/// default location.
/// </summary>
public void ReadAndSaveMeasurement()
{
this.SaveFile(SerializeMeasurement(this.Measure()));
}
}
Si cela n'améliore pas la commodité, je ne me soucierais pas du tout de la méthode.
Cette méthode pratique soulève une autre question.
L'interface IFileAccess
doit-elle connaître le type de mesure et comment le sérialiser? Si c'est le cas, vous pouvez ajouter une méthode à IFileAccess
:
interface IFileAccess
{
void SaveFile(string fileContents);
void SaveMeasurement(IMeasurement m);
}
Maintenant, les appelants font juste ceci:
fileAccess.SaveFile(measurer.Measure());
qui est tout aussi court et probablement plus clair que votre méthode de commodité telle que conçue dans la question.
Le client consommateur ne devrait pas avoir à gérer une paire d'articles lorsqu'un seul article suffit. Dans votre cas, ils ne le font presque pas, jusqu'à l'invocation de CreateDataFile
.
La solution potentielle que vous proposez est de créer une interface dérivée combinée. Cependant, cette approche nécessite un objet unique qui implémente les deux interfaces, ce qui est plutôt contraignant, sans doute une abstraction qui fuit en ce qu'il est essentiellement personnalisé pour une implémentation particulière. Considérez à quel point ce serait compliqué si quelqu'un voulait implémenter les deux interfaces dans des objets distincts: il faudrait qu'il proxy toutes les méthodes dans l'une des interfaces afin de transmettre à l'autre objet. (FWIW, une autre option consiste à simplement fusionner les interfaces plutôt que d'exiger qu'un seul objet doive implémenter deux interfaces via une interface dérivée.)
Cependant, une autre approche moins contraignante/moins contraignante pour l'implémentation est que IFileAccess
est associé à un IMeasurer
dans la composition, de sorte que l'un d'eux est lié à et référence l'autre. (Cela augmente quelque peu l'abstraction de l'un d'entre eux, car il représente désormais également l'appariement.) Ensuite, CreateDataFile
ne peut prendre qu'une des références, par exemple IFileAccess
, et toujours obtenir l'autre si nécessaire . Votre implémentation actuelle en tant qu'objet implémentant les deux interfaces serait simplement return this;
pour la référence de composition, voici le getter pour IMeasurer
dans IFileAccess
.
Si le couplage s'avère faux à un moment donné du développement, c'est-à-dire que parfois un mesureur différent est utilisé avec le même accès aux fichiers, vous pouvez faire ce même couplage mais à un niveau supérieur à la place, ce qui signifie que l'interface supplémentaire introduite le ferait. ne pas être une interface dérivée, mais plutôt une interface qui a deux getters, associant un accès à un fichier et un mesureur ensemble via la composition plutôt que la dérivation. Le client consommateur a alors un élément dont il doit s'occuper aussi longtemps que l'appariement est maintenu, et des objets individuels à traiter (pour composer de nouveaux appariements) si nécessaire.
Sur une autre note, je pourrais demander à qui appartient CreateDataFile
, et la question va à qui est ce tiers. Nous avons déjà un client consommateur qui appelle CreateDataFile
, l'objet/classe propriétaire de CreateDataFile
et les IFileAccess
et IMeasurer
. Parfois, lorsque nous adoptons une vue plus large du contexte, des organisations alternatives, parfois meilleures, peuvent apparaître. Difficile à faire ici car le contexte est incomplet, donc juste matière à réflexion.
Certains ont signalé que CreateDataFile
en faisait trop. Je pourrais suggérer que Board
en fait trop, car l'accès à un fichier semble être une préoccupation distincte du reste du forum.
Cependant, si nous supposons que ce n'est pas une erreur, le plus gros problème est que l'interface doit être définie par le client, dans ce cas CreateDataFile
.
Le Interface Segregation Principle indique que le client ne devrait pas avoir à dépendre d'une interface plus importante que ce dont il a besoin. En empruntant la phrase de cette autre réponse , cela peut être paraphrasé comme "une interface est définie par ce dont le client a besoin".
Maintenant, il est possible de composer cette interface spécifique au client en utilisant IFileAccess
et IMeasurer
comme d'autres réponses le suggèrent, mais finalement, ce client devrait avoir une interface sur mesure pour cela.