Hoppa till innehåll
Meny
CDhistory
CDhistory

Ruby Method Auditing Using Module#prepend

Publicerat den december 12, 2021 av admin

I går intervjuade jag en Ruby-utvecklare på ett företag här i Denver när min intervjuare gav mig en bra metaprogrammeringsutmaning: han bad mig skriva en automatiserad metodrevisor.

Han beskrev en klassmetod som skulle ta namnet på en instansmetod och generera en automatisk, loggad pappersspårning varje gång som målmetoden kallas. Något i stil med detta:

Det skulle ge utdata som såg ut så här:

Jag kom fram till en lösning som fungerade men som innebar några potentiellt farliga hackningar av Object#send. Innan jag presenterade min lösning sa jag ”detta är förmodligen en dålig idé, men…” och jag hade rätt. Bra idéer börjar sällan på det sättet.

Lösningen som han föreslog innebar att man använde den ofta förbisedda Module#prepend och jag tyckte att den var tillräckligt häftig för att motivera en artikel.

Strategin gick ut på att använda beteendet hos Module#prepend för att skapa en dynamiskt genererad omslagsmetod för Messenger#share.

I denna wrapper har vi ett ”Performing”-meddelande som avfyras innan metodkroppen exekveras, själva utförandet av metoden och sedan ett sista ”Exiting”-meddelande när utförandet av metoden har slutförts framgångsrikt.

Men för att förstå hur vi ska använda Module#prepend för att göra detta behöver vi först ha en god förståelse för hur metodarv fungerar i Ruby.

  • Ancestor Chains
  • Modulen själv
  • Sammanfogning av allting
  • Slutsats

Ancestor Chains

Varje Ruby-objekt sitter i slutet av vad som kallas en Ancestor Chain. Det är en lista över de anhörigobjekt som objektet ärver från. Ruby använder denna Ancestor Chain för att bestämma vilken version av en metod (om någon alls) som exekveras när objektet tar emot ett meddelande.

Du kan faktiskt se Ancestor Tree för vilket objekt som helst genom att ringa Module#ancestors. Här är till exempel anförankringskedjan för vår Messenger-klass:

När vi #include eller #prepend en modul i en klass gör Ruby ändringar i klassens anförankringskedja och justerar vilka metoder som hittas och i vilken ordning. Den stora skillnaden mellan #include och #prepend är var ändringen görs. Låt oss skapa en modul som heter Auditable som (så småningom) kommer att innehålla koden som utför vår revisionsmagi:

Om vi använder Module#include för att importera de (för närvarande obefintliga) metoderna från Auditable, kommer Ruby att klämma in den modulen som en anfader till vår Messenger-klass. Se själv:

När vi anropar en metod i Messenger kommer Ruby att titta på vad som är definierat i Messenger-klassen och – om den inte kan hitta en metod som matchar anropet – klättra uppåt i anförankringskedjan till Auditable för att leta igen. Om den inte hittar det den söker på Auditable går den vidare till Object och så vidare.

Det är #include. Om vi istället använder Module#prepend för att importera modulens innehåll får vi en helt annan effekt:

#prepend gör Messenger till en förfader till Auditable. Vänta. Vad?

Det kan se ut som om Messenger nu är en överklass till Auditable, men det är inte riktigt vad som händer här. Instanser av klassen Messenger är fortfarande instanser av klassen Messenger, men Ruby kommer nu att leta efter metoder att använda för Messenger i modulen Auditable innan den letar efter dem i själva klassen Messenger.

Och det, mina vänner, är vad vi kommer att dra nytta av för att bygga den här revisorn: om vi skapar en metod som heter Auditable#share, kommer Ruby att hitta den innan den hittar Messenger#share. Vi kan sedan använda ett super-anrop i Auditable#share för att komma åt (och exekvera!) den ursprungliga metoden som definierats på Messenger.

Modulen själv

Vi kommer faktiskt inte att skapa en metod som heter Auditable#share. Varför inte? Därför att vi vill att detta ska vara ett flexibelt verktyg. Om vi hårdkodar metoden Auditable#share kommer vi bara att kunna använda den på metoder som implementerar metoden #share. Eller ännu värre, vi skulle vara tvungna att återimplementera samma granskningsmönster för varje metod som vi någonsin vill granska. Nej tack.

Så istället ska vi definiera vår metod dynamiskt och audit_method-klassens metod för att avfyra den:

När audit_method anropas i en implementerande klass skapas en metod i Auditable-modulen med samma namn som den metod som ska granskas. I vårt fall skapas en metod som heter Auditable#share. Som nämnts kommer Ruby att hitta den här metoden innan den hittar den ursprungliga metoden på Messenger eftersom vi prependierar Auditable-modulen i den implementerande klassen.

Det betyder att vi kan använda ett superanrop för att nå upp i anförankringskedjan och exekvera Messenger#send. När vi gör det skickar vi vidare argumenten som vi har samlat in (*arguments) uppåt i kedjan också.

När vi har anropat den ursprungliga metoden skriver vi ut vårt exitmeddelande och slutar för dagen. Bra jobbat!

Sammanfogning av allting

Nu är det bara en fråga om att prependlägga den här modulen till vår Messenger klass och vi borde vara redo att köra:

Och herregud, det fungerar:

Den här implikationen är enorm för auditering, men det finns mer att göra med det här tricket. Du kan använda prepending för att ändra objektens beteende utan att ändra själva objekten. Detta är ett anpassningsbart och tydligt sätt att skapa komponenter av högre ordning i Ruby. Prestandatestning, felhantering. Du kan göra mycket här.

Slutsats

Ruby-moduler är mer komplicerade än vad de flesta tror. Det är inte så enkelt som att ”dumpa” kod från ett ställe till ett annat, och att förstå dessa skillnader låser upp några riktigt snygga verktyg för ditt verktygsbälte.

Du kanske har lagt märke till att jag bara pratade om Module#include och Module#prepend idag, och att jag inte berörde Module#extend. Det beror på att den fungerar väldigt annorlunda än sina kusiner. Jag kommer snart att skriva en djupgående förklaring av Module#extend för att komplettera uppsättningen.

För tillfället, om du vill lära dig mer rekommenderar jag att du läser Ruby-moduler: Include vs Prepend vs Extend av Léonard Hetsch. Den var verkligen till hjälp när jag satte ihop allt detta.

Lämna ett svar Avbryt svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *

Senaste inläggen

  • Acela är tillbaka:
  • OMIM Entry – # 608363 – KROMOSOM 22q11.2 DUPLIKATIONSSYNDROM
  • Kate Albrechts föräldrar – Lär dig mer om hennes far Chris Albrecht och hennes mor Annie Albrecht
  • Temple Fork Outfitters
  • Burr (roman)

Arkiv

  • februari 2022
  • januari 2022
  • december 2021
  • november 2021
  • oktober 2021
  • september 2021
  • augusti 2021
  • juli 2021
  • juni 2021
  • maj 2021
  • april 2021
  • DeutschDeutsch
  • NederlandsNederlands
  • SvenskaSvenska
  • DanskDansk
  • EspañolEspañol
  • FrançaisFrançais
  • PortuguêsPortuguês
  • ItalianoItaliano
  • RomânăRomână
  • PolskiPolski
  • ČeštinaČeština
  • MagyarMagyar
  • SuomiSuomi
  • 日本語日本語
©2022 CDhistory | Drivs med WordPress och Superb Themes