[PYTHON] Codes que je vois souvent en travaillant chez SIer et comment les corriger - Légères améliorations du code hautement dépendant

Préface

Je le laisse comme mémo en me souvenant du code que j'ai vu récemment et du processus de réparation moi-même. Je suis désolé si je ne comprends pas.

Comme le titre l'indique, j'ai passé beaucoup de temps à vérifier l'opération tout en essayant d'utiliser du code dépendant, donc je l'enregistre comme une réflexion. (J'ai entendu dire que Kotlin est populaire, alors je l'ai écrit bien que ce soit un code pour débutant.)

Code à améliorer et définition des problèmes

«SampleFileReader» et «SampleFileWriter» sont le code fourni à l'origine. ** Le contenu est texto, veuillez donc le considérer comme aucun problème ici **

//Classe à améliorer
class SampleManager(private val reader: SampleFileReader,
                    private val writer: SampleFileWriter) {
    
    fun doSomething1() {
        reader.read()
        writer.writeLine("Kitto faire quelque chose1")
    }

    fun doSomething2() {
        reader.read()
        writer.writeLine("Kitto faire quelque chose2")
    }
}

//Classe à améliorer
class WorkFlow {
    private val readPath: String = "read.txt"
    private val writePath: String = "write.txt"
    
    //Une instance de SampleManager est disponible.
    //Je veux garder les dépendances séparées pour des tests faciles.
    private val manager: SampleManager
        get() {
            val reader = SampleFileReader(readPath)
            val writer = SampleFileWriter(writePath)
            return SampleManager(reader, writer) //
        }

    //Traitement à l'aide d'une instance de SampleManager
    fun output() {
        manager.doSomething1()
        manager.doSomething2()
    }
}

class SampleFileReader(private val filePath: String) {
    private val file: File?
        get() = Paths.get(filePath)?.toFile()

    fun read(): Iterator<String> {
        val br = BufferedReader(InputStreamReader(FileInputStream(file)))
        return br.readLines().iterator()
    }
}

class SampleFileWriter(private val filePath: String) {
    private val bw = BufferedWriter(OutputStreamWriter(FileOutputStream(file), "UTF-8"))

    private val file: File?
        get() = Paths.get(filePath)?.toFile()

    fun writeLine(str: String) = bw.write(str)

    fun close() = bw.close()
}

Les problèmes ici sont définis comme 1 et 2 ci-dessous.

[problème]

  1. Je crée une instance de SampleManager dans la classe WorkFlow, mais quand je considère la classe WorkFlow comme une cible de test, cela dépend de ** SampleManager, donc elle est testée. Il y a un problème que c'est difficile **.
  2. Comme vous pouvez le voir en l'écrivant et en le déplaçant réellement, une erreur se produira si le chemin du fichier référencé par le «SampleFileReader» dont dépend le «SampleManager» n'existe pas réellement. Ceci est également sujet à amélioration.

Donc, en considérant le test WorkFlow, j'ai inventé un peu le processus d'instanciation SampleManager pour séparer les dépendances.

Code que j'ai essayé d'améliorer

Réponse au problème 1.

La politique était de créer une instance SampleManager dans la classe WorkFlow et de rendre la classe WorkFlow ** héritable ** afin que le code de test puisse définir l'instance SampleManager pour le test. Plus précisément, les mesures suivantes ont été prises.

Ne prenez pas la peine de rendre le code de production «ouvert». Je pensais que, mais dans mon entreprise, il semblait qu'il n'y aurait pas de problème même si je le changeais en «open», alors j'ai utilisé «open» et en ai hérité.

//Cela sera amélioré à la prochaine étape
class SampleManager(private val reader: SampleFileReader,
                    private val writer: SampleFileWriter) {
    
    fun doSomething1() {
        reader.read()
        writer.writeLine("Kitto faire quelque chose1")
    }

    fun doSomething2() {
        reader.read()
        writer.writeLine("Kitto faire quelque chose2")
    }
}

//Kotlin n'est pas héritable par défaut, donc open est ajouté.
open class WorkFlow {
    private val readPath: String = "read.txt"
    private val writePath: String = "write.txt"
    
    private val manager: SampleManager
        get() {
            //Utiliser la méthode d'usine
            return createManager()
        }

    fun output() {
        manager.doSomething1()
        manager.doSomething2()
    }


    //Extrait comme méthode d'usine
    //Modificateurs pouvant être remplacés dans les sous-classes`protected open`À
    protected open fun createManager(): SampleManager {
        val reader = SampleFileReader(readPath)
        val writer = SampleFileWriter(writePath)
        return SampleManager(reader, writer)
    }

}

//Classe à tester au lieu de WorkFlow
class TestWorkFlow : WorkFlow() {
    override fun createManager(): SampleManager {
        TODO("Créer une instance de SampleManager pour le test")
    }
}

Cependant, quand j'ai essayé d'écrire la partie TODO pour renvoyer réellement le SampleManager pour le test, Une erreur se produira si le chemin du fichier référencé par le «SampleFileReader» dont dépend le «SampleManager» n'existe pas. J'aimerais pouvoir créer une instance de SampleManager, mais j'ai également besoin deSampleFileReader et de SampleFileWriter, qui est fortement dépendant.

class TestWorkFlow : WorkFlow() {
    override fun createManager(): SampleManager {
        val reader = SampleFileReader("Vous devez spécifier un chemin existant pour lire")
        val writer = SampleFileWriter("Écrire le chemin de destination")
        return SampleManager(reader, writer)
    }
}

