Leide ich unter Überbeanspruchung der Kapselung?

11

Ich habe in verschiedenen Projekten etwas in meinem Code bemerkt, das mir nach Code riecht und etwas Schlechtes zu tun ist, aber ich kann damit nicht umgehen.

Beim Versuch, "sauberen Code" zu schreiben, neige ich dazu, private Methoden zu häufig zu verwenden, um das Lesen meines Codes zu erleichtern. Das Problem ist, dass der Code zwar sauberer ist, aber auch schwieriger zu testen ist (ja, ich weiß, dass ich private Methoden testen kann ...), und im Allgemeinen scheint es mir eine schlechte Angewohnheit zu sein.

Hier ist ein Beispiel für eine Klasse, die einige Daten aus einer CSV-Datei liest und eine Gruppe von Kunden zurückgibt (ein anderes Objekt mit verschiedenen Feldern und Attributen).

public class GroupOfCustomersImporter {
    //... Call fields ....
    public GroupOfCustomersImporter(String filePath) {
        this.filePath = filePath;
        customers = new HashSet<Customer>();
        createCSVReader();
        read();
        constructTTRP_Instance();
    }

    private void createCSVReader() {
        //....
    }

    private void read() {
        //.... Reades the file and initializes the class attributes
    }

    private void readFirstLine(String[] inputLine) {
        //.... Method used by the read() method
    }

    private void readSecondLine(String[] inputLine) {
        //.... Method used by the read() method
    }

    private void readCustomerLine(String[] inputLine) { 
        //.... Method used by the read() method
    }

    private void constructGroupOfCustomers() {
        //this.groupOfCustomers = new GroupOfCustomers(**attributes of the class**);
    }

    public GroupOfCustomers getConstructedGroupOfCustomers() {
        return this.GroupOfCustomers;
    }
}

Wie Sie sehen, verfügt die Klasse nur über einen Konstruktor, der einige private Methoden aufruft, um die Aufgabe zu erledigen. Ich weiß, dass dies im Allgemeinen keine gute Vorgehensweise ist, aber ich ziehe es vor, alle Funktionen in der Klasse zu kapseln, anstatt die Methoden in diesem Fall öffentlich zu machen Ein Kunde sollte folgendermaßen arbeiten:

GroupOfCustomersImporter importer = new GroupOfCustomersImporter(filepath)
importer.createCSVReader();
read();
GroupOfCustomer group = constructGoupOfCustomerInstance();

Ich bevorzuge dies, weil ich keine nutzlosen Codezeilen in den Seitencode des Clients einfügen möchte, die die Clientklasse mit Implementierungsdetails belästigen.

Also, ist das eigentlich eine schlechte Angewohnheit? Wenn ja, wie kann ich das vermeiden? Bitte beachten Sie, dass dies nur ein einfaches Beispiel ist. Stellen Sie sich die gleiche Situation in etwas Komplexerem vor.

Florents Tselai
quelle

Antworten:

17

Ich denke, Sie sind tatsächlich auf dem richtigen Weg, um Implementierungsdetails vor dem Client zu verbergen. Sie möchten Ihre Klasse so gestalten, dass die Schnittstelle, die der Client sieht, die einfachste und präziseste API ist, die Sie erstellen können. Der Client wird nicht nur nicht mit Implementierungsdetails "belästigt", sondern Sie können auch den zugrunde liegenden Code umgestalten, ohne befürchten zu müssen, dass Aufrufer dieses Codes geändert werden müssen. Das Reduzieren der Kopplung zwischen verschiedenen Modulen hat einige echte Vorteile, und Sie sollten dies auf jeden Fall anstreben.

Wenn Sie also den Ratschlägen folgen, die ich gerade gegeben habe, erhalten Sie etwas, das Sie bereits in Ihrem Code bemerkt haben, eine ganze Reihe von Logik, die hinter einer öffentlichen Schnittstelle verborgen bleibt und nicht leicht zugänglich ist.

Im Idealfall sollten Sie in der Lage sein, Ihre Klasse nur anhand ihrer öffentlichen Schnittstelle zu testen. Wenn sie externe Abhängigkeiten aufweist, müssen Sie möglicherweise gefälschte / mock / stub-Objekte einführen, um den zu testenden Code zu isolieren.

