FESTE vs. statische Methoden

11

Hier ist ein Problem, auf das ich häufig stoße: Es soll ein Webshop-Projekt mit einer Produktklasse geben. Ich möchte eine Funktion hinzufügen, mit der Benutzer Bewertungen zu einem Produkt veröffentlichen können. Ich habe also eine Review-Klasse, die auf ein Produkt verweist. Jetzt brauche ich eine Methode, die alle Bewertungen zu einem Produkt auflistet. Es gibt zwei Möglichkeiten:

(EIN)

public class Product {
  ...
  public Collection<Review> getReviews() {...}
}

(B)

public class Review {
  ...
  static public Collection<Review> forProduct( Product product ) {...}
}

Wenn ich mir den Code anschaue, würde ich (A) wählen: Er ist nicht statisch und benötigt keinen Parameter. Ich spüre jedoch, dass (A) das Prinzip der Einzelverantwortung (Single Responsibility Principle, SRP) und das Open-Closed-Prinzip (OCP) verletzt, während (B) nicht:

  • (SRP) Wenn ich die Art und Weise ändern möchte, wie Bewertungen für ein Produkt gesammelt werden, muss ich die Produktklasse ändern. Es sollte jedoch nur einen Grund geben, die Produktklasse zu ändern. Und das sind sicherlich nicht die Bewertungen. Wenn ich alle Funktionen packe, die etwas mit Produkten in Product zu tun haben, wird es bald klappern.

  • (OCP) Ich muss die Produktklasse ändern, um sie mit dieser Funktion zu erweitern. Ich denke, dies verstößt gegen den Teil des Prinzips, der für Veränderungen geschlossen ist. Bevor ich die Anfrage des Kunden zur Implementierung der Überprüfungen erhielt, betrachtete ich das Produkt als fertig und "schloss" es.

Was ist wichtiger: Befolgen Sie die SOLID-Prinzipien oder haben Sie eine einfachere Oberfläche?

Oder mache ich hier überhaupt etwas falsch?

Ergebnis

Wow, danke für all deine tollen Antworten! Es ist schwer, eine als offizielle Antwort auszuwählen.

Lassen Sie mich die Hauptargumente aus den Antworten zusammenfassen:

  • Pro (A): OCP ist kein Gesetz und die Lesbarkeit des Codes ist ebenfalls wichtig.
  • pro (A): Die Entitätsbeziehung sollte navigierbar sein. Beide Klassen kennen möglicherweise diese Beziehung.
  • pro (A) + (B): Beides tun und in (A) an (B) delegieren, damit das Produkt weniger wahrscheinlich erneut geändert wird.
  • pro (C): Finder-Methoden in die dritte Klasse (Service) einordnen, wo sie nicht statisch sind.
  • contra (B): Verhindert das Verspotten in Tests.

Ein paar zusätzliche Dinge, die meine Hochschulen bei der Arbeit beigetragen haben:

  • pro (B): Unser ORM-Framework kann automatisch den Code für (B) generieren.
  • pro (A): Aus technischen Gründen unseres ORM-Frameworks muss in einigen Fällen die "geschlossene" Einheit geändert werden, unabhängig davon, wohin der Finder geht. Ich werde mich also sowieso nicht immer an SOLID halten können.
  • contra (C): zu viel Aufhebens ;-)

Fazit

Ich verwende beide (A) + (B) mit Delegation für mein aktuelles Projekt. In einer serviceorientierten Umgebung werde ich jedoch mit (C) gehen.

Wolfgang
quelle
2
Solange es keine statische Variable ist, ist alles cool. Statische Methoden sind einfach zu testen und einfach zu verfolgen.
Coder
3
Warum nicht einfach eine ProductsReviews-Klasse? Dann bleiben Produkt und Bewertung gleich. Oder vielleicht verstehe ich falsch.
ElGringoGrande
2
@Coder "Statische Methoden sind einfach zu testen", wirklich? Sie können nicht verspottet werden, siehe: googletesting.blogspot.com/2008/12/… für weitere Details.
StuperUser
1
@StuperUser: Es gibt nichts zu verspotten. Assert(5 = Math.Abs(-5));
Coder
2
Testen Abs()ist nicht das Problem, etwas zu testen, das davon abhängt. Sie haben keine Naht zum Isolieren des abhängigen Code-Under-Test (CUT), um ein Modell zu verwenden. Dies bedeutet, dass Sie es nicht als atomare Einheit testen können und alle Ihre Tests zu Integrationstests werden, die die Logik der Testeinheit testen. Ein Fehler in einem Test kann in CUT oder in Abs()(oder seinem abhängigen Code) liegen und die Diagnosevorteile von Komponententests aufheben.
StuperUser

