Objektorientering i Ruby och Elixir

 

 

link: http://mikepackdev.com/blog_posts/45-object-orientation-in-ruby-and-elixir

När man talar om vanliga programspråk, som vi ofta sätter dem i två stora hinkar: objektorienterad programmering och funktionell programmering. Det finns andra program paradigm, men vi agerar som OOP och FP är som olja och vatten. I denna artikel kommer jag att sudda ut linjerna i dessa två paradigm.

Det har varit lite diskussion i Erlang och Elixir världen runt detta ämne. Det har nyligen konstaterats att Elixir är “den mest objektorienterat språk.” Avdi Grimm har genomförda övningar från Brian Marick Funktionell Programmering för Objektorienterad Programmerare, ursprungligen skriven i Clojure. Det finns även en lustiga bibliotek och lightning talk från Wojtek Mach för out-of-the-box objektorientering i Elixir.

Dessa resurser till att kasta ljus på OOP i Elixir, men de behöver inte visa de byggstenar som utgör en fungerande modell. I den här artikeln, vi kommer att bygga ett objekt system i Elixir från scratch och bara ben som möjligt. För att göra detta, alla Elixir kod kommer att vara baserad på Ruby exempel. Målet är att lyfta fram några av de centrala begreppen Elixir och likna dem centrala begrepp för Ruby. (En viss förtrogenhet med Elixir antas.)

Om vi ska bygga en fungerande modell, det är inte tänkt att vara ett perfekt objekt systemet inte heller är jag som förespråkar denna typ av programmering. I själva verket, Erlang gemenskapen bygger på väldigt olika mönster. Detta är bara en övning i att lära sig om budskap som går och statliga förvaltning i Elixir. Det finns sannolikt flera sätt att uppnå samma effekt och jag skulle älska att höra om andra tekniker i kommentarerna.

Primära Begrepp inom objektorientering

Objektorienterad och funktionell programmering har samexisterat sedan början av moderna datorer. Funktionell programmering kommer från matematik och var givetvis tänkt först. Objektorientering kom strax efter med Simula, vars mål var att göra den statliga styrningen enklare.

Mest litteratur på objektorientering rehashes samma grundläggande koncept: tillstånd, beteende och polymorfism. I Ruby, detta kan översättas till exempel variabler, metoder, anka och skriva. Dessa fragment kan utökas till andra OO begrepp, som inkapsling och FASTA principer.

Men modern objektorienterad språk är onödigt begränsande. Det är nästan som om de missar skogen för alla träd. Den stora tekniker i objektorientering kan skrivas i en funktionell stil, om du vill ha dem, med bibehållen renhet och säker samtidighet, som vi alla vill ha.

En Grundläggande Objekt

Ett objekt som binder ihop tillstånd och beteende. Det sammanfattar stat internt och exponerar beteende externt. Inkapsling är viktigt eftersom det är ett effektivt sätt att organisera och styra staten. Beteende är det medel genom vilket vi ändrar tillstånd.

Betrakta följande exempel på en bil i Ruby. Bilen har en position, x, och förmågan att köra framåt. När det driver, det steg x med 1.

19

Att märka i den ovan klass, när #kör är kallade, den skriver ut namnet på den klass, dess färg-attribut, och förändringen i staten x.

Den rörliga @attrs är inkapslade staten. #kör – metoden är problemet. Vad som är vackert om denna klass är att när instansieras, vi kan kalla den #kör – metoden och vi behöver inte tänka på det inre tillståndet av x. Låt oss göra det nu.
29

Vi kallar #kör två gånger och det inre tillståndet av x ändringar. Inkapsling är enkla och kraftfulla.

Vi kan skriva denna “klass” och detta “objekt” i Elixir. Den främsta skillnaden är hur vi kapsla in staten och åberopa beteende. I stället för inkapsling att vara en första klassens medborgare som det är i Ruby, vi använda rekursion för att representera staten. Istället för att ringa metoder som vi gör i Ruby, passerar vi ett meddelande.

39а

Här är hur vi skulle använda den:

49

Låt oss bryta detta Elixir koden anges.

