Ruby / Rails Code Smell-Grundlagen 01

Themen

  • Kopf hoch
  • Widerstand
  • Große Klasse / Gott-Klasse
  • Klasse extrahieren
  • Lange Methode
  • Lange Parameterliste

Kopf hoch

Die folgende kurze Artikelserie ist für erfahrene Ruby-Entwickler und -Starter gedacht. Ich hatte den Eindruck, dass Code-Gerüche und deren Refactorings Neulinge sehr entmutigend und einschüchternd wirken können - vor allem, wenn sie nicht in der glücklichen Position sind, Mentoren zu haben, die mystische Programmierkonzepte in leuchtende Glühlampen verwandeln können.

Nachdem ich diese Schuhe offensichtlich selbst betreten hatte, fiel mir ein, dass es unnötig neblig war, in Codegerüche und Refactorings zu geraten.

Einerseits erwarten Autoren ein gewisses Maß an Können und fühlen sich daher nicht unbedingt dazu gezwungen, den Leser mit dem gleichen Kontext auszustatten, den ein Neuling möglicherweise früher in diese Welt eintauchen könnte.

Infolgedessen erwecken Neulinge vielleicht den Eindruck, dass sie etwas länger warten sollten, bis sie fortgeschrittener sind, um etwas über Gerüche und Refactorings zu erfahren. Ich stimme diesem Ansatz nicht zu und denke, dass die Annäherung an dieses Thema ihnen helfen wird, bessere Software früher in ihrer Karriere zu entwickeln. Zumindest hoffe ich, dass es hilfreich ist, jungen Leuten einen soliden Vorsprung zu bieten.

Worüber sprechen wir genau dann, wenn Leute Codegerüche erwähnen? Ist es immer ein Problem in Ihrem Code? Nicht unbedingt! Kannst du sie vollständig vermeiden? Ich glaube nicht Meinen Sie damit, dass Codegerüche zu gebrochenem Code führen? Nun, manchmal und manchmal nicht. Sollte es meine Priorität sein, sie sofort zu reparieren? Gleiche Antwort, fürchte ich: Manchmal ja und manchmal sollten Sie zuerst größere Fische braten. Bist du verrückt? Faire Frage an dieser Stelle!

Bevor Sie weiter in dieses ganze stinkende Geschäft eintauchen, denken Sie daran, eine Sache von all dem wegzunehmen: Versuchen Sie nicht, jeden Geruch zu reparieren, auf den Sie stoßen - dies ist mit Sicherheit eine Zeitverschwendung!

Es scheint mir, dass Code-Gerüche in einem schön beschrifteten Karton etwas schwer einzuwickeln sind. Es gibt alle Arten von Gerüchen mit verschiedenen Optionen, um sie anzusprechen. Unterschiedliche Programmiersprachen und Frameworks neigen auch zu unterschiedlichen Gerüchen - aber es gibt definitiv viele übliche „genetische“ Stämme. Mein Versuch, Codegerüche zu beschreiben, besteht darin, sie mit medizinischen Symptomen zu vergleichen, die Sie auf ein Problem hinweisen. Sie können auf alle möglichen latenten Probleme hinweisen und haben bei Diagnose eine Vielzahl von Lösungen.

Zum Glück sind sie insgesamt nicht so kompliziert wie der Umgang mit dem menschlichen Körper und natürlich mit der Psyche. Es ist jedoch ein fairer Vergleich, denn einige dieser Symptome müssen sofort behandelt werden, und andere geben Ihnen ausreichend Zeit, um eine Lösung zu finden, die für das allgemeine Wohlbefinden des Patienten am besten ist. Wenn Sie Arbeitscode haben und auf etwas stinkendes stoßen, müssen Sie die schwierige Entscheidung treffen, wenn es sich lohnt, einen Fix zu finden, und wenn das Refactoring die Stabilität Ihrer App verbessert.

Wenn Sie jedoch auf Code stoßen, den Sie sofort verbessern können, ist es ratsam, den Code ein wenig besser als zuvor zu belassen - sogar ein kleines bisschen besser summiert sich im Laufe der Zeit erheblich.

Widerstand

Die Qualität Ihres Codes wird fragwürdig, wenn die Einbeziehung von neuem Code schwieriger wird, wenn Sie beispielsweise entscheiden, wo neuer Code eingefügt werden soll, oder wenn Sie in der gesamten Codebase viele Welleneffekte haben. Dies wird Widerstand genannt.