Antworten:

7

Was ist wichtiger: Befolgen Sie die SOLID-Prinzipien oder haben Sie eine einfachere Oberfläche?

Schnittstelle gegenüber SOLID

Diese schließen sich nicht gegenseitig aus. Die Benutzeroberfläche sollte die Art Ihres Geschäftsmodells idealerweise in Bezug auf das Geschäftsmodell ausdrücken. Das SOLID-Prinzip ist ein Koan zur Maximierung der Wartbarkeit von objektorientiertem Code (und ich meine "Wartbarkeit" im weitesten Sinne). Ersteres unterstützt die Verwendung und Manipulation Ihres Geschäftsmodells und letzteres optimiert die Codepflege.

Offenes / geschlossenes Prinzip

"Fass das nicht an!" ist eine zu vereinfachende Interpretation. Und wenn wir "die Klasse" meinen, ist das willkürlich und nicht unbedingt richtig. OCP bedeutet vielmehr, dass Sie Ihren Code so gestaltet haben, dass Sie zum Ändern des Verhaltens nicht direkt den vorhandenen Arbeitscode ändern müssen (sollten). Darüber hinaus ist es ideal, den Code nicht zu berühren, um die Integrität vorhandener Schnittstellen zu erhalten. Dies ist meiner Ansicht nach eine bedeutende Folge von OCP.

Schließlich sehe ich OCP als Indikator für die vorhandene Designqualität. Wenn ich zu oft offene Klassen (oder Methoden) knacke und / oder ohne wirklich solide (ha, ha) Gründe dafür, kann dies bedeuten, dass ich ein schlechtes Design habe (und / oder nicht) weiß, wie man OO codiert).

Geben Sie Ihr Bestes, wir haben ein Team von Ärzten bereit

Wenn Ihre Anforderungsanalyse besagt, dass Sie die Produkt-Überprüfungs-Beziehung aus beiden Perspektiven ausdrücken müssen, tun Sie dies.

Deshalb, Wolfgang, haben Sie vielleicht einen guten Grund, diese vorhandenen Klassen zu ändern. Wenn angesichts der neuen Anforderungen eine Überprüfung nun ein grundlegender Bestandteil eines Produkts ist und jede Erweiterung des Produkts eine Überprüfung erfordert, wenn dies den Client-Code angemessen aussagekräftig macht, integrieren Sie ihn in das Produkt.

Radarbob
quelle
1
+1 für die Feststellung, dass OCP eher ein Indikator für gute Qualität ist, keine schnelle Regel für Code. Wenn Sie feststellen, dass es schwierig oder unmöglich ist, die OCP ordnungsgemäß zu befolgen, ist dies ein Zeichen dafür, dass Ihr Modell überarbeitet werden muss, um eine flexiblere Wiederverwendung zu ermöglichen.
CodexArcanum
Ich habe das Beispiel Produkt und Überprüfung gewählt, um darauf hinzuweisen, dass Produkt eine Kerneinheit des Projekts ist, während Überprüfung nur ein Add-On ist. Das Produkt ist also bereits vorhanden, fertig und die Überprüfung erfolgt später und sollte eingeführt werden, ohne dass vorhandener Code (einschließlich Produkt) geöffnet wird.
Wolfgang
10

SOLID sind Richtlinien, also beeinflussen Sie die Entscheidungsfindung, anstatt sie zu diktieren.

Bei der Verwendung statischer Methoden ist zu beachten, dass sie sich auf die Testbarkeit auswirken .


Testen forProduct(Product product)ist kein Problem.

Etwas zu testen, das davon abhängt, wird sein.