nya/1 funktion som heter “ny” för att spegla Ruby .ny – metoden, men det kan heta vad som helst. Denna funktion skapar ett nytt Elixir-processen med hjälp av spawn_link/1. Vi kan betrakta spawn_link/1 motsvarigheten till Ruby ‘ s särskilda .ny – metoden. Genom lek en ny process, vi har nu skapat en yta för att kapsla in staten.

Om du inte är bekant med Elixir processer, kan du tänka på dem på samma sätt som du tänker på operativsystem processer – i själva verket de är modellerad efter OS processer men är extremt lätt och signifikant snabbare. De kör oberoende av varandra, har sitt eget minne som inte blöder, och kan inte i isolering.

Inne i den nyskapade process, standard attribut är sammanslagna med de attribut som skickas som ett argument. Sedan, rekursiv funktion kör/1 är kallade.

59

run/1 – funktionen är kärnan i våra “objekt” och staten är rekursivt skickas till kör/1 om och om igen, kapsla staten som ett argument till funktionen. När vi vill uppdatera läget, vi kallar kör/1 – funktionen med den nya staten.

Låt oss titta närmare på kör/1 funktion.

69

En viktig komponent för att få detta att fungera är det samtal till att få funktion. När att få är kallade till, det kommer att blockera den nuvarande processen och vänta tills du får ett meddelande. Kom ihåg, denna kod körs i en ny process för alla sina egna. När ett meddelande skickas till processen, det kommer att avblockera sig och köra koden förklaras i samband med förfarandet block.

Detta förfarande block beräknar en ny stat genom uppräkning x i en ny variabel, uppdatering x i den karta som representerar staten, och sedan rekursivt anrop run/1 funktion. Efter att kör/1 rekursivt, processen igen block på att få. Det fortsätter att rekursivt göra detta på obestämd tid tills kör/1 – funktionen bestämmer sig för att inte kalla sig längre. När vi inte längre recurse, processen dör och staten är skräp samlas in. (Detta icke-rekursiva fallet är inte representerade i denna kod.)

Låt oss se igen vad “instansiering” och budskap som går ut. Den inbyggda funktionen, skicka/2 som används för att skicka en kör meddelande till processen två gånger.

79

I ovanstående kod, kallar Bil.nya/1 spawns processen och returnerar ett process-ID eller pid.” Den skickar då ett meddelande till den här pid med hjälp av skicka/2. skicka/2 är i princip samma sak som att anropa en metod i Ruby. Upphovsmannen till begreppet objektorientering, Alan Kay, känns remissed som meddelande passerar har fördrivits av method invocation. Den stora skillnaden mellan budskap som går och method invocation är att budskap som går är asynkron – mer om det senare.

Det är vår grundläggande objekt. Vi har inkapslade staten och förutsatt beteende som ändrar tillstånd. Ruby version döljer staten i en instans variabel och Elixir versionen gör staten explicit som en rekursiv funktion som argument. Ruby version samtal metoder, Elixir version passerar meddelanden.

Arv

Utöver tillstånd och beteende, arv är en annan grundläggande princip i objektorienterad programmering. Arv tillåter oss att utöka typer (klasser) med nya tillstånd och beteende.

Arv är en första klassens medborgare i Ruby, vilket gör det lätt att kategorisera tillstånd och beteende i undertyper. Följande kod ska vara påtaglig för alla Rubyists. Denna kod skapar en ny Lastbil det är typ som en subtyp av Bil och lägger till #offroad – metoden endast tillgänglig för lastbilar.

89
Eftersom vi har ärvt från Bil med klassen, vi kan kalla både den #kör och #offroad metoder på en instans av Lastbil – klass.

99

Det är Ruby version. Elixir har inte klasser. Arv i Elixir är inte en första klassens medborgare. Det kommer att kräva mer installation och ceremoni för att utföra.

