Hintergrund
Ich arbeite an einem laufenden C # -Projekt. Ich bin kein C # -Programmierer, hauptsächlich ein C ++ - Programmierer. Mir wurden also grundsätzlich einfache und umgestaltende Aufgaben übertragen.
Der Code ist ein Durcheinander. Es ist ein riesiges Projekt. Da unser Kunde häufige Releases mit neuen Funktionen und Fehlerkorrekturen forderte, waren alle anderen Entwickler gezwungen, beim Codieren Brute-Force-Ansätze zu wählen. Der Code ist kaum zu pflegen und alle anderen Entwickler stimmen dem zu.
Ich bin nicht hier, um zu diskutieren, ob sie es richtig gemacht haben. Während ich überarbeite, frage ich mich, ob ich es richtig mache, da mein überarbeiteter Code komplex erscheint! Hier ist meine Aufgabe als einfaches Beispiel.
Problem
Es gibt sechs Klassen: A
, B
, C
, D
, E
und F
. Alle Klassen haben eine Funktion ExecJob()
. Alle sechs Implementierungen sind sehr ähnlich. Grundsätzlich wurde zunächst A::ExecJob()
geschrieben. Dann wurde eine etwas andere Version benötigt, die B::ExecJob()
durch Copy-Paste-Modifikation von implementiert wurde A::ExecJob()
. Wenn eine andere etwas andere Version benötigt wurde, C::ExecJob()
wurde geschrieben und so weiter. Alle sechs Implementierungen haben einen gemeinsamen Code, dann verschiedene Codezeilen, dann wieder einen gemeinsamen Code und so weiter. Hier ist ein einfaches Beispiel für die Implementierungen:
A::ExecJob()
{
S1;
S2;
S3;
S4;
S5;
}
B::ExecJob()
{
S1;
S3;
S4;
S5;
}
C::ExecJob()
{
S1;
S3;
S4;
}
Wo SN
ist eine Gruppe von genau gleichen Aussagen.
Um sie gemeinsam zu nutzen, habe ich eine weitere Klasse erstellt und den gemeinsamen Code in eine Funktion verschoben. Mit dem Parameter steuern Sie, welche Anweisungsgruppe ausgeführt werden soll:
Base::CommonTask(param)
{
S1;
if (param.s2) S2;
S3;
S4;
if (param.s5) S5;
}
A::ExecJob() // A inherits Base
{
param.s2 = true;
param.s5 = true;
CommonTask(param);
}
B::ExecJob() // B inherits Base
{
param.s2 = false;
param.s5 = true;
CommonTask(param);
}
C::ExecJob() // C inherits Base
{
param.s2 = false;
param.s5 = false;
CommonTask(param);
}
Beachten Sie, dass in diesem Beispiel nur drei Klassen und stark vereinfachte Anweisungen verwendet werden. In der Praxis CommonTask()
sieht die Funktion bei all diesen Parameterprüfungen sehr komplex aus, und es gibt viel mehr Anweisungen. In echtem Code gibt es auch mehrere CommonTask()
Funktionen , die aussehen.
Obwohl alle Implementierungen gemeinsamen Code verwenden und die ExecJob()
Funktionen besser aussehen, gibt es zwei Probleme, die mich stören:
- Bei jeder Änderung
CommonTask()
müssen alle sechs (und möglicherweise in Zukunft noch weitere) Funktionen getestet werden. CommonTask()
ist schon komplex. Mit der Zeit wird es komplexer.
Mache ich es richtig?
quelle
Antworten:
Ja, Sie sind absolut auf dem richtigen Weg!
Nach meiner Erfahrung ist mir aufgefallen, dass die Änderungen bei komplizierten Dingen in kleinen Schritten erfolgen. Was Sie getan haben, ist Schritt 1 im Evolutionsprozess (oder Refactoring-Prozess). Hier ist Schritt 2 und Schritt 3:
Schritt 2
Dies ist das 'Template Design Pattern' und es ist einen Schritt voraus im Refactoring-Prozess. Wenn sich die Basisklasse ändert, müssen die Unterklassen (A, B, C) nicht betroffen sein. Sie können relativ einfach neue Unterklassen hinzufügen. Anhand des obigen Bildes können Sie jedoch erkennen, dass die Abstraktion fehlerhaft ist. Die Notwendigkeit einer „leeren Implementierung“ ist ein guter Indikator. Es zeigt, dass etwas mit Ihrer Abstraktion nicht stimmt. Es mag kurzfristig eine akzeptable Lösung gewesen sein, aber es scheint eine bessere zu geben.
Schritt 3
Dies ist das 'Strategy Design Pattern' und scheint gut zu Ihrem Fall zu passen. Es gibt verschiedene Strategien, um den Job auszuführen, und jede Klasse (A, B, C) implementiert ihn anders.
Ich bin sicher, dass es in diesem Prozess einen Schritt 4 oder 5 gibt oder viel bessere Refactoring-Ansätze. Auf diese Weise können Sie jedoch doppelten Code entfernen und sicherstellen, dass die Änderungen lokalisiert sind.
quelle
Sie tun tatsächlich das Richtige. Ich sage das, weil:
quelle
Sie sehen diese Art von Code, der sich viel mit ereignisgesteuertem Design (insbesondere .NET) teilt. Der am besten zu wartende Weg ist es, Ihr gemeinsames Verhalten so klein wie möglich zu halten.
Lassen Sie den High-Level-Code eine Reihe kleiner Methoden wiederverwenden, und lassen Sie den High-Level-Code aus der gemeinsam genutzten Basis heraus.
Sie werden viel Kesselblech in Ihren Blatt- / Beton-Implementierungen haben. Keine Panik, es ist in Ordnung. All dieser Code ist direkt und leicht zu verstehen. Sie müssen es gelegentlich neu anordnen, wenn etwas kaputt geht, aber es ist leicht zu ändern.
Sie werden viele Muster im High-Level-Code sehen. Manchmal sind sie real, meistens nicht. Die "Konfigurationen" der fünf Parameter dort oben sehen ähnlich aus, sind es aber nicht. Das sind drei völlig unterschiedliche Strategien.
Beachten Sie auch, dass Sie all dies mit der Komposition erledigen können und sich keine Gedanken über Vererbung machen müssen. Sie werden weniger Kopplung haben.
quelle
Wenn ich Sie wäre, würde ich am Anfang wahrscheinlich einen weiteren Schritt hinzufügen: eine UML-basierte Studie.
Das Umgestalten des Codes, in dem alle gemeinsamen Teile zusammengeführt werden, ist nicht immer der beste Schritt. Es klingt eher nach einer vorübergehenden Lösung als nach einem guten Ansatz.
Zeichnet ein UML-Schema, hält die Dinge einfach, aber effektiv, bedenkt einige grundlegende Konzepte über Ihr Projekt wie "Was soll diese Software tun?" "Was ist der beste Weg, um dieses Stück Software abstrakt, modular, erweiterbar, ... etc etc zu halten?" "Wie kann ich die Kapselung am besten implementieren?"
Ich sage nur folgendes: Kümmere dich jetzt nicht um den Code, du musst dich nur um die Logik kümmern, wenn du eine klare Logik im Hinterkopf hast, kann der Rest zu einer wirklich einfachen Aufgabe werden, am Ende all dieser Art von Problemen, denen Sie gegenüberstehen, wird nur durch eine schlechte Logik verursacht.
quelle
Der allererste Schritt, egal wohin das führt, sollte darin bestehen, die scheinbar große Methode zu brechen
A::ExecJob
in kleinere Teile zu zerlegen.Deshalb statt
du erhältst
Ab hier gibt es viele Möglichkeiten. Meine Meinung dazu: Machen Sie A zur Basisklasse Ihrer Klassenhierarchie und zu ExecJob, und es wird einfach, B, C, ... ohne zu viel Kopieren und Einfügen zu erstellen. Ersetzen Sie ExecJob (jetzt ein Fünfliner) durch einen modifizierten Ausführung.
Aber warum gibt es überhaupt so viele Kurse? Vielleicht können Sie sie alle durch eine einzelne Klasse ersetzen, die einen Konstruktor hat, der angibt, welche Aktionen erforderlich sind
ExecJob
.quelle
Ich stimme den anderen Antworten zu, dass Ihr Ansatz in die richtige Richtung geht, obwohl ich nicht der Meinung bin, dass Vererbung der beste Weg ist, um gemeinsamen Code zu implementieren - ich bevorzuge die Komposition. Aus den C ++ - FAQ, die es besser erklären können, als ich es jemals könnte: http://www.parashift.com/c++-faq/priv-inherit-vs-compos.html
quelle
Zunächst sollten Sie sicherstellen, dass die Vererbung hier wirklich das richtige Werkzeug für den Job ist - nur weil Sie einen gemeinsamen Platz für Funktionen benötigen, die von Ihren Klassen verwendet werden
A
,F
bedeutet dies nicht, dass eine gemeinsame Basisklasse hier das Richtige ist - manchmal ein separater Helfer Klasse macht den Job besser. Es kann sein, es kann nicht sein. Das hängt davon ab, ob zwischen A und F und Ihrer gemeinsamen Basisklasse ein "Ist-Ist" -Verhältnis besteht, was bei künstlichen Namen wie AF unmöglich zu sagen ist. Hier finden Sie einen Blogbeitrag zu diesem Thema.Nehmen wir an, Sie entscheiden, dass die gemeinsame Basisklasse in Ihrem Fall die richtige ist. Jeweils implementiert in einem separaten Verfahren dann das zweite , was ich tun würde , ist sicherzustellen , dass Ihre Codefragmente S1 bis S5 machen
S1()
aufS5()
Ihrer Basisklasse. Danach sollten die "ExecJob" -Funktionen so aussehen:Wie Sie jetzt sehen, sind S1 bis S5 nur Methodenaufrufe, keine Codeblöcke mehr, die Codeduplizierung wurde fast vollständig entfernt, und Sie müssen keine Parameter mehr überprüfen, um das Problem der zunehmenden Komplexität zu vermeiden, das möglicherweise auftritt Andernfalls.
Schließlich, aber nur als dritter Schritt (!), Könnten Sie darüber nachdenken, all diese ExecJob-Methoden in einer Ihrer Basisklassen zu kombinieren, wobei die Ausführung dieser Teile durch Parameter gesteuert werden kann, wie Sie es vorgeschlagen haben, oder durch Verwendung von Schablonenmethodenmuster. Sie müssen selbst entscheiden, ob sich der Aufwand in Ihrem Fall lohnt, basierend auf dem tatsächlichen Code.
Aber meiner Meinung nach ist die grundlegende Technik, um große Methoden in kleine Methoden zu zerlegen, viel wichtiger, um Code-Duplikationen zu vermeiden, als das Anwenden von Mustern.
quelle