Sie haben keine Naht zum Isolieren des abhängigen Code-Under-Test (CUT), um ein Modell zu verwenden, da bei Ausführung der Anwendung die statischen Methoden unbedingt vorhanden sein müssen.

Lassen Sie uns eine Methode namens CUT()aufrufenforProduct()

Wenn dies der Fall forProduct()ist static, können Sie nicht CUT()als atomare Einheit testen , und alle Ihre Tests werden zu Integrationstests, die die Logik der Testeinheit bestimmen.

Ein Fehler in einem Test für CUT kann durch ein Problem in CUT()oder in forProduct()(oder einem seiner abhängigen Codes) verursacht werden, das die Diagnosevorteile von Komponententests aufhebt.

Weitere Informationen finden Sie in diesem ausgezeichneten Blog-Beitrag: http://googletesting.blogspot.com/2008/12/static-methods-are-death-to-testability.html


Dies kann zu Frustrationen führen, wenn Tests nicht bestanden werden und bewährte Verfahren und Vorteile, die sie umgeben, aufgegeben werden.

StuperUser
quelle
1
Das ist ein sehr guter Punkt. Im Allgemeinen aber nicht für mich. ;-) Ich bin nicht so streng beim Schreiben von Unit-Tests, die mehr als eine Klasse abdecken. Sie gehen alle in die Datenbank, das ist okay für mich. Ich werde mich also nicht über das Geschäftsobjekt oder dessen Finder lustig machen.
Wolfgang
+1, danke, dass du die rote Fahne für Tests gesetzt hast! Ich stimme @Wolfgang zu, normalerweise bin ich auch nicht so streng, aber wenn ich solche Tests brauche, hasse ich statische Methoden wirklich. Wenn eine statische Methode zu stark mit ihren Parametern interagiert oder wenn sie mit einer anderen statischen Methode interagiert, ziehe ich es normalerweise vor, sie zu einer Instanzmethode zu machen.
Adriano Repetti
Kommt es nicht ganz auf die verwendete Sprache an? Das OP verwendete ein Java-Beispiel, erwähnte jedoch nie eine Sprache in der Frage, noch ist eine in den Tags angegeben.
Izkata
1
@StuperUser If forProduct() is static you can't test CUT() as an atomic unit and all of your tests become integration tests that test unit logic.- Ich glaube, Javascript und Python ermöglichen es, statische Methoden zu überschreiben / zu verspotten. Ich bin mir jedoch nicht 100% sicher.
Izkata
1
@Izkata JS ist dynamisch typisiert, hat also keine static, Sie simulieren es mit einem Abschluss und dem Singleton-Muster. Wenn Sie Python lesen (insbesondere stackoverflow.com/questions/893015/… ), müssen Sie es erben und erweitern. Überschreiben ist kein Spott; Sieht so aus, als hätten Sie immer noch keine Naht zum Testen des Codes als atomare Einheit.
StuperUser
4

Wenn Sie der Meinung sind, dass das Produkt der richtige Ort ist, um die Bewertungen des Produkts zu finden, können Sie dem Produkt jederzeit eine helfende Klasse geben, um die Arbeit für das Produkt zu erledigen. (Sie können feststellen, dass Ihr Unternehmen nur in Bezug auf ein Produkt über eine Bewertung spricht.)

Zum Beispiel wäre ich versucht, etwas zu injizieren, das die Rolle eines Review Retrievers spielt. Ich würde es wahrscheinlich die Schnittstelle geben IRetrieveReviews. Sie können dies in den Konstruktor des Produkts einfügen (Dependency Injection). Wenn Sie ändern möchten, wie die Bewertungen abgerufen werden, können Sie dies einfach tun, indem Sie einen anderen Mitarbeiter einsetzen - einen TwitterReviewRetrieveroder einen AmazonReviewRetrieveroder MultipleSourceReviewRetrieveroder was auch immer Sie sonst noch benötigen.

Beide haben jetzt eine einzige Verantwortung (sie sind die Anlaufstelle für alle produktbezogenen Dinge bzw. das Abrufen von Bewertungen), und in Zukunft kann das Verhalten des Produkts in Bezug auf Bewertungen geändert werden, ohne das Produkt tatsächlich zu ändern (Sie können es erweitern als ProductWithReviewsob Sie wirklich pedantisch über Ihre SOLID-Prinzipien sein wollten, aber das wäre gut genug für mich).