För det första, hur gör vi för att representera olika typer och subtyper utan klasser? En observant läsare har märkt att vid definiera Bil – modul i Elixir, en av de förvalda värden var ett område som heter typ med värdet av “Bil”. I Elixir, klasser och typer kan representeras som vanligt av data, som binärfiler (string). Konceptet med att använda data för att representera olika typer, och inte konkreta klasser som med Ruby, som är genomgående i funktionell programmering – ta till exempel poster och rader märkta.

För att modellen ärvt typer i Elixir vi kommer att använda data för att representera Bil och Lastbil subtyp. För att efterlikna arv av beteenden (metoder) som en subtyp härrör från en överordnad typ, vi ska hålla en instans av Bil som vi delegera meddelandet till.

910

Låt oss bryta ner ovanstående kod.

I nya/1 – funktion, vi är tvingande värde av typ egendom och instansieras en ny Bil. Detta blir vår inmatad data. Att vi sedan leka våra överordnade processen.

911
Vi måste hålla våra överordnade processen runt så att vi kan delegera meddelanden när subtyp inte direkt svara. Då kallar vi vår run/1 funktion.

912

run/1 – funktionen i Lastbil – modulen bör ser bekant ut. Vi har lagt till en ny offroad meddelanden som vi inte svara på. När Lastbil processen får ett meddelande om den inte förstår, för det framåt det på att föräldern Bil – processen.
Låt oss se hur det går.

913

Du kan se att Lastbil typ har ärvt allt av beteendet Bil.

Polymorfism

Polymorfism är en av objektorientering starkaste egenskaper. Det är till vad programmering utbytbara delar är till tillverkning. Det tillåter oss att ersätta undertyper för deras överordnade typ var den överordnade typen används. Dessutom i Ruby och Elixir, gör det möjligt för oss att ersätta alla skriver för en annan typ lika länge som det svarar för att rätt metod eller ett meddelande.

Precis som arv, polymorfism följer den Liskov substitutionsprincipen, ett väl etablerat kännetecken för god objektorienterad design och en del av den FASTA principer design.

För det första, polymorfism i Ruby. Vi kommer att använda Bil och Lastbil exempel för att visa att de är utbytbara med hänsyn till #kör – metoden. Vi kommer slumpmässigt att välja antingen exempel och kallar #kör.

914

Array#prov – metoden kommer att returnera antingen en Bil eller Lastbil exempel, men eftersom dessa är anka skrivit objekt kan vi lyckas ringa #kör på heller. Om vi hade en annan klass som inte ärver från Bil men innehöll också en #drivemetod, skulle vi kunna ersätta en instans av denna klass här också. Polymorfism vid dess finaste.

Det är lika lätt i Elixir. Istället för att ringa metoder, vi kommer att skicka meddelanden. Den enda stora skillnaden mellan Ruby och Elixir versionen är hur vi väljer den slumpmässiga objekt eller en process. Resten är i stort sett identiska.

 

915

Polymorfism är en naturlig del av Elixir, men det är sällan tänkte på detta sätt. Ett Elixir processen tar gärna emot alla meddelanden som du klarar det, oavsett om det kan göra något med det budskapet. Det finns inga begränsningar på vilka meddelanden som kan skickas. En phantom meddelande kommer helt enkelt att sitta i processen postlåda, men det anstår mig att nämna att ohanterat meddelanden kan orsaka minnesläckor.

Asynchrony

För de flesta avseenden, som vi har byggt upp stora delar av ett objektorienterat system i ett funktionellt språk. Det har en del brister och inte använder den mest sofistikerade Elixir verktyg, men det visar att det är möjligt att representera dessa mönster i Elixir.

Det är en inte så subtil nyans dolda i dessa kodexempel som skulle föda sitt huvud omedelbart vid själva genomförandet. Anropa metoder i Ruby är synkron och passerar meddelanden i Elixir är asynkron. Med andra ord, att kalla en Ruby metod kommer att göra en paus i programmet, utföra kroppen av den metoden, och returnera resultatet av denna metod för att den som ringer. Det är en blockering, synkron verksamhet. Passerar ett meddelande i Elixir är en icke-blockerande, asynkron verksamhet. Elixir kommer att skicka en process, ett meddelande och genast tillbaka utan att vänta på att meddelandet ska tas emot.

