web-dev-qa-db-fra.com

Est-ce que je fais mes cours trop granulaires? Comment fonctionner le principe de responsabilité unique doit-il être appliqué?

J'écris beaucoup de code qui implique trois étapes de base.

  1. Obtenir des données de quelque part.
  2. Transformer ces données.
  3. Mettre ces données quelque part.

Je finis généralement par utiliser trois types de classes - inspiré de leurs modèles de conception respectifs.

  1. Usines - pour créer un objet de certaines ressources.
  2. Médiateurs - Pour utiliser l'usine, effectuez la transformation, puis utilisez le commandant.
  3. Commandants - mettre ces données ailleurs.

Mes cours ont tendance à moi être assez petit, souvent une méthode (publique), par ex. Obtenez des données, transformez les données, effectuez des travaux, enregistrez des données. Cela conduit à une prolifération de classes, mais en général fonctionne bien.

Là où je lutterai lorsque je viens tester, je finirai par des tests étroitement couplés. Par example;

  • Factory - lit les fichiers du disque.
  • Commandant - écrit des fichiers sur le disque.

Je ne peux pas tester un sans l'autre. Je pourrais écrire un code "test" supplémentaire pour faire de disque en lecture/écriture de disque aussi, mais je me répète.

En regardant .net, la classe fichier prend une approche différente, il combine les responsabilités (de mon) usine et commandant ensemble. Il a des fonctions pour créer, supprimer, exister et lire tout au même endroit.

Devrais-je regarder pour suivre l'exemple de .NET et combiner - en particulier lors de la gestion des ressources externes - mes classes ensemble? Le code qu'il a toujours couplé, mais c'est plus intentionnel - cela se produit à la mise en œuvre initiale, plutôt que dans les tests.

Mon problème est-il ici que j'ai appliqué un principe de responsabilité unique quelque peu surnomément? J'ai des cours séparés responsables de la lecture et de l'écriture. Lorsque je pouvais avoir une classe combinée responsable de traiter avec une ressource particulière, par exemple Disque système.

9
James Wood

Généralement, vous avez la bonne idée.

Obtenir des données de quelque part. Transformer ces données. Mettre ces données quelque part.

On dirait que vous avez trois responsabilités. OMI Le "médiateur" peut faire beaucoup. Je pense que vous devriez commencer par modéliser vos trois responsabilités:

interface Reader[T] {
    def read(): T
}

interface Transformer[T, U] {
    def transform(t: T): U
}

interface Writer[T] {
    def write(t: T): void
}

Ensuite, un programme peut être exprimé comme suit:

def program[T, U](reader: Reader[T], 
                  transformer: Transformer[T, U], 
                  writer: Writer[U]): void =
    writer.write(transformer.transform(reader.read()))

Cela conduit à une prolifération de classes

Je ne pense pas que ce soit un problème. OMI beaucoup de petites classes cohérentes et testables sont meilleures que de grandes classes moins cohésives.

Là où je lutterai lorsque je viens tester, je finirai par des tests étroitement couplés. Je ne peux pas tester un sans l'autre.

Chaque pièce doit être testable de manière indépendante. Modelé ci-dessus, vous pouvez représenter la lecture/l'écriture dans un fichier comme suit:

class FileReader(fileName: String) implements Reader[String] {
    override read(): String = // read file into string
}

class FileWriter(fileName: String) implements Writer[String] {
    override write(str: String) = // write str to file
}

Vous pouvez écrire des tests d'intégration pour tester ces classes pour vérifier qu'ils lisent et écrivent au système de fichiers. Le reste de la logique peut être écrit comme des transformations. Par exemple, si les fichiers sont au format JSON, vous pouvez transformer le Strings.

class JsonParser implements Transformer[String, Json] {
    override transform(str: String): Json = // parse as json
}

Ensuite, vous pouvez transformer en objets appropriés:

class FooParser implements Transformer[Json, Foo] {
    override transform(json: Json): Foo = // ...
}

Chacun d'entre eux est testable de manière indépendante. Vous pouvez également trouver un test program ci-dessus en moqueur reader, transformer et writer.

2
Samuel

Je finis par des tests étroitement couplés. Par example;

  • Factory - lit les fichiers du disque.
  • Commandant - écrit des fichiers sur le disque.

Donc, le focus ici est sur Qu'est-ce qui les couplit ensemble. Passez-vous un objet entre les deux (tels qu'un File?) Alors c'est le fichier qu'ils sont couplés, pas l'un de l'autre.

D'après ce que vous avez dit, vous avez séparé vos cours. Le piège est que vous les testez ensemble parce que c'est plus facile ou "a du sens".

Pourquoi avez-vous besoin de l'entrée sur Commander pour venir d'un disque? Tout cela se soucie de l'écriture à l'aide d'une certaine entrée, vous pouvez alors vérifier que cela a écrit le fichier correctement en utilisant ce qui est dans le test.

La pièce réelle que vous testez pour Factory est 'va-t-elle lire ce fichier correctement et émettre la bonne chose'? Donc, maquette le fichier avant de le lire dans le test.

Vous pouvez également tester que l'usine et le commandant fonctionnent lorsqu'ils sont couplés ensemble conviennent - il tombe en ligne avec des tests d'intégration très heureusement. La question ici est davantage une question de savoir si votre unité peut ou non tester séparément.

2
Erdrik Ironrose