Lunivore
quelle
Klingt nach dem DAO-Muster, das in service- / komponentenorientierter Software sehr verbreitet ist. Ich mag diese Idee, weil sie zum Ausdruck bringt, dass das Abrufen von Objekten nicht in der Verantwortung dieser Objekte liegt. Da ich jedoch einen objektorientierten Weg dem serviceorientierten vorziehe.
Wolfgang
1
Die Verwendung einer solchen Schnittstelle verhindert, dass IRetrieveReviewssie serviceorientiert ist - sie bestimmt nicht, was die Bewertungen erhält, wie oder wann. Vielleicht ist es ein Service mit vielen Methoden für solche Dinge. Vielleicht ist es eine Klasse, die das eine macht. Möglicherweise handelt es sich um ein Repository oder eine HTTP-Anforderung an einen Server. Du weißt es nicht. Du solltest es nicht wissen. Das ist der Punkt.
Lunivore
Ja, es wäre also eine Implementierung des Strategiemusters. Welches wäre ein Argument für eine dritte Klasse. (A) und (B) würden dies nicht unterstützen. Der Finder wird definitiv ein ORM verwenden, daher gibt es keinen Grund, den Algorithmus zu ersetzen. Entschuldigung, wenn mir dies in meiner Frage nicht klar war.
Wolfgang
3

Ich hätte eine ProductsReview-Klasse. Sie sagen, Review ist sowieso neu. Das heißt nicht, dass es einfach alles sein kann. Es muss immer noch nur einen Grund geben, sich zu ändern. Wenn Sie ändern, wie Sie die Bewertungen aus irgendeinem Grund erhalten, müssen Sie die Überprüfungsklasse ändern.

Das ist nicht richtig

Sie fügen die statische Methode in die Review-Klasse ein, weil ... warum? Haben Sie nicht damit zu kämpfen? Ist das nicht das ganze Problem?

Dann nicht. Machen Sie eine Klasse, deren alleinige Verantwortung darin besteht, die Produktbewertungen zu erhalten. Sie können es dann in ProductReviewsByStartRating unterordnen. Oder unterklassifizieren Sie es, um Bewertungen für eine Produktklasse zu erhalten.

ElGringoGrande
quelle
Ich bin nicht damit einverstanden, dass die Methode zur Überprüfung gegen SRP verstoßen würde. Auf Produkt würde es aber nicht auf Review. Mein Problem dort war, dass es statisch ist. Wenn ich die Methode in eine dritte Klasse verschieben würde, wäre sie immer noch statisch und hätte den Produktparameter.
Wolfgang
Sie sagen also, dass die einzige Verantwortung für die Review-Klasse, der einzige Grund für ihre Änderung, darin besteht, dass die statische Methode forProduct geändert werden muss? Es gibt keine andere Funktionalität in der Review-Klasse?
ElGringoGrande
Es gibt eine Menge Dinge auf Review. Aber ich denke, dass ein forProduct (Produkt) gut dazu passen würde. Das Finden von Bewertungen hängt stark von den Attributen und der Struktur der Überprüfung ab (welche eindeutigen Bezeichner, welche Bereiche ändern die Attribute?). Das Produkt kennt und sollte jedoch nichts über Bewertungen wissen.
Wolfgang
3

Ich würde die Funktion "Bewertungen für Produkte abrufen" weder in die ProductKlasse noch in die ReviewKlasse aufnehmen ...

Sie haben einen Ort, an dem Sie Ihre Produkte abrufen können, oder? Etwas mit GetProductById(int productId)und vielleicht GetProductsByCategory(int categoryId)und so weiter.

Ebenso sollten Sie einen Ort haben, an dem Sie Ihre Bewertungen abrufen können, mit einem GetReviewbyId(int reviewId)und vielleicht einem GetReviewsForProduct(int productId).

Wenn ich die Art und Weise ändern möchte, wie Bewertungen für ein Produkt gesammelt werden, muss ich die Produktklasse ändern.

