Grundlagen zum Geruch von Ruby / Rails 03

Dieser Artikel ist für Anfänger gedacht und behandelt eine weitere Runde von Gerüchen und Refactorings, mit denen Sie sich früh in Ihrer Karriere vertraut machen sollten. Wir behandeln Case-Anweisungen, Polymorphismus, Nullobjekte und Datenklassen.

Themen

  • Case Statements
  • Polymorphismus
  • Nullobjekte
  • Datenklasse

Case Statements

Dies könnte auch als "Checklistengeruch" oder so bezeichnet werden. Case-Anweisungen sind ein Geruch, weil sie zu Duplikationen führen - oft sind sie auch nicht relevant. Sie können auch zu unnötig großen Klassen führen, da all diese Methoden, die auf die verschiedenen (und potenziell wachsenden) Fallszenarien reagieren, oft in derselben Klasse enden, die dann alle möglichen gemischten Zuständigkeiten hat. Es ist nicht selten der Fall, dass Sie über viele private Methoden verfügen, die in eigenen Klassen besser wären.

Ein großes Problem mit case-Anweisungen tritt auf, wenn Sie diese erweitern möchten. Dann müssen Sie diese bestimmte Methode ändern - möglicherweise immer wieder. Und das nicht nur dort, denn oft haben sie überall Zwillinge, die jetzt auch ein Update benötigen. Eine großartige Möglichkeit, um Fehler sicher zu züchten. Wie Sie sich vielleicht erinnern, möchten wir für die Erweiterung offen sein, aber für Änderungen geschlossen sein. Modifikationen sind hier unvermeidlich und nur eine Frage der Zeit.

Sie machen es schwieriger für Sie, Code zu extrahieren und wiederzuverwenden. Außerdem ist es eine tickende Clutter-Bombe. Häufig hängt der Ansichtscode von solchen Fallaussagen ab, die dann den Geruch duplizieren und das Tor für eine Runde einer Flintenoperation in der Zukunft weit öffnen. Autsch! Wenn Sie ein Objekt abfragen, bevor Sie die richtige Methode zum Ausführen finden, ist dies eine unangenehme Verletzung des "Tell-Don't-Ask" -Prinzips.

Polymorphismus

Es gibt eine gute Technik, um mit der Notwendigkeit von Case-Anweisungen umzugehen. Phantasie Wort eingehend! Polymorphismus. Auf diese Weise können Sie dieselbe Schnittstelle für verschiedene Objekte erstellen und das für verschiedene Szenarien erforderliche Objekt verwenden. Sie können nur das entsprechende Objekt austauschen, und es passt sich an Ihre Bedürfnisse an, da es die gleichen Methoden enthält. Ihr Verhalten unter diesen Methoden ist anders, aber solange die Objekte auf dieselbe Schnittstelle reagieren, kümmert sich Ruby nicht darum. Zum Beispiel, VegetarianDish.new.order und VeganDish.new.order verhalten sich anders, aber beide reagieren darauf #Auftrag in der gleichen Weise. Sie möchten einfach nur bestellen und beantworten nicht jede Menge Fragen, z. B. ob Sie Eier essen oder nicht.

Polymorphismus wird implementiert, indem eine Klasse für die Verzweigung der Fallanweisungen extrahiert und diese Logik in eine neue Methode für diese Klasse verschoben wird. Sie machen das weiterhin für jedes Bein im Bedingungsbaum und geben ihnen allen den gleichen Methodennamen. Auf diese Weise kapseln Sie dieses Verhalten in ein Objekt, das am besten geeignet ist, um diese Art von Entscheidung zu treffen, und hat keinen Grund, weitere Änderungen vorzunehmen. Auf diese Weise können Sie all diese nörgelnden Fragen zu einem Objekt vermeiden. Sie sagen ihm einfach, was er tun soll. Wenn Bedarf an mehr bedingten Fällen besteht, erstellen Sie einfach eine andere Klasse, die sich um diese einzige Verantwortung unter demselben Methodennamen kümmert.

Case Statement Logic

