Passer l'objet deux fois à la même méthode ou consolider avec l'interface combinée?


15

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 boardFileAccesset boardMeasurersont la même instance d'un Boardobjet qui implémente à la fois IFileAccesset IMeasurer. IMeasurerest 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 IFileAccess. Boardest situé dans un projet distinct.

Je suis arrivé à la conclusion que CreateDataFilefaire une chose en faisant 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 une mesure et écrire dans un fichier comme appels de méthode séparés.

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 IDataFileCreatorqui va s'étendre IFileAccess, IMeasurerpuis avoir une implémentation contenant une Boardinstance qui appellera simplement les Boardméthodes requises . Étant donné que le même objet de carte serait toujours utilisé pour la mesure et l'écriture de fichiers, est-ce une mauvaise pratique de passer deux fois le même objet à une méthode? Dans l'affirmative, l'utilisation d'une interface locale et la mise en œuvre est-elle une solution appropriée?


2
Il est difficile, voire impossible de distinguer l'intention de votre code à partir des noms que vous utilisez. Une interface nommée IDataFileCreator transmise à une méthode nommée CreateDataFile est ahurissante. Concurrencent-ils la responsabilité de conserver les données? De quelle classe CreateDataFile est-il une méthode de toute façon? La mesure n'a rien à voir avec des données persistantes, donc c'est clair. Votre question ne concerne pas le plus gros problème que vous rencontrez avec votre code.
Martin Maat

Est-il jamais envisageable que votre objet d'accès au fichier et votre objet mesureur soient deux objets différents? Je dirais oui. Si vous changez maintenant, vous devrez changer de nouveau dans la version 2 supports que la prise de mesures à travers le réseau.
user253751

2
Voici une autre question cependant: pourquoi l'accès aux fichiers de données et les objets de mesure sont-ils les mêmes en premier lieu?
user253751

Réponses:


40

Non, c'est parfaitement bien. Cela signifie simplement que l'API est sur-conçue par rapport à votre application actuelle .

Mais cela ne prouve pas qu'il n'y aura jamais de cas d'utilisation dans lequel la source de données et le mesureur sont différents. Le but 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.


7

Je suis 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 particulière que son contrat devrait clarifier.


4

Je suis arrivé à la conclusion que CreateDataFilefaire une chose en faisant 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 une mesure et écrire dans un fichier comme appels de méthode séparés.

Je pense que c'est votre problème, en fait. La méthode ne fait rien. Il exécute deux opérations distinctes qui impliquent des E / S vers différents appareils , toutes deux déchargées sur d'autres objets:

  • Récupérer une mesure
  • Enregistrez ce résultat dans un fichier quelque part

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 y a une étape intermédiaire implicite:

  • Récupérer une mesure
  • Sérialiser la mesure dans un format connu
  • Enregistrer la mesure sérialisée dans un fichier

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. A un strict minimum, chaque pièce doit être disponible pour tous les appels. 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 peut être quelque chose de simple, comme un stringou 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 cela 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 permet à l' appelant de décider de ce dont il a besoin et quand, plutôt que de forcer vos idées préconçues sur les E / S à effectuer. Les appelants doivent avoir le contrôle pour effectuer toute opération valide , que vous pensiez que c'est utile ou non.

Une fois que vous avez des implémentations distinctes pour chaque opération, votre CreateDataFileméthode devient simplement 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. Cela devrait être et est quelque chose de 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 Boarddirectement sur la classe:

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' IFileAccessinterface doit-elle connaître le type de mesure et comment le sérialiser? Si oui, vous pouvez ajouter une méthode pour 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.


2

Le client consommateur ne devrait pas avoir à traiter avec 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 objet doive implémenter deux interfaces via une interface dérivée.)

Cependant, une autre approche qui est moins contraignante / dictée pour l'implémentation est celle qui IFileAccessest associée à une IMeasurercomposition in, de sorte que l'une d'entre elles est liée à 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.) Il CreateDataFilene pourrait alors prendre qu'une seule des références, par exemple IFileAccess, et obtenir l'autre au besoin. Votre implémentation actuelle en tant qu'objet qui implémente les deux interfaces serait simplement return this;pour la référence de composition, ici le getter pour IMeasurerin 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à quelques clients qui consomment invoque CreateDataFile, l'objet / classe propriétaire de CreateDataFile, et IFileAccesset 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.


0

Certains ont évoqué que cela CreateDataFilefait trop. Je pourrais suggérer que cela Boardfait 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 principe de séparation des interfaces stipule 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 IFileAccesset IMeasurercomme le suggèrent d'autres réponses, mais en fin de compte, ce client devrait avoir une interface sur mesure.


@Downvoter - Qu'en est-il de incorrect ou d'amélioration?
Xtros
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.