Detta kan göra enkla saker i Ruby mer besvärliga i Elixir. Ta till exempel försöker bara att returnera ett värde från en annan meddelandet. I Ruby detta är enkel.

916

Vi kan göra samma sak i Elixir när vi pratar inte om meddelandet passerar. Nedan, vi ringer en funktion som returnerar ett värde och allt fungerar som förväntat.

917

Men när vi börjar arbeta med processer, detta blir mer utmanande. Här är ett intuitivt, men trasig bit av Elixir kod.

918

Hade du väntat dig att skicka den till bil – processen en färg meddelande om du vill returnera värdet “Röd”? I stället är det returnerade värdet är färg. skicka/2 returnerar meddelandet som skickades till en process, inte det värde som returneras när meddelandet har hanterats.

Budskap som går i Elixir är asynkron, men om vi vill att modellen synkron beteende Ruby ‘ s method invocation vi måste få lite kreativ.

Sedan att få blockerar processen och väntar på ett meddelande, kan vi använda det i samband med våra ringer. Så, vem kallar färg skulle behöva för att blockera och vänta på ett svar för att fortsätta programmet, precis som Ruby.

Till skillnad från Ruby, det är lite mer ceremoni i att få detta att fungera. Vi kommer att behöva skicka den som ringer pid i båda. De båda kommer då att skicka tillbaka ett meddelande till den som ringer med den slutliga avkastningen värde.

919

I ovanstående kod, som vi passerar på den som ringer pid i callee, som kan nås genom att ringa self/0. Den som ringer sedan väntar på ett meddelande från båda innehåller svar. I den ringer, svaret är mönstret matchas för att extrahera värde. Returvärdet från uppringarens att få blockera den slutliga svaret på “Röd”.

Det är en hel del av ceremonin. Lyckligtvis, Elixir har fin abstraktioner för att undvika litania. Här kommer vi att titta på Agenter. Med hjälp av Agenter, kan vi behandla vår kod synkront igen och eliminera den låga nivå på skicka och att få funktioner.

920

Elixir har en mängd olika verktyg som bidrar till att hålla koden ren medan programmering synkront. Ett sådant verktyg är GenServer.ring/3. GenServers är en mycket användbar abstraktion kring processer som tillåter oss att genomföra tillstånd och beteende i en förenklad form, ungefär som Ruby.

Som en sista tanke runt asynchrony, jag skulle vilja nämna två saker.

Budskap som går i Elixir är långsammare än method invocation i Ruby. Detta är på grund av att det finns en fördröjning mellan när meddelandet skickas och när den mottagande process som hanterar den.

Budskap som går i Elixir är den primitiva samtidighet konstruera. Det är den skådespelare modell av samtidighet. Detta är inte ett alternativ i Ruby om du använder ett bibliotek som Celluloid. Samtidighet i Ruby är oftast gängade. Skådespelaren modell är en abstraktion om trådar, bakat in Elixir, som ger en nivå av samtidighet inte uppnås i Ruby.

Avsluta

Vi har blandat objektorientering och funktionell programmering under loppet av denna artikel. Oavsett om du föredrar Ruby version eller Elixir version, de båda har sin plats.

Objektorientering i Ruby är enkelt, elegant och gör mig glad. Det ger inte samtidighet kontroller som Elixir erbjuder, men planeringen modell är behaglig att använda. Å andra sidan, Elixir låter oss modellera ett system i ett objektorienterat sätt samtidigt utnyttja mer kraftfull samtidighet kontroller.

Objektorientering i Elixir kan eller kan inte vara en framkomlig väg. Jag har inte tillräckligt med data ännu inte att dra några slutsatser. Det är värt att nämna igen att den funktionella använder olika mönster. Skaparen av Erlang, Joe Armstrong, har klagade över OOP på grund av den blandning av tillstånd och beteende, även om jag tycker att denna blandning är oundvikliga processer. Så även om det kan vara vanligt att använda funktionella språk i en motsatt orienterad stil, det är säkert möjligt och kan vara mer graciös när modellering några domäner.

Glad kodning!