Als Richtlinie für die Codequalität können Sie immer daran messen, wie einfach Änderungen vorgenommen werden können. Wenn das immer schwieriger wird, ist es definitiv an der Zeit, etwas zu ändern und den letzten Teil davon zu übernehmen rot-grün-REFACTOR ernster in der Zukunft.

Große Klasse / Gott-Klasse

Beginnen wir mit etwas ausgefallenen Klängen - „God-Klassen“ - weil ich denke, dass sie für Anfänger besonders leicht zu verstehen sind. Gottesklassen sind ein Spezialfall eines Codegeruchs Große Klasse. In diesem Abschnitt werde ich beide ansprechen. Wenn Sie ein wenig Zeit in Rails Land verbracht haben, haben Sie sie wahrscheinlich so oft gesehen, dass sie für Sie normal aussehen.

Sie erinnern sich sicherlich an das Mantra „Fat Models, Skinny Controller“? Tatsächlich ist Skinny gut für alle diese Klassen, aber als Richtlinie ist es ein guter Rat für Neulinge, denke ich.

Gottesklassen sind Objekte, die wie schwarzes Loch alle Arten von Wissen und Verhalten anziehen. Zu den üblichen Verdächtigen zählen meistens das Benutzermodell und das Problem (hoffentlich!), Das Ihre App zu lösen versucht. Eine ToDo-App kann sich auf der Todos Modell, eine Einkaufs-App auf Produkte, eine Foto-App auf Fotos-Sie bekommen den Drift.

Die Leute nennen sie Gottheiten, weil sie zu viel wissen. Sie haben zu viele Verbindungen zu anderen Klassen - meistens, weil sie von ihnen faul modelliert wurden. Es ist jedoch harte Arbeit, den Gottesunterricht in Schach zu halten. Sie machen es wirklich einfach, mehr Verantwortlichkeiten auf sie zu werfen, und wie viele griechische Helden bestätigen würden, bedarf es einiger Geschicklichkeit, um „Götter“ zu teilen und zu erobern..

Das Problem bei ihnen ist, dass sie immer schwieriger zu verstehen sind, insbesondere für neue Teammitglieder, die schwieriger zu ändern sind und die Wiederverwendung von ihnen immer weniger eine Option ist, je mehr Schwerkraft sie anhäufen. Oh ja, du hast recht, deine Tests sind auch unnötig schwieriger zu schreiben. Kurz gesagt, es gibt nicht wirklich einen großen Vorteil, wenn man große Klassen und insbesondere Gottesstunden hat.

Es gibt einige häufige Symptome / Anzeichen, dass Ihre Klasse Heroismus / Operation benötigt:

  • Du musst scrollen!
  • Tonnenweise private Methoden?
  • Verfügt Ihre Klasse über sieben oder mehr Methoden??
  • Schwer zu sagen, was Ihre Klasse wirklich macht - prägnant!
  • Hat Ihre Klasse viele Gründe, sich zu ändern, wenn sich Ihr Code weiterentwickelt??

Auch, wenn Sie auf Ihre Klasse blinzeln und denken: „Eh? Ew! ”Vielleicht bist du auch bei etwas dran. Wenn Ihnen das alles bekannt vorkommt, stehen die Chancen gut, dass Sie ein feines Exemplar gefunden haben.

"Ruby-Klasse CastingInviter EMAIL_REGEX = /\A([^@\s(++@@((??:[-a-z0-9(++). )+[a-z(2 ,)\z/

attr_reader: Nachricht,: Eingeladene,: Casting

def initialize (Attribute = ) @message = Attribute [: Nachricht] || "@invitees = Attribute [: Eingeladene] ||" @sender = Attribute [: Sender] @casting = Attribute [: Casting] Ende

