[PYTHON] Codes, die ich oft bei SIer sehe und wie man sie behebt - Leichte Verbesserungen an stark abhängigem Code

Vorwort

Ich lasse es als Notiz, während ich mich an den Code erinnere, den ich kürzlich gesehen habe, und an den Prozess, ihn selbst zu reparieren. Es tut mir leid, wenn ich nicht verstehe.

Wie der Titel schon sagt, habe ich viel Zeit damit verbracht, den Vorgang zu überprüfen, während ich versucht habe, abhängigen Code zu verwenden. Deshalb nehme ich ihn als Reflexion auf. (Ich habe gehört, dass Kotlin beliebt ist, also habe ich es geschrieben, obwohl es ein Anfängercode ist.)

Zu verbessernder Code und Definition von Problemen

SampleFileReader und SampleFileWriter sind der Code, der ursprünglich bereitgestellt wurde. ** Der Inhalt ist texto, bitte betrachten Sie es hier als kein Problem **.

//Klasse verbessert werden
class SampleManager(private val reader: SampleFileReader,
                    private val writer: SampleFileWriter) {
    
    fun doSomething1() {
        reader.read()
        writer.writeLine("Kitto etwas tun1")
    }

    fun doSomething2() {
        reader.read()
        writer.writeLine("Kitto etwas tun2")
    }
}

//Klasse verbessert werden
class WorkFlow {
    private val readPath: String = "read.txt"
    private val writePath: String = "write.txt"
    
    //Eine Instanz von SampleManager ist verfügbar.
    //Ich möchte die Abhängigkeiten für einfache Tests getrennt halten.
    private val manager: SampleManager
        get() {
            val reader = SampleFileReader(readPath)
            val writer = SampleFileWriter(writePath)
            return SampleManager(reader, writer) //
        }

    //Verarbeitung mit einer Instanz von 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()
}

Die Probleme hier sind nachstehend als 1 und 2 definiert.

[Problem]

  1. Ich erstelle eine Instanz von "SampleManager" in der "WorkFlow" -Klasse, aber wenn ich mir die "WorkFlow" -Klasse als Testziel vorstelle, hängt dies von ** "SampleManager" ab, sodass sie getestet wird. Es gibt ein Problem, dass es schwierig ist **.
  2. Wie Sie sehen können, wenn Sie es tatsächlich schreiben und verschieben, tritt ein Fehler auf, wenn der Dateipfad, auf den der SampleFileReader verweist, von dem der SampleManager abhängt, nicht vorhanden ist. Dies ist ebenfalls verbesserungsbedürftig.

Wenn ich den WorkFlow-Test in Betracht ziehe, habe ich ein wenig den Instanziierungsprozess "SampleManager" erfunden, um die Abhängigkeiten zu trennen.

Code, den ich verbessern wollte

Antwort auf Problem 1.

Die Richtlinie bestand darin, eine "SampleManager" -Instanz in der "WorkFlow" -Klasse zu erstellen und die "WorkFlow" -Klasse ** vererbbar ** zu machen, damit der Testcode die "SampleManager" -Instanz zum Testen festlegen konnte. Insbesondere wurden die folgenden Maßnahmen ergriffen.

Machen Sie sich nicht die Mühe, den Produktionscode "offen" zu machen. Ich dachte das, aber in meinem Geschäft schien es kein Problem zu geben, wenn ich es auf "Öffnen" stellte und es reparierte, also benutzte ich "Öffnen" und Vererbung.

//Dies wird im nächsten Schritt verbessert
class SampleManager(private val reader: SampleFileReader,
                    private val writer: SampleFileWriter) {
    
    fun doSomething1() {
        reader.read()
        writer.writeLine("Kitto etwas tun1")
    }

    fun doSomething2() {
        reader.read()
        writer.writeLine("Kitto etwas tun2")
    }
}

//Kotlin ist standardmäßig nicht vererbbar, daher wird open hinzugefügt.
open class WorkFlow {
    private val readPath: String = "read.txt"
    private val writePath: String = "write.txt"
    
    private val manager: SampleManager
        get() {
            //Verwenden Sie die Werksmethode
            return createManager()
        }

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


    //Als Werksmethode extrahiert
    //Modifikatoren, die in Unterklassen überschrieben werden können`protected open`Zu
    protected open fun createManager(): SampleManager {
        val reader = SampleFileReader(readPath)
        val writer = SampleFileWriter(writePath)
        return SampleManager(reader, writer)
    }

}

//Klasse, die anstelle von WorkFlow getestet werden soll
class TestWorkFlow : WorkFlow() {
    override fun createManager(): SampleManager {
        TODO("Erstellen Sie eine Instanz von SampleManager zum Testen")
    }
}