class Operation def price case @mission_tpe when: counter_intelligence @standard_fee + @counter_intelligence_fee when: terrorismus @standard_fee + @terrorism_fee when: rache @standard_fee + @revenge_fee when: erpressung @standard_fee + @extortion_fee end end counter_intel_op = operation.new (missionsauftrag): counter_intelligence) counter_intel_op.price terror_op = Operation.new (mission_type:: terrorismus) terror_op.price revenge_op = Operation.new (mission_type:: rache) revenge_op.price extortion_op = Operation.new (mission_type:: extortion) extortion_op.price 

In unserem Beispiel haben wir eine Operation Klasse, die nach ihrer fragen muss mission_type bevor es Ihnen seinen Preis sagen kann. Das ist leicht zu sehen Preis Die Methode wartet nur darauf, geändert zu werden, wenn eine neue Art von Vorgang hinzugefügt wird. Wenn Sie das auch in Ihrer Ansicht anzeigen möchten, müssen Sie diese Änderung auch dort anwenden. (Zu Ihrer Information, für Ansichten können Sie polymorphe Partials in Rails verwenden, um zu vermeiden, dass diese Case-Anweisungen in Ihrer gesamten Ansicht dargestellt werden.)

Polymorphe Klassen

class CounterIntelligenceOperation def price @standard_fee + @counter_intelligence_fee end end class TerrorismOperation def price @standard_fee + @terrorism_fee end end Klasse RevengeOperation def price @standard_fee + @revenge_fee end end Klasse ExtortionOperation def price @standard_fee + @extortion_fee Ende .price terror_op = CounterIntelligenceOperation.new terror_op.price revenge_op = CounterIntelligenceOperation.new revenge_op.price extortion_op = CounterIntelligenceOperation.new extortion_op.price 

Anstatt dieses Kaninchenloch zu erkunden, haben wir eine Reihe von Operationsklassen erstellt, die über ihr eigenes Wissen darüber verfügen, wie viel ihre Gebühren zum Endpreis addieren. Wir können ihnen einfach sagen, uns ihren Preis zu geben. Sie müssen nicht immer das Original loswerden (Operation) Nur Klasse, wenn Sie feststellen, dass Sie alles Wissen extrahiert haben.

Ich denke, die Logik hinter den Aussagen von Fällen ist unvermeidlich. Szenarien, in denen Sie eine Art Checkliste durchgehen müssen, bevor Sie das Objekt oder das Verhalten finden, das die Aufgabe erfüllt, sind einfach zu häufig. Die Frage ist einfach, wie sie am besten behandelt werden. Ich bin dafür, sie nicht zu wiederholen, und mit den Werkzeugen der objektorientierten Programmierung können diskrete Klassen entworfen werden, die einfach über ihre Benutzeroberfläche ausgetauscht werden können.

Nullobjekte

Die Suche nach Null überall ist ein besonderer Geruch. Ein Objekt nach nil zu fragen ist oft eine Art versteckter Fall. Ein bedingter Umgang mit Null kann die Form von haben object.nil?, object.present?, object.try, und dann eine Art Aktion in dem Fall Null erscheint auf deiner Party.

Eine weitere hinterlistige Frage ist, ein Objekt nach seiner Wahrhaftigkeit zu fragen - ergo ob es existiert oder ob es nichts ist - und dann etwas zu unternehmen. Sieht harmlos aus, ist aber nur eine Verkleidung. Lassen Sie sich nicht täuschen: ternäre Operatoren oder || Betreiber fallen natürlich auch in diese Kategorie. Anders ausgedrückt, Konditionen zeigen sich nicht nur eindeutig als es sei denn, ansonsten oder Fall Aussagen. Sie haben subtilere Möglichkeiten, Ihre Party zum Absturz zu bringen. Fragen Sie keine Objekte nach ihrer Nichtigkeit, sondern sagen Sie ihnen, ob das Objekt für Ihren glücklichen Pfad nicht vorhanden ist, dass jetzt ein Nullobjekt für die Beantwortung Ihrer Nachrichten zuständig ist.

Nullobjekte sind gewöhnliche Klassen. Es gibt nichts Besonderes an ihnen - nur einen "ausgefallenen Namen". Sie extrahieren eine bedingte Logik, die damit zusammenhängt Null und dann beschäftigen Sie sich damit polymorph. Sie enthalten dieses Verhalten, steuern den Fluss Ihrer App über diese Klassen und haben auch Objekte, die für andere Erweiterungen geöffnet sind, die zu ihnen passen. Denken Sie darüber nach, wie ein Versuch (NullSubscription) Die Klasse könnte im Laufe der Zeit wachsen. Es ist nicht nur DRY und Yadda-Yadda-Yadda, es ist auch viel beschreibender und stabiler.