//J'espère que vous pouvez imaginer l'instanciation dans le code de test...
fun main(args: Array<String>) {
    val flow = TestWorkFlow()
    flow.output()
}

Donc, je vais l'améliorer un peu en réponse au problème 2.

Réponse au problème 2.

Ici, j'ai pensé à la politique d'extraction du comportement de SampleManager en tant qu'interface.

//Extraire le comportement en tant qu'interface
interface SampleManager {
    fun doSomething1()
    fun doSomething2()
}

//Renommé la classe avec SampleManager en SampleManagerImpl
class SampleManagerImpl(private val reader: SampleFileReader,
                        private val writer: SampleFileWriter) : SampleManager {

    override fun doSomething1() {
        val list = reader.read()
        println(list)
        writer.writeLine("Kitto faire quelque chose1")
    }

    override fun doSomething2() {
        reader.read()
        writer.writeLine("Kitto faire quelque chose2")
    }
}

Le code de test est ci-dessous.

//Pseudo classe pour les tests. Implémentation de SampleManager.
//Il n'est pas nécessaire que ce soit une classe de données, cela peut être une classe ordinaire.
data class FakeSampleManager(val prop1: String = "alice", 
                             val prop2: String = "bob") : SampleManager {
    override fun doSomething1() = println("$prop1 $prop1 $prop1")
    override fun doSomething2() = println("$prop2 $prop2 $prop2")
}

//Classe à tester au lieu de WorkFlow
class TestWorkFlow : WorkFlow() {
    override fun createManager(): SampleManager {
        //Modifié pour renvoyer un pseudo objet pour le test.
        return FakeSampleManager()
    }
}

Le code ci-dessus ne nécessite pas «SampleFileReader» et «SampleFileWriter».

Cela a résolu le problème de "J'aimerais pouvoir créer une instance de" SampleManager ", mais j'avais également besoin de" SampleFileReader "et de" SampleFileWriter ", qui est très dépendante" pendant les tests.

Code entier

Pour référence, je vais mettre tout le code ici.

** Code de production **

class SampleFileReader(private val filePath: String) {
    private val file: File?
        get() = Paths.get(filePath)?.toFile()

    fun read(): List<String> {
        val br = BufferedReader(InputStreamReader(FileInputStream(file)))
        return br.readLines()
    }
}


class SampleFileWriter(private val filePath: String) {
    private val bw = BufferedWriter(OutputStreamWriter(FileOutputStream(file), "UTF-8"))

    private val file: File?
        get() = Paths.get(filePath)?.toFile()

    fun writeLine(str: String) = bw.write(str)

    fun close() = bw.close()
}

interface SampleManager {
    fun doSomething1()
    fun doSomething2()
}

class SampleManagerImpl(private val reader: SampleFileReader,
                        private val writer: SampleFileWriter) : SampleManager {

    override fun doSomething1() {
        val list = reader.read()
        println(list)
        writer.writeLine("Kitto faire quelque chose1")
    }

    override fun doSomething2() {
        reader.read()
        writer.writeLine("Kitto faire quelque chose2")
    }
}


open class WorkFlow {
    private val readPath: String = "read.txt"
    private val writePath: String = "write.txt"

    private val manager: SampleManager
        get() {
            return createManager()
        }

    fun output() {
        manager.doSomething1()
        manager.doSomething2()
    }
    
    protected open fun createManager(): SampleManager {
        val reader = SampleFileReader(readPath)
        val writer = SampleFileWriter(writePath)
        return SampleManagerImpl(reader, writer)
    }

}

** pour test **

data class FakeSampleManager(val prop1: String = "alice", 
                             val prop2: String = "bob") : SampleManager {
    override fun doSomething1() = println("$prop1 $prop1 $prop1")
    override fun doSomething2() = println("$prop2 $prop2 $prop2")
}

//Classe à tester au lieu de WorkFlow
class TestWorkFlow : WorkFlow() {
    override fun createManager(): SampleManager {
        return FakeSampleManager()
    }
}

//Instanciation pendant le test
fun testXX {
    val flow = TestWorkFlow()
    flow.output()
    ...
}

Même dans une culture où le code de test n'est pas tellement écrit, les développeurs de maintenance doivent vérifier au moment du développement si les dépendances sont séparées et faciles à faire lors de l'instanciation. Ça ne me dérange pas. Je pense que c'est vers cette heure aujourd'hui.

Je l'ai écrit en p.s. Kotlin et j'ai pensé que ce serait bien pendant un moment parce que j'étais plein de Scala. Depuis que j'avais expérimenté Scala, le coût d'apprentissage était très faible (environ 2 heures de grammaire). Si j'ai une chance, je voudrais l'utiliser sérieusement dans mon entreprise.

Recommended Posts

Codes que je vois souvent en travaillant chez SIer et comment les corriger - Légères améliorations du code hautement dépendant
Codes que je vois souvent en travaillant chez SIer et comment les corriger - violation de la loi de Demeter
Comment traiter l'erreur "Impossible de charger le module" canberra-gtk-module "qui apparaît lorsque vous exécutez OpenCV
Comment gérer les erreurs lors de l'installation de whitenoise et du déploiement sur Heroku
Comment gérer les erreurs lors de l'installation de Python et de pip avec choco
Comment créer un environnement d'exécution Python et Jupyter avec VSCode
Convertissez l'image du visage en une qualité d'image élevée avec PULSE afin que vous puissiez voir les pores et la texture