Als ich jedoch versuchte, den TODO-Teil zu schreiben, um den SampleManager tatsächlich zum Testen zurückzugeben, Ein Fehler tritt auf, wenn der Dateipfad, auf den der SampleFileReader verweist, von dem der SampleManager abhängt, nicht vorhanden ist. Ich wünschte, ich könnte eine Instanz von "SampleManager" erstellen, aber ich benötige auch "SampleFileReader" und "SampleFileWriter", was stark abhängig ist.

class TestWorkFlow : WorkFlow() {
    override fun createManager(): SampleManager {
        val reader = SampleFileReader("Sie müssen einen vorhandenen Pfad zum Lesen angeben")
        val writer = SampleFileWriter("Zielpfad schreiben")
        return SampleManager(reader, writer)
    }
}

//Ich hoffe, Sie können sich die Instanziierung im Testcode vorstellen...
fun main(args: Array<String>) {
    val flow = TestWorkFlow()
    flow.output()
}

Also werde ich es als Antwort auf Problem 2 ein wenig verbessern.

Antwort auf Problem 2.

Hier habe ich über die Richtlinie nachgedacht, das Verhalten von "SampleManager" als Schnittstelle zu extrahieren.

//Verhalten als Schnittstelle extrahieren
interface SampleManager {
    fun doSomething1()
    fun doSomething2()
}

//Die Klasse wurde mit SampleManager in SampleManagerImpl umbenannt
class SampleManagerImpl(private val reader: SampleFileReader,
                        private val writer: SampleFileWriter) : SampleManager {

    override fun doSomething1() {
        val list = reader.read()
        println(list)
        writer.writeLine("Kitto etwas tun1")
    }

    override fun doSomething2() {
        reader.read()
        writer.writeLine("Kitto etwas tun2")
    }
}

Der Testcode ist unten.

//Pseudoklasse zum Testen. SampleManager implementiert.
//Es muss keine Datenklasse sein, es kann eine gewöhnliche Klasse sein.
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")
}

//Klasse, die anstelle von WorkFlow getestet werden soll
class TestWorkFlow : WorkFlow() {
    override fun createManager(): SampleManager {
        //Geändert, um ein Pseudoobjekt zum Testen zurückzugeben.
        return FakeSampleManager()
    }
}

Der obige Code erfordert nicht "SampleFileReader" und "SampleFileWriter".

Dies löste das Problem "Ich wünschte, ich könnte eine Instanz von" SampleManager "erstellen, aber ich brauchte auch" SampleFileReader "und" SampleFileWriter ", die stark abhängig sind" während des Tests.

Ganzer Code

Als Referenz werde ich den gesamten Code hier einfügen.

** Produktionscode **

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 etwas tun1")
    }

    override fun doSomething2() {
        reader.read()
        writer.writeLine("Kitto etwas tun2")
    }
}


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)
    }

}

** zum 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")
}

//Klasse, die anstelle von WorkFlow getestet werden soll
class TestWorkFlow : WorkFlow() {
    override fun createManager(): SampleManager {
        return FakeSampleManager()
    }
}

//Instanziierung während des Tests
fun testXX {
    val flow = TestWorkFlow()
    flow.output()
    ...
}

Selbst in einer Kultur, in der Testcode nicht so häufig geschrieben wird, müssen Wartungsentwickler zum Zeitpunkt der Entwicklung überprüfen, ob Abhängigkeiten getrennt sind und bei der Instanziierung einfach durchzuführen sind. Es macht mir nichts aus. Ich denke, es ist heute um diese Zeit.

Ich schrieb es in S. Kotlin und dachte, es wäre für eine Weile gut, weil ich voll mit Scala war. Da ich Scala erlebt hatte, waren die Lernkosten sehr niedrig (ca. 2 Stunden Grammatik). Wenn ich eine Chance habe, würde ich sie gerne ernsthaft in meinem Geschäft einsetzen.

Recommended Posts

Codes, die ich oft bei SIer sehe und wie man sie behebt - Leichte Verbesserungen an stark abhängigem Code
Umgang mit dem Fehler "Fehler beim Laden des Moduls" canberra-gtk-module ", der beim Ausführen von OpenCV auftritt
Umgang mit Fehlern bei der Installation von Whitenoise und der Bereitstellung auf Heroku
Umgang mit Fehlern bei der Installation von Python und Pip mit Choco
So erstellen Sie eine Python- und Jupyter-Ausführungsumgebung mit VSCode
Konvertieren Sie Gesichtsbilder mit PULSE in eine hohe Bildqualität, damit Sie die Poren und die Textur sehen können