Wenn Sie Ihre Daten Zugriff von Ihren Domain - Klassen trennen, werden Sie nicht ändern müssen entweder Domain - Klasse , wenn Sie die Art , wie die Bewertungen werden gesammelt ändern.

Eric King
quelle
So würde ich damit umgehen, und ich bin verwirrt (und besorgt darüber, ob meine eigenen Ideen richtig sind), weil diese Antwort bis zu Ihrem Beitrag fehlt. Es ist klar, dass weder die Klasse, die einen Bericht darstellt, noch die Darstellung eines Produkts dafür verantwortlich sein sollten, das andere tatsächlich abzurufen. Eine Art von Daten, die Dienste bereitstellen, sollte dies handhaben.
CodexArcanum
@ CodeArcanum Nun, du bist nicht allein. :-)
Eric King
2

Muster und Prinzipien sind Richtlinien, keine in den Stein geschriebenen Regeln. Meiner Meinung nach ist die Frage nicht, ob es besser ist, den SOLID-Prinzipien zu folgen oder eine einfachere Oberfläche beizubehalten. Was Sie sich fragen sollten, ist, was für die meisten Menschen lesbarer und verständlicher ist. Oft bedeutet dies, dass es so nah wie möglich an der Domain sein muss.

In diesem Fall würde ich die Lösung (B) bevorzugen, da für mich der Ausgangspunkt das Produkt ist, nicht die Überprüfung, aber stellen Sie sich vor, Sie schreiben eine Software zum Verwalten von Bewertungen. In diesem Fall ist das Zentrum die Überprüfung, daher kann Lösung (A) vorzuziehen sein.

Wenn ich viele Methoden wie diese habe ("Verbindungen" zwischen Klassen), entferne ich sie alle außerhalb und erstelle eine (oder mehrere) neue statische Klassen, um sie zu organisieren. Normalerweise können Sie sie als Abfragen oder als eine Art Repository sehen.

Adriano Repetti
quelle
Ich habe auch über diese Idee der Semantik beider Enden nachgedacht und welche "stärker" ist, damit sie den Finder beansprucht. Deshalb habe ich mir (B) ausgedacht. Es würde auch helfen, eine Hierarchie der "Abstraktion" der Geschäftsklassen zu erstellen. Das Produkt war nur ein grundlegendes Objekt, bei dem die Bewertung höher ist. Daher kann Review auf das Produkt verweisen, aber nicht umgekehrt. Auf diese Weise können Referenzzyklen zwischen den Geschäftsklassen vermieden werden, was ein weiteres Problem auf meiner Liste wäre, das gelöst wurde.
Wolfgang
Ich denke, für eine kleine Gruppe von Klassen könnte es sogar schön sein, BEIDE Methoden (A) und (B) bereitzustellen. Zu oft ist die Benutzeroberfläche zum Zeitpunkt des Schreibens dieser Logik nicht bekannt.
Adriano Repetti
1

Ihr Produkt kann nur an Ihre statische Überprüfungsmethode delegieren. In diesem Fall stellen Sie eine praktische Schnittstelle an einem natürlichen Ort bereit (Product.getReviews), Ihre Implementierungsdetails befinden sich jedoch in Review.getForProduct.

SOLID sind Richtlinien und sollten zu einer einfachen, sinnvollen Schnittstelle führen. Alternativ können Sie SOLID von einfachen, sinnvollen Schnittstellen ableiten. Es geht um das Abhängigkeitsmanagement innerhalb von Code. Ziel ist es, die Abhängigkeiten zu minimieren, die Reibung verursachen und Hindernisse für unvermeidliche Veränderungen schaffen.