Ein großer Vorteil der Verwendung von Nullobjekten ist, dass die Dinge nicht so leicht in die Luft gehen können. Diese Objekte reagieren auf die gleichen Nachrichten wie die emulierten Objekte. Sie müssen natürlich nicht immer die gesamte API auf null Objekte kopieren. Dies gibt Ihrer App wenig Anlass, verrückt zu werden. Beachten Sie jedoch, dass Null-Objekte bedingte Logik einkapseln, ohne sie vollständig zu entfernen. Sie finden einfach ein besseres Zuhause dafür.

Da es sehr ansteckend und ungesund ist, wenn Sie in Ihrer App viele Aktionen mit Nullen durchführen, stelle ich mir null Objekte gerne als „Nil Containment Pattern“ vor (bitte verklagen Sie mich nicht!). Warum ansteckend? Wenn du bestanden hast Null Um einen anderen Ort in Ihrer Hierarchie herum muss eine andere Methode früher oder später auch gefragt werden, ob Null ist in der Stadt - was dann zu einer weiteren Runde von Gegenmaßnahmen führt, um sich mit einem solchen Fall zu befassen.

Anders ausgedrückt: Null ist nicht cool, um damit herumzuhängen, weil die Nachfrage nach seiner Anwesenheit ansteckend wird. Objekte fragen nach Null ist höchstwahrscheinlich immer ein Symptom für schlechtes Design - keine Beleidigung und kein schlechtes Gewissen! - Wir waren alle dort. Ich möchte nicht "willkürlich" darüber gehen, wie unfreundlich Null sein kann, aber ein paar Dinge müssen erwähnt werden:

  • Nil ist ein Partykämpfer (Entschuldigung, ich musste sagen).
  • Nil ist nicht hilfreich, weil es an Bedeutung fehlt.
  • Nil reagiert auf nichts und verstößt gegen die Idee von "Duck Typing".
  • Nil-Fehlermeldungen sind oft ein Problem.
  • Nil wird dich früher oder später beißen.

Insgesamt tritt das Szenario, dass einem Objekt etwas fehlt, sehr häufig auf. Ein häufig zitiertes Beispiel ist eine App, die eine registrierte hat Nutzer und ein NilUser. Da jedoch Benutzer, die nicht existieren, ein dummes Konzept sind, wenn diese Person Ihre App eindeutig durchsucht, ist es wahrscheinlich kühler, eine App zu haben Gast das hat sich noch nicht angemeldet. Ein fehlendes Abonnement könnte ein sein Versuch, Eine Nullladung könnte eine sein Freebie, und so weiter.

Das Benennen Ihrer Nullobjekte ist manchmal offensichtlich und manchmal sehr schwer. Vermeiden Sie es jedoch, alle Nullobjekte mit einem führenden Null oder Nein zu benennen. Das kannst du besser! Ich denke, ein bisschen Kontext zu liefern ist ein langer Weg. Wählen Sie einen Namen, der spezifischer und aussagekräftiger ist, etwas, das den tatsächlichen Anwendungsfall widerspiegelt. Auf diese Weise kommunizieren Sie klarer mit anderen Teammitgliedern und natürlich mit Ihrem zukünftigen Selbst.

Es gibt mehrere Möglichkeiten, diese Technik zu implementieren. Ich zeige Ihnen eine, die ich für unkompliziert und Anfängerfreundlich halte. Ich hoffe, dass dies eine gute Basis für das Verständnis von mehr einbezogenen Ansätzen ist. Wenn Sie wiederholt auf eine Art von Bedingungen stoßen, die sich damit befassen Null, Sie wissen, es ist Zeit, einfach eine neue Klasse zu erstellen und dieses Verhalten zu verschieben. Danach lassen Sie die ursprüngliche Klasse wissen, dass dieser neue Spieler sich jetzt mit Nichts beschäftigt.

Im folgenden Beispiel können Sie sehen, dass die Gespenst Klasse fragt ein bisschen zu viel Null und verstopft den Code unnötig. Es will sicherstellen, dass wir eine haben böse_operation bevor es beschließt zu laden. Kannst du die Verletzung von "Tell-Don't-Ask" sehen??

Ein weiterer problematischer Aspekt ist, warum sich Specter um die Einführung eines Nullpreises kümmern muss. Das Versuchen Methode fragt auch hinterlistig, ob die böse_operation hat ein Preis mit dem Nichts über die oder (||) Aussage. böse_operation.gegenwart? macht den gleichen Fehler. Wir können das vereinfachen:

Die Klasse Spectre umfasst ActiveModel :: Model attr_accessor: credit_card,: evil_operation def charge, es sei denn, evil_operation.nil? evil_operation.charge (credit_card) end end def has_discount? böse_operation.gegenwart? && evil_operation.hat_discount? end def price evil_operation.try (: price) || 0 Ende Ende Klasse EvilOperation enthalten ActiveModel :: Model attr_accessor: Rabatt,: Preis def has_discount? Rabatt Ende def. (credit_card) credit_card.charge (price) end end 
Klasse NoOperation Def Charge (Kreditkarte) "Keine böse Operation registriert" Ende Def has_discount? false end def price 0 Ende end class Zu Specter gehören ActiveModel :: Model attr_accessor: credit_card,: evil_operation def charge evil_operation.charge (credit_card) end def has_discount? evil_operation.has_discount? end def price evil_operation.price ende private def evil_operation @evil_operation || NoOperation.new End-End-Klasse EvilOperation enthält ActiveModel :: Model attr_accessor: rabatt,: price def has_discount? Rabatt Ende def. (credit_card) credit_card.charge (price) end end 

Ich denke, dieses Beispiel ist einfach genug, um sofort zu erkennen, wie elegant Nullobjekte sein können. In unserem Fall haben wir eine Keine Operation Null-Klasse, die mit einer nicht vorhandenen bösen Operation umgehen kann:

  • Bearbeitungsgebühren von 0 Beträge.
  • Zu wissen, dass nicht vorhandene Vorgänge auch keinen Rabatt haben.
  • Rückgabe einer kleinen informativen Fehlerzeichenfolge, wenn die Karte aufgeladen ist.

Wir haben diese neue Klasse erstellt, die sich mit dem Nichts befasst, einem Objekt, das die Abwesenheit von Dingen behandelt, und es in die alte Klasse tauschen, wenn Null auftaucht. Die API ist hier der Schlüssel, denn wenn sie mit der ursprünglichen Klasse übereinstimmt, können Sie das Nullobjekt nahtlos mit den Rückgabewerten austauschen, die wir benötigen. Genau das haben wir in der privaten Methode getan Gespenst # Böse-Operation. Wenn wir die haben böse_operation Objekt, wir müssen das verwenden; wenn nicht, benutzen wir unsere Null Chamäleon, das mit diesen Botschaften umgehen kann, so böse_operation wird nie mehr nil zurückkehren. Ente tippt von seiner besten Seite.

Unsere problematische bedingte Logik ist jetzt an einer Stelle eingeschlossen, und unser Nullobjekt ist für das Verhalten verantwortlich Gespenst schaut nach. TROCKEN! Wir haben die gleiche Schnittstelle aus dem ursprünglichen Objekt neu aufgebaut, die von nil nicht verarbeitet werden konnte. Denken Sie daran, dass Nil keine schlechte Arbeit beim Empfang von Nachrichten hat - niemand zu Hause, immer! Von nun an sagt es Objekten nur, was sie tun sollen, ohne sie vorher um Erlaubnis zu bitten. Was auch cool ist, ist, dass es nicht nötig war, das zu berühren EvilOperation Klasse überhaupt.

Last but not least habe ich die Prüfung auf bösartige Operationen in Spectre # has_discount?. Sie müssen nicht sicherstellen, dass eine Operation vorhanden ist, um den Rabatt zu erhalten. Als Ergebnis des Nullobjekts wird der Gespenst Die Klasse ist viel schlanker und teilt die Verantwortlichkeiten anderer Klassen nicht so sehr.

Eine gute Richtlinie, die man davon wegnimmt, ist, Objekte nicht zu überprüfen, wenn sie dazu neigen, etwas zu tun. Befehlen Sie ihnen wie ein Feldwebel. Wie so oft ist es im wirklichen Leben wahrscheinlich nicht cool, aber es ist ein guter Rat für objektorientiertes Programmieren. Jetzt gib mir 20!

Im Allgemeinen gelten alle Vorteile der Verwendung von Polymorphismus anstelle von case-Anweisungen auch für Nullobjekte. Letztendlich. es ist nur ein spezieller Fall von Case-Aussagen. Das gleiche gilt für Nachteile:

  • Das Verständnis der Implementierung und des Verhaltens kann schwierig werden, da sich der Code verbreitet und Null-Objekte ihre Existenz nicht besonders explizit machen.
  • Das Hinzufügen eines neuen Verhaltens muss möglicherweise zwischen Nullobjekten und ihren Gegenstücken synchronisiert werden.

Datenklasse

Schließen wir diesen Artikel mit etwas Leichtem. Dies ist ein Geruch, da es sich um eine Klasse handelt, die kein anderes Verhalten hat als das Abrufen und Festlegen der Daten. Im Grunde ist es nichts weiter als ein Datencontainer ohne zusätzliche Methoden, die etwas mit diesen Daten tun. Das macht sie passiv, weil diese Daten dort für andere Klassen verwendet werden. Und wir wissen bereits, dass dies kein ideales Szenario ist, weil es schreit Feature Neid. Im Allgemeinen möchten wir Klassen haben, die über zustandsrelevante Daten verfügen, für die sie sorgen, sowie Verhaltensweisen, die sich auf Methoden beziehen, die auf diese Daten einwirken können, ohne dass sie sich stark in andere Klassen einmischen.

Sie können Klassen wie diese umgestalten, indem Sie möglicherweise das Verhalten von anderen Klassen extrahieren, die auf die Daten wirken, in Ihre Datenklasse. Auf diese Weise könnten Sie langsam nützliches Verhalten für diese Daten gewinnen und ihnen ein ordentliches Zuhause geben. Es ist völlig in Ordnung, wenn diese Klassen im Laufe der Zeit an Verhalten gewinnen. Manchmal können Sie eine ganze Methode leicht verschieben, und manchmal müssen Sie zuerst Teile einer größeren Methode extrahieren und dann in die Datenklasse extrahieren. Wenn Sie im Zweifelsfall den Zugriff anderer Klassen auf Ihre Datenklasse reduzieren können, sollten Sie dies unbedingt tun und dieses Verhalten ändern. Ihr Ziel sollte es sein, die Getter und Setter, die anderen Objekten Zugriff auf diese Daten gewährten, zu einem bestimmten Zeitpunkt einzustellen. Infolgedessen haben Sie eine geringere Kopplung zwischen den Klassen - was immer ein Gewinn ist!

Schlussgedanken

Sehen Sie, es war nicht so kompliziert! Es gibt eine Menge ausgefallener Lingues und kompliziert klingender Techniken rund um Code-Gerüche, aber hoffentlich haben Sie erkannt, dass Gerüche und deren Refactorings auch einige Eigenschaften haben, die in ihrer Anzahl begrenzt sind. Code-Gerüche werden niemals verschwinden oder irrelevant werden - zumindest nicht, bevor unser Körper von den AIs verbessert wird, die uns die Art von Qualitätscode schreiben lassen, von der wir im Moment Lichtjahre entfernt sind.

Im Verlauf der letzten drei Artikel habe ich Ihnen einen guten Ausschnitt der wichtigsten und am häufigsten vorkommenden Codegeruchszenarien vorgestellt, auf die Sie während Ihrer objektorientierten Programmierkarriere stoßen werden. Ich denke, dies war eine gute Einführung für Neulinge in dieses Thema, und ich hoffe, dass die Codebeispiele ausführlich genug waren, um dem Thread leicht folgen zu können.

Sobald Sie diese Prinzipien für die Gestaltung von Qualitätscodes herausgefunden haben, werden Sie in kürzester Zeit neue Gerüche und deren Refactorings entdecken. Wenn Sie es bisher geschafft haben und das Gefühl haben, dass Sie das große Bild nicht verloren haben, sind Sie bereit, sich dem OOP-Status des Boss-Levels zu nähern.