def gültig gültige Nachricht? && valid_invitees? Ende def liefern, falls gültig? Eingeladene Liste. E-Mail | invitation = create_invitation (email) Mailer.invitation_notification (invitation, @message) end else failure_message = "Ihre # @casting -Nachricht konnte nicht gesendet werden. E-Mails oder Nachrichten der eingeladenen Personen sind ungültig." invitation = create_invitation (@sender) Mailer.invitation_notification (invitation, failure_message) end end private def invalid_invitees @invalid_invitees || = invitee_list.map do | item | es sei denn item.match (EMAIL_REGEX) Element end end.compact end def einladungsliste @invitee_list || = @ invitees.gsub (/ \ s + /, "). split (/ [\ n,;] + /) end def valid_message? @ message.present? end def valid_invitees? invalid_invitees.empty? end 

def create_invitation (email) Invitation.create (Casting: @casting, Absender: @sender, invitee_email: E-Mail, Status: 'ausstehend') Ende Ende "

Hässlicher Typ, was? Kannst du sehen, wie viel Bosheit hier gebündelt ist? Natürlich habe ich ein bisschen Kirsche drauf, aber Sie werden früher oder später auf Code wie diesen stoßen. Lasst uns darüber nachdenken, welche Verantwortung dies hat CastingInviter Klasse muss jonglieren.

  • E-Mail senden
  • Überprüfen auf gültige Nachrichten und E-Mail-Adressen
  • Leerraum loswerden
  • Aufteilen von E-Mail-Adressen in Kommas und Semikolons

Sollte das alles auf eine Klasse geworfen werden, die einfach einen Casting-Anruf über liefern möchte liefern? Sicherlich nicht! Wenn sich Ihre Einladungsmethode ändert, können Sie davon ausgehen, dass Sie in eine Schrotflintenoperation geraten. CastingInviter muss die meisten dieser Details nicht kennen. Das ist eher die Aufgabe einer Klasse, die sich auf den Umgang mit E-Mails spezialisiert hat. Zukünftig finden Sie hier auch viele Gründe, Ihren Code zu ändern.

Klasse extrahieren

Wie sollen wir damit umgehen? Das Extrahieren einer Klasse ist oft ein praktisches Refactoring-Muster, das sich als eine vernünftige Lösung für Probleme wie große, verschachtelte Klassen darstellt - insbesondere, wenn die betreffende Klasse mehrere Verantwortlichkeiten behandelt.

Private Methoden sind oft gute Kandidaten für den Einstieg - und auch für einfache Noten. Manchmal müssen Sie sogar mehr als eine Klasse aus einem solchen bösen Jungen extrahieren - tun Sie dies nicht in einem Schritt. Wenn Sie genügend kohärentes Fleisch gefunden haben, das in ein eigenes Spezialobjekt zu gehören scheint, können Sie diese Funktionalität in eine neue Klasse extrahieren.

Sie erstellen eine neue Klasse und verschieben die Funktionalität nach und nach. Verschieben Sie jede Methode einzeln und benennen Sie sie um, wenn Sie einen Grund dafür sehen. Verweisen Sie dann auf die neue Klasse in der ursprünglichen Klasse und delegieren Sie die erforderliche Funktionalität. Gut, dass Sie eine Testabdeckung haben (hoffentlich!), Mit der Sie überprüfen können, ob die Dinge bei jedem Schritt noch einwandfrei funktionieren. Versuchen Sie, Ihre extrahierten Klassen auch wiederzuverwenden. Es ist einfacher zu sehen, wie es in Aktion gemacht wird, also lesen wir etwas Code:

"Ruby-Klasse CastingInviter

attr_reader: Nachricht,: Eingeladene,: Casting

def initialize (Attribute = ) @message = Attribute [: Nachricht] || "@invitees = Attribute [: Eingeladene] ||" @casting = Attribute [: Casting] @sender = Attribute [: Sender] Ende

def gültig casting_email_handler.valid? Ende

def liefern casting_email_handler.deliver ende

Privatgelände

def casting_email_handler @casting_email_handler || = CastingEmailHandler.new (Nachricht: Nachricht, Eingeladene: Eingeladene, Casting: Casting, Absender: @sender) Ende Ende "

"Ruby-Klasse CastingEmailHandler EMAIL_REGEX = /\A([^@\s(++)@(???--[-a-z0-94++)

def initialize (attr = ) @message = attr [: message] || "@invitees = attr [: invitees] ||" @casting = attr [: casting] @sender = attr [: sender] end

def gültig gültige Nachricht? && valid_invitees? Ende

def liefern wenn gültig? Eingeladene Liste. E-Mail | invitation = create_invitation (email) Mailer.invitation_notification (invitation, @message) end else failure_message = “Ihre # @casting -Nachricht konnte nicht gesendet werden. Eingeladene E-Mails oder Nachrichten sind ungültig. Invitation = create_invitation (@sender) Mailer.invitation_notification (invitation, failure_message) end end

Privatgelände

def invalid_invitees @invalid_invitees || = invitee_list.map do | item | es sei denn item.match (EMAIL_REGEX) item end end.compact end

def invitee_list @invitee_list || = @ invitees.gsub (/ \ s + /, "). split (/ [\ n,;] + /) ende

def valid_invitees? invalid_invitees.empty? Ende

def valid_message? @ message.present? Ende

def create_invitation (email) Invitation.create (Casting: @casting, Absender: @sender, invitee_email: E-Mail, Status: 'ausstehend') Ende Ende "

In dieser Lösung sehen Sie nicht nur, wie die Trennung der Probleme Ihre Codequalität beeinflusst, sie liest sich auch viel besser und wird leichter zu verstehen.

Hier delegieren wir Methoden an eine neue Klasse, die darauf spezialisiert ist, diese Einladungen per E-Mail zuzustellen. Sie haben einen dedizierten Ort, der überprüft, ob die Nachrichten und eingeladenen Personen gültig sind und wie sie zugestellt werden müssen. CastingInviter Sie müssen nichts über diese Details wissen, daher delegieren wir diese Verantwortlichkeiten an eine neue Klasse CastingEmailHandler.

Das Wissen, wie diese Casting-Einladungs-E-Mails zugestellt und auf Gültigkeit überprüft werden können, ist jetzt in unserer neuen extrahierten Klasse enthalten. Haben wir jetzt mehr Code? Sie wetten Lohnte es sich, Bedenken zu trennen? Ziemlich sicher! Können wir darüber hinausgehen und umgestalten CastingEmailHandler etwas mehr? Absolut! Sich selbst ausknocken!

Falls Sie sich über die Frage wundern gültig? Methode auf CastingEmailHandler und CastingInviter, Hier können Sie einen benutzerdefinierten Matcher erstellen. Dies lässt mich etwas schreiben wie:

Ruby Expect (Casting_Inviter). to_valid

Ziemlich praktisch, denke ich.

Es gibt mehr Techniken für den Umgang mit großen Klassen / Gegenständen, und im Verlauf dieser Serie werden Sie einige Möglichkeiten kennenlernen, wie Sie solche Objekte umgestalten können.

Es gibt kein festes Rezept für den Umgang mit diesen Fällen - es hängt immer davon ab, und es ist eine Entscheidung von Fall zu Fall, ob Sie die großen Geschütze mitbringen müssen oder ob kleinere, inkrementelle Refactoring-Techniken am besten geeignet sind. Ich weiß, manchmal frustrierend. Das Single-Responsibility-Prinzip (SRP) zu befolgen, wird jedoch einen langen Weg gehen und ist eine gute Nase.

Lange Methode

Methoden zu haben, die ein bisschen groß geworden sind, ist eines der häufigsten Dinge, denen Sie als Entwickler begegnen. Im Allgemeinen möchten Sie auf einen Blick wissen, was eine Methode tun soll. Es sollte auch nur eine Verschachtelungsebene oder eine Abstraktionsebene aufweisen. Kurz gesagt, vermeiden Sie das Schreiben komplizierter Methoden.

Ich weiß, das hört sich hart an und ist es oft. Eine häufig vorkommende Lösung besteht darin, Teile der Methode in eine oder mehrere neue Funktionen zu extrahieren. Diese Refactoring-Technik wird als Methode extrahieren-Es ist eines der einfachsten, aber dennoch sehr effektiv. Als netter Nebeneffekt wird Ihr Code besser lesbar, wenn Sie Ihre Methoden entsprechend benennen.

Werfen wir einen Blick auf Feature-Spezifikationen, wo Sie diese Technik sehr brauchen. Ich erinnere mich daran, wie ich mich mit der Methode extrahieren beim Schreiben solcher Features und wie erstaunlich es war, als die Glühbirne weiterging. Da diese technischen Daten leicht verständlich sind, eignen sie sich gut für Demonstrationszwecke. Außerdem werden Sie beim Schreiben Ihrer Daten immer wieder auf ähnliche Szenarien stoßen.

spec / features / some_feature_spec.rb

"Rubin erfordert 'schienen_helfer'

Feature 'M markiert Mission als abgeschlossen' Szenario 'erfolgreich abgeschlossen' do visit_root_path fill_in 'Email', mit: '[email protected]' click_button 'Submit' visit missions_path click_on 'Mission erstellen' fill_in 'Mission Name', mit: 'Projekt Moonraker 'click_button' absenden '

in "li: contains ('Project Moonraker')" do click_on 'Mission beendet' Ende erwartet (Seite) .to have_css 'ul.missions li.mission-name.completed', Text: 'Project Moonraker' end end "

Wie Sie leicht sehen können, ist in diesem Szenario viel los. Sie gehen zur Indexseite, melden sich an und erstellen eine Mission für das Setup. Anschließend müssen Sie die Mission als abgeschlossen markieren und schließlich das Verhalten überprüfen. Keine Raketenwissenschaft, aber auch nicht sauber und definitiv nicht für die Wiederverwendbarkeit komponiert. Wir können es besser machen:

spec / features / some_feature_spec.rb

"Rubin erfordert 'schienen_helfer'

Feature 'M markiert Mission als abgeschlossen' do Szenario 'erfolgreich abgeschlossen' do_s_in_as '[email protected]' create_classified_mission_named 'Project Moonraker'

mark_mission_as_complete 'Project Moonraker' agent_sees_completed_mission 'Project Moonraker' Ende 

def create_classified_mission_named (mission_name) visit missions_path click_on 'Create Mission' fill_in 'Mission Name', mit: mission_name click_button 'Submit' end

def mark_mission_as_complete (mission_name) innerhalb von "li: contains ('# mission_name')" do click_on 'Mission beendet' Ende

def agent_sees_completed_mission (mission_name) erwarten (Seite) .to_css 'ul.missions li.mission_name.completed', Text: mission_name end

def sign_in_as (email) besuchen root_path fill_in 'Email', mit: email click_button 'Submit' end "

Hier haben wir vier Methoden extrahiert, die jetzt in anderen Tests einfach wiederverwendet werden können. Ich hoffe es ist klar, dass wir drei Fliegen mit einer Klappe schlagen. Die Funktion ist viel übersichtlicher, liest sich besser und besteht aus extrahierten Komponenten ohne Duplizierung.

Stellen Sie sich vor, Sie hätten alle möglichen ähnlichen Szenarien geschrieben, ohne diese Methoden zu extrahieren, und Sie wollten einige Implementierungen ändern. Nun wünschten Sie, Sie hätten sich die Zeit genommen, Ihre Tests zu überarbeiten und hätten einen zentralen Platz, um Ihre Änderungen anzuwenden.

Sicher, es gibt einen noch besseren Weg, um mit Feature-Spezifikationen wie this-Page Objects zum Beispiel umzugehen - aber das ist heute nicht unsere Aufgabe. Ich denke, das ist alles, was Sie über das Extrahieren von Methoden wissen müssen. Sie können dieses Refactoring-Muster überall in Ihrem Code anwenden - natürlich nicht nur in Spezifikationen. In Bezug auf die Häufigkeit der Nutzung schätze ich, dass es Ihre wichtigste Technik sein wird, um die Qualität Ihres Codes zu verbessern. Habe Spaß!

Lange Parameterliste

Schließen wir diesen Artikel mit einem Beispiel, wie Sie Ihre Parameter reduzieren können. Es wird ziemlich schnell langweilig, wenn Sie Ihren Methoden mehr als ein oder zwei Argumente hinzufügen müssen. Wäre es nicht schön, stattdessen ein Objekt vorzulegen? Genau das können Sie tun, wenn Sie a einführen Parameterobjekt.

All diese Parameter sind nicht nur schwierig zu schreiben und in Ordnung zu halten, sondern können auch zu Code-Duplizierung führen - und wir möchten dies sicherlich vermeiden, wo immer dies möglich ist. Was ich besonders an dieser Refactoring-Technik mag, ist, wie sich dies auch auf andere Methoden auswirkt. Sie sind oft in der Lage, eine Menge Parametermüll in der Nahrungskette zu beseitigen.

Gehen wir dieses einfache Beispiel durch. M kann eine neue Mission zuweisen und benötigt einen Missionsnamen, einen Agenten und ein Ziel. M ist auch in der Lage, den doppelten 0-Status der Agenten zu wechseln.

"Ruby-Klasse M def assign_new_mission (mission_name, agent_name, objektiv, licence_to_kill: nil) print" Mission # mission_name wurde # agent_name mit dem Ziel # Ziel zugewiesen. " wurde erteilt. ”else print“ Die Lizenz zum Töten wurde nicht erteilt. ”Ende Ende Ende

m = M.new m.assign_new_mission ('Octopussy', 'James Bond', 'Finde das Nukleargerät', licence_to_kill: true) # => Mission Octopussy wurde James Bond mit dem Ziel zugewiesen, das Nukleargerät zu finden. Die Lizenz zum Töten wurde erteilt. "

Wenn Sie das betrachten und fragen, was passiert, wenn die Parameter der Mission an Komplexität zunehmen, haben Sie bereits etwas vor. Das ist ein schmerzlicher Punkt, den Sie nur lösen können, wenn Sie ein einzelnes Objekt übergeben, das alle Informationen enthält, die Sie benötigen. In den meisten Fällen hilft dies auch dabei, die Methode nicht zu ändern, wenn sich das Parameterobjekt aus irgendeinem Grund ändert.

"ruby class Mission attr_reader: mission_name,: agent_name,: objektiv,: licence_to_kill

def initialize (mission_name: mission_name, agent_name: agent_name, Ziel: Ziel, Lizenz_zu_kill: licence_to_kill) @mission_name = missionsname @agent_name = agent_name @objective = objektiv @licence_to_kill = Ende der Lizenz_to_kill

def zuweisen print "Mission # mission_name wurde # agent_name mit dem Ziel # Ziel zugewiesen." if licence_to_kill print "Die Lizenz zum Töten wurde erteilt." else print "Die Lizenz zum Töten wurde nicht erteilt." Ende Ende Ende 

Klasse M def assign_new_mission (mission) mission.assign end end

m = M.new mission = Mission.new (mission_name: 'Octopussy', Agent_name: 'James Bond', Ziel: 'Finde das Atomgerät', licence_to_kill: true) m.assign_new_mission (mission) # => Mission Octopussy war James Bond zugewiesen mit dem Ziel, das Nukleargerät zu finden. Die Lizenz zum Töten wurde erteilt. "

Also haben wir ein neues Objekt erstellt, Mission, das ist nur auf die Bereitstellung ausgerichtet M mit den erforderlichen Informationen, um eine neue Mission zuzuordnen und bereitzustellen #assign_new_mission mit einem einzigen Parameterobjekt. Sie müssen diese lästigen Parameter nicht selbst übergeben. Stattdessen weisen Sie das Objekt an, die Informationen, die Sie in der Methode selbst benötigen, preiszugeben. Außerdem haben wir einige Verhaltensweisen - die Informationen zum Drucken - in das Neue extrahiert Mission Objekt.

Warum soll M Sie müssen wissen, wie Missionsaufträge gedruckt werden? Das neue #zuordnen profitierte auch von der Extraktion durch Gewichtsverlust, da wir den Parameter object nicht übergeben mussten mission.mission_name, mission.agent_name und so weiter. Jetzt benutzen wir einfach unser attr_reader(s), was viel sauberer ist als ohne Extraktion. Du gräbst?

Das Praktische daran ist auch das Mission sammelt möglicherweise alle möglichen zusätzlichen Methoden oder Zustände, die an einem Ort gut eingekapselt sind und für den Zugriff bereit sind.

Mit dieser Technik erhalten Sie Methoden, die kürzer sind, besser lesen und nicht die gleiche Gruppe von Parametern wiederholen. Ziemlich gutes Geschäft! Die Beseitigung identischer Parametergruppen ist auch eine wichtige Strategie für DRY-Code.

Achten Sie darauf, mehr als nur Ihre Daten zu extrahieren. Wenn Sie auch Verhalten in der neuen Klasse platzieren können, haben Sie Objekte, die nützlicher sind. Andernfalls riechen sie auch schnell.

Sicher, die meiste Zeit werden Sie mit komplizierteren Versionen davon konfrontiert - und Ihre Tests müssen bei solchen Refactorings sicherlich auch gleichzeitig angepasst werden - aber wenn Sie dieses einfache Beispiel im Griff haben, sind Sie bereit zu handeln.

Ich werde jetzt den neuen Bond sehen. Ich habe gehört, dass es nicht so gut ist…

Update: Saw Spectre. Mein Fazit: Im Vergleich zu Skyfall - was MEH imho-Spectre war - war Wawawiwa!