pfries
quelle
Ich bin mir nicht sicher, ob ich das wollen würde. Auf diese Weise habe ich die Methode an beiden Stellen, was gut ist, da andere Entwickler nicht in beiden Klassen nachsehen müssen, um zu sehen, wo ich sie abgelegt habe. Andererseits hätte ich sowohl Nachteile der statischen Methode als auch die Änderung der "geschlossenen" Produktklasse. Ich bin nicht sicher, ob die Delegation aus dem Reibungsproblem heraus hilft. Wenn es jemals einen Grund gab, den Finder zu ändern, wird dies mit Sicherheit zu einer Änderung der Signatur und damit zu einer erneuten Änderung des Produkts führen.
Wolfgang
Der Schlüssel von OCP ist, dass Sie Implementierungen ersetzen können, ohne Ihren Code zu ändern. In der Praxis ist dies eng mit der Liskov-Substitution verbunden. Eine Implementierung muss durch eine andere ersetzt werden können. Möglicherweise haben Sie DatabaseReviews und SoapReviews als unterschiedliche Klassen, die beide IGetReviews.getReviews und getReviewsForProducts implementieren. Dann ist Ihr System für Erweiterungen geöffnet (Ändern, wie Überprüfungen abgerufen werden) und für Änderungen geschlossen (Abhängigkeiten von IGetReviews brechen nicht). Das meine ich mit Abhängigkeitsmanagement. In Ihrem Fall müssen Sie Ihren Code ändern, um sein Verhalten zu ändern.
pfries
Ist das nicht das Abhängigkeitsinversionsprinzip (DIP), das Sie beschreiben?
Wolfgang
Das sind verwandte Prinzipien. Ihr Substitutionspunkt wird durch DIP erreicht.
pfries
1

Ich sehe das etwas anders als die meisten anderen Antworten. Ich denke das Productund Reviewsind im Grunde Datenübertragungsobjekte (DTOs). In meinem Code versuche ich, meine DTOs / Entitäten dazu zu bringen, Verhaltensweisen zu vermeiden. Sie sind nur eine nette API, um den aktuellen Status meines Modells zu speichern.

Wenn Sie über OO und SOLID sprechen, sprechen Sie im Allgemeinen von einem "Objekt", das nicht (notwendigerweise) den Status darstellt, sondern eine Art Dienst darstellt, der Fragen für Sie beantwortet oder an den Sie einen Teil Ihrer Arbeit delegieren können . Zum Beispiel:

interface IProductRepository
{
    void SaveNewProduct(IProduct product);
    IProduct GetProductById(ProductId productId);
    bool TryGetProductByName(string name, out IProduct product);
}

interface IProduct
{
    ProductId Id { get; }
    string Name { get; }
}

class ExistingProduct : IProduct
{
    public ProductId Id { get; private set; }
    public string Name { get; private set; }
}

Dann würde Ihr tatsächlicher ProductRepositoryein ExistingProductfür die GetProductByProductIdMethode usw. zurückgeben.

Jetzt folgen Sie dem Prinzip der Einzelverantwortung (was auch immer von erbt, IProducthält nur am Status fest, und was auch immer von erbt, IProductRepositoryist dafür verantwortlich, zu wissen, wie Sie Ihr Datenmodell beibehalten und rehydrieren können).

Wenn Sie Ihr Datenbankschema ändern, können Sie Ihre Repository-Implementierung ändern, ohne Ihre DTOs usw. zu ändern.

Kurz gesagt, ich denke, ich würde keine Ihrer Optionen wählen. :) :)

Scott Whitlock
quelle
0

Statische Methoden sind möglicherweise schwieriger zu testen, aber das bedeutet nicht, dass Sie sie nicht verwenden können. Sie müssen sie nur so einrichten, dass Sie sie nicht testen müssen.

Machen Sie beide product.GetReviews und Review.ForProduct einzeilige Methoden, die so etwas wie sind

new ReviewService().GetReviews(productID);

ReviewService enthält den komplexeren Code und verfügt über eine Schnittstelle, die auf Testbarkeit ausgelegt ist, jedoch nicht direkt dem Benutzer zugänglich ist.

Wenn Sie eine 100% ige Abdeckung benötigen, lassen Sie Ihre Integrationstests die Methoden der Produkt- / Überprüfungsklasse aufrufen.

Es kann hilfreich sein, wenn Sie eher an das öffentliche API-Design als an das Klassendesign denken. In diesem Zusammenhang ist nur eine einfache Schnittstelle mit logischer Gruppierung von Bedeutung. Die tatsächliche Codestruktur ist nur für Entwickler von Bedeutung.

Tom Clarkson
quelle