Wenn Sie dies jedoch tun und immer noch das Gefühl haben, dass Sie nicht jeden Teil der Klasse einfach testen können, ist es sehr wahrscheinlich, dass Ihre eine Klasse zu viel tut. Im Geiste des SRP-Prinzips können Sie dem folgen, was Michael Feathers "Sprout Class Pattern" nennt, und einen Teil Ihrer ursprünglichen Klasse in eine neue Klasse extrahieren.

In Ihrem Beispiel haben Sie eine Importerklasse, die auch für das Lesen einer Textdatei verantwortlich ist. Eine Ihrer Optionen besteht darin, einen Teil des Dateilesecodes in eine separate InputFileParser-Klasse zu extrahieren. Jetzt werden alle diese privaten Funktionen öffentlich und daher leicht testbar. Gleichzeitig ist die Parser-Klasse für externe Clients nicht sichtbar (markieren Sie sie als "intern", veröffentlichen Sie keine Header-Dateien oder veröffentlichen Sie sie einfach nicht als Teil Ihrer API), da diese weiterhin das Original verwenden Importeur, dessen Schnittstelle weiterhin kurz und bündig bleibt.

DXM
quelle
1

Als Alternative zum Einfügen vieler Methodenaufrufe in Ihren Klassenkonstruktor - was meiner Meinung nach eine Gewohnheit ist, die Sie generell vermeiden sollten - können Sie stattdessen eine Factory-Methode erstellen, um alle zusätzlichen Initialisierungsaufgaben zu verarbeiten, die den Client nicht stören sollen Seite mit. Dies würde bedeuten, dass Sie eine Methode verfügbar machen, um etwas wie Ihre constructGroupOfCustomers()Methode als öffentlich anzusehen , und sie entweder als statische Methode Ihrer Klasse verwenden:

GroupOfCustomersImporter importer = 
    new GroupOfCustomersImporter.CreateInstance();

oder als Methode einer separaten Fabrikklasse vielleicht:

ImporterFactory factory = new ImporterFactory();
GroupOfCustomersImporter importer = factory.CreateGroupOfCustomersImporter();

Dies sind nur die ersten Optionen, an die ich sofort gedacht habe.

Es lohnt sich auch zu bedenken, dass Ihr Instinkt versucht, Ihnen etwas zu sagen. Wenn Ihr Darm Ihnen sagt, dass der Code anfängt zu riechen, riecht er wahrscheinlich und es ist besser, etwas dagegen zu tun, bevor er stinkt! Sicherlich gibt es an sich nichts an Ihrem Code an sich, aber eine schnelle Überprüfung und ein Refactor helfen Ihnen dabei, Ihre Gedanken über das Problem zu klären, sodass Sie sicher zu anderen Dingen übergehen können, ohne sich Sorgen machen zu müssen, wenn Sie einen Code erstellen potenzielles Kartenhaus. In diesem speziellen Fall können Sie durch die Übertragung einiger Verantwortlichkeiten des Konstrukteurs an eine Fabrik oder auf deren Aufruf sicherstellen, dass Sie die Instanziierung Ihrer Klasse und ihrer potenziellen zukünftigen Nachkommen besser kontrollieren können.

S.Robins
quelle
1

Ich füge meine eigene Antwort hinzu, da sie für einen Kommentar zu lang war.

Sie haben absolut Recht, dass Kapselung und Testen ziemlich gegensätzlich sind - wenn Sie fast alles verstecken, ist es schwer zu testen.

Sie können das Beispiel jedoch testbarer machen, indem Sie einen Stream anstelle eines Dateinamens angeben. Auf diese Weise geben Sie einfach eine bekannte CSV aus dem Speicher oder eine Datei ein, wenn Sie möchten. Dies macht Ihre Klasse auch robuster, wenn Sie http-Unterstützung hinzufügen müssen;)

Schauen Sie sich auch die Verwendung von Lazy-Init an:

public class Csv
{
  private CustomerList list;

  public CustomerList get()
  {
    if(list == null)
        load();
     return list;
  }
}
Max
quelle
1

Der Konstruktor sollte nicht zu viel tun, außer einige Variablen oder den Status des Objekts zu initialisieren. Wenn Sie im Konstruktor zu viel tun, können Laufzeitausnahmen ausgelöst werden. Ich würde versuchen zu vermeiden, dass der Kunde so etwas tut:

MyClass a;
try {
   a = new MyClass();
} catch (MyException e) {
   //do something
}

Anstatt zu tun:

MyClass a = new MyClass(); // Or a factory method
try {
   a.doSomething();
} catch (MyException e) {
   //do something
}
Trotuar
quelle