Wie kann man Code in einen gängigen Code umwandeln?

16

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, Eund 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 SNist 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?

Donotalo
quelle
Das Refactoring- Buch von Martin Fowler enthält eine Reihe spezifischer Techniken für das Refactoring von Code, die Sie möglicherweise nützlich finden.
Allan

Antworten:

14

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

class Base {
  method ExecJob() {
    S1();
    S2();
    S3();
    S4();
    S5();
  }
  method S1() { //concrete implementation }
  method S3() { //concrete implementation }
  method S4() { //concrete implementation}
  abstract method S2();
  abstract method S5();
}

class A::Base {
  method S2() {//concrete implementation}
  method S5() {//concrete implementation}
}

class B::Base {
  method S2() { // empty implementation}
  method S5() {//concrete implementation}
}

class C::Base {
  method S2() { // empty implementation}
  method S5() { // empty implementation}
}

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

interface JobExecuter {
  void executeJob();
}
class A::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S2();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class B::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class C::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
  }
}

class Base{
   void ExecJob(JobExecuter executer){
       executer->executeJob();
   }
}

class Helper{
    void S1(){//Implementation} 
    void S2(){//Implementation}
    void S3(){//Implementation}
    void S4(){//Implementation} 
    void S5(){//Implementation}
}

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.

Guven
quelle
Das Hauptproblem, das ich bei der in "Schritt 2" beschriebenen Lösung sehe, besteht darin, dass die konkrete Implementierung von S5 zweimal vorhanden ist.
user281377
1
Ja, die Code-Vervielfältigung wird nicht beseitigt! Und das ist ein weiterer Indikator dafür, dass die Abstraktion nicht funktioniert. Ich wollte nur den zweiten Schritt ausführen, um zu zeigen, wie ich über den Prozess denke. Ein schrittweiser Ansatz, um etwas besseres zu finden.
Guven
1
+1 Sehr gute Strategie (und ich spreche nicht über das Muster )!
Jordão
7

Sie tun tatsächlich das Richtige. Ich sage das, weil:

  1. Wenn Sie den Code für eine allgemeine Aufgabenfunktionalität ändern müssen, müssen Sie ihn nicht in allen 6 Klassen ändern, die den Code enthalten würden, wenn Sie ihn nicht in eine allgemeine Klasse schreiben würden.
  2. Die Anzahl der Codezeilen wird verringert.
prema
quelle
3

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.

Tom Kerr
quelle
3

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.

Mikro
quelle
Dies sollte der erste Schritt sein, bevor ein Refactoring durchgeführt werden soll. Bis der Code so weit verstanden wurde, dass er abgebildet werden konnte (uml oder eine andere Karte der Wildnis), wird das Refactoring im Dunkeln geplant.
Kzqai
3

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

A::ExecJob()
{
    S1; // many lines of code
    S2; // many lines of code
    S3; // many lines of code
    S4; // many lines of code
    S5; // many lines of code
}

du erhältst

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

A:S1()
{
   // many lines of code
}

A:S2()
{
   // many lines of code
}

A:S3()
{
   // many lines of code
}

A:S4()
{
   // many lines of code
}

A:S5()
{
   // many lines of code
}

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.

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

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.

user281377
quelle
2

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

Ameise
quelle
1

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, Fbedeutet 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()auf S5()Ihrer Basisklasse. Danach sollten die "ExecJob" -Funktionen so aussehen:

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

C::ExecJob()
{
    S1();
    S3();
    S4();
}

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.

Doc Brown
quelle