Ruby

Jeg rotet meg litt bort i programmeringspråket Ruby for noen år siden, og har lovet meg selv å komme tilbake til det nå når IronRuby blir modent på .NET plattformen. Her kan du lese det jeg har skevet om dette..

Litt ADO.NET i IronRuby

For automatisering av driftsrutiner og andre ad-hoc oppgaver er det gull å ha et bra, dynamisk skriptspråk tilgjengelig. Og med IronRuby får du også tilgang til å bruke hele .Net-rammeverket, så da har du i både pose og sekk for å si det sånn. Akkurat nå sitter jeg og lager noen ADO.Net-spørringer mot SQL Server i IronRuby, og tenkte det kunne være greit å vise hvor enkelt det er.

Så uten noe mer fjas og vas, her er et eksempel hvor jeg henter ut noe data og printer det til konsollet:

1 load_assembly System.Data
2 include System::Data::SqlClient
3
4 # Utility method to open a DB connection, read and print
5 # some data based on a SQL command and a hash of field
6 # display names and related ordinals in the recordset.
7 def execute_read connection_string, sql, fields
8   connection = SqlConnection.new connection_string
9   command = SqlCommand.new sql, connection
10   connection.open
11
12   reader = command.execute_reader
13   while reader.read
14     puts fields.inject({}) do |acc, field|
15       #{acc} #{field.first}: #{reader[field.last]} 
16     end
17   end
18   connection.close
19 end
20
21 execute_read(
22   User ID=foo;Password=bar;Data Source=THEBOSS\\SQL2005;Initial Catalog=theDB;,
23   SELECT * from Rule,
24   Id => 0, Name => 1, Active => 3, Transp => 4)

Så lett er det å inkludere et namespace fra .Net-rammeverket og ta det i bruk.

Det eneste som kan være litt vanskelig å tyde her er måten jeg skriver ut rader til konsollet på – jeg har blitt så utrolig glad i inject (aggregate/reduce/fold/whatever) i det siste, og bruker det hele tiden, men det resulterer ikke alltid i den mest lesbare koden i verden for dem som ikke er vandt til slikt ;)

Les også: Slette/tømme MSMQ-køer med IronRuby.

filtrer, projiser, aggreger

Det siste året har jeg knyttet intim bekjentskap til bruk av såkalte higher order functions; først da jeg virkelig tok i bruk LINQ og lambda i C# 3.0, deretter da jeg lærte meg Ruby, og til sist nå når jeg lærer meg å bruke språk som Erlang og F# i den funksjonelle paradigmen. Jeg har lært om Lambda calculus, og hva en closure er for noe – ting jeg nå mener alle utviklere burde ha et forhold til.

For å gi deg, kjære leser, et bedre innblikk i noe av dette, vil jeg snakke litt om noen av disse funksjonene av “høyere orden”. Higher order function betyr i bunn og grunn bare en funksjon som tar som innput en annen funskjon som spesifiserer en del av hva den skal utføre.

Filtrer

Filter er en av de mest vanlige, høyere orden funskjonene som brukes på lister eller andre datastrukturer. Gitt en liste og et predikat så brukes filter-funksjonen til å returnere de elementene i listen som tilfredstiller predikatet. Jeg sier “filter” fordi det er det funksjonen heter i de fleste programmeringsspråkene. I C# heter derimot filter-funksjonen for IEnumerable<T> Where, og den kan for eksempel brukes slik:

var aList = new []{1, 2, 3, 5, 8, 13, 21, 33, 54};
var evenNumbers = aList.Where(n => n % 2 == 0);
// array contains: 2, 8, 54

Filter-funksjonen for Enumerations i Ruby heter select (evt. find_all). Her er samme eksempel i Ruby:

1 $a_list = [1, 2, 3, 5, 8, 13, 21, 33, 54]
2 $even_numbers = $a_list.select {|n| n % 2 == 0}
3 # array contains: 2, 8, 54

Projiser

Den neste av de mest brukte, høyere orden funksjonene er Map. Denne funksjonen tar en liste eller lignende, samt en funksjon for å omforme (“mappe”) elementene i listen til en ny type elementer. Dette kaller vi også en projisering (projection). C#-varianten av map heter Select (og er altså ikke det samme som select i Ruby). Ønsker jeg for eksempel å konvertere listen med tall om til en liste med strenger kan jeg gjøre det slik:

var evenNumbersAsString = evenNumbers.Select(n => n.ToString());

I de fleste andre språk heter funksjonen map, men i Ruby har man også et alias som heter collect.

Aggreger

Filter og map er fine funksjoner som er anvendelige og lette å forstå og å huske på. En som ikke er fullt så mye brukt, blant utviklere innenfor den imperative paradigmen i alle fall, er Fold. For å forvirre oss fremstår også denne funksjonen med ulike navn i de ulike språkene – som for eksempel Reduce, Accumulate, Compress og Inject. Og i .NET heter den Aggregate:

Console.Write(evenNumbersAsString.Aggregate(
    "", (accumulator, next) => accumulator + next + " "));
// prints "2 8 54 "

Ser du hvordan den virker? I motsetning til de andre funksjonene som returnerer lister, så returnerer Aggregate én verdi. En verdi som er bygget opp ved å kalle det spesifiserte lambda-uttrykket for hvert element i listen. Accumulator er både output og input til lamdaen. For det første elementet vil accumulator ha verdien som ble spesifisert som den første parameteren til Aggregate, altså en tom streng. Når funksjonen kalles igjen for det andre elementet vil accumulator ha verdien som ble returnert fra den første. Og så fortsetter det til listen er tom, og accumulator returneres.

Her er nesten samme eksempel i Ruby, hvor fold heter inject (eller reduce om du ønsker å bruke det):

5 $foo = $even_numbers.inject() {|acc,next| #{acc}#{next.to_s}, }
6 puts $foo[0..-3] # outputs “2, 8, 54″

Har du aldri brukt en slik funksjon ser dette sikkert ganske gresk ut. Den er på kanten til å være uleselig, og hvis du jobber i et team som ikke er så godt kjent med fold/aggregate så bør du kanskje være forsiktig med å bruke den. Men jeg lover deg at om du venner deg til denne funksjonen så vil du i mange tilfeller kunne produsere mere kompakt og elegant kode.

Oppsummering i F#

Her er et oppsummerende eksempel i F#. Jeg oppretter en liste, og bruker pipelining til å sende listen gjennom filter, map (projiser) og fold (aggreger), for til slutt å skrive ut den resulterende strengen.

open System
 
let aList = [1; 2; 3; 5; 8; 13; 21; 33; 54]
 
aList |> List.filter (fun n -> n % 2 = 0)
      |> List.map    (fun n -> n.ToString())
      |> List.fold   (fun acc n -> acc + n + " ") String.Empty
      |> Console.WriteLine

Pixel-liv

I denne blogposten om hvordan det er å være en gammer programmerer refererte Uncle Bob Martin i kommentarfeltet til noe han kalte Langton’s Ant. Wikipedia kunne fortelle meg at dette er en todimensjonal Turingmaskin oppfunnet av Chris Langton i 1986. Den består av et par enkle regler, men utviser kompleks, emergent adferd. Reglene er: Står mauren på en hvit celle, snu 90 grader til høyre – står mauren på en svart celle, snu 90 grader til venstre. Endre deretter fargen på cellen, og gå én celle frem.

Langton’s Ant virket som en interessant, liten programmeringsoppgave i samme bane som Conway’s Game of Life. Jeg fikk selvsagt lyst til å prøve meg på en implementasjon, så jeg satte igang i mitt favoritt-fritids-språk Ruby. For den grafiske representasjonen fant jeg frem Gosu, et genialt og enkelt rammeverkt jeg har sett litt på tidligere for å lage grafiske spill i Ruby. Gosu gir meg et vindu med en update-draw-syklus (for dem som er vandt til spill-utvikling), og enkle metoder for å plassere og manipulere grafikk.

Og her er resultatet – etter noen få hundre steg har mauren laget en liten krussedull:

Ser du den lille, rød mauren? Etter noen tusen steg virker det som om alt bare er kaos:

Men etter rundt 10.000 steg begynner det å skje noe. Mauren begynner å lage en “motorvei”. Det er ikke bevist at dette alltid vil skje, men alle forsøk som er gjort – uansett start-forhold – har før eller senere produsert et slikt mønster.

Det var ganske kult å for en gangs skyld ikke kunne skrive enhetstester for adferden jeg var ute etter. I stedet måtte jeg vente over ti tusen sykler (ca 2,8 minutter – 0,01685 sekund pr steg – en framerate på nesten 60 pr sekund) for å se at mauren min fungerte som den skulle. Jeg kunne selvsagt skrevet tester for andre ting, men valgte å kjøre uten sikkerhetsnett denne gangen.

Hvis noen er interessert i å se koden så ligger den på min github-konto. Selve logikken for å vise et todimensjonalt sort/hvitt grid i et Gosu-vindu gjorde jeg generell. Jeg implementerte også en versjon av Conway’s Life med den – den ble noe tregere enn jeg hadde håpet, skylder det på Gosu-rammeverket inntil jeg finner en bedre optimalisering i min egen kode :P

Hva har du gjort i dag for å bli en bedre utvikler enn du var i går? Her er to forslag til hva du kan gjøre. 1) Les koden min, last den ned, og forsøk enkelte modifikasjoner for å forsikre deg om at du forstår den. Kom gjerne med spørsmål til koden, eller forbedringsforslag. Eller 2) Implementer din egen Langton’s Ant. Hva med en .NET-implementasjon vha. XNA?

Dynamisk opprette typer basert på XML

Én av fordelene med å programmere i et dynamisk språk er at man kan opprette typer og objekter basert på data som er ukjent i designtime. Som for eksempel å opprette sterkt typede objekter basert på XML. For hvorfor skal jeg jobbe med XML når jeg kan jobbe med objekter?!

Men da jeg begynte å skikke på XML-biblotekene i Ruby ble jeg faktisk litt skuffet.., det fungerte nemlig ikke sånn out-of-the-box. Jeg fant en rekke open source prosjekter som tenkte i samme bane som meg, men kvaliteten var svært varierende. Jeg fikk derfor lyst til å forsøke selv, mest får å trene på meta-programmering.

Jeg har ikke kommet så langt som jeg ønsket enda – denne formen for programmering er krevende, ettersom man har to lag med kode man må holde rede på (den man skriver, og den som oppstår når programmet kjører). Jeg har derimot noe som fungerer godt nok for et lite eksempel, så da er det på tide med en blogpost.

Sett at jeg har XML’en som følger nedenfor. Jeg ønsker å kunne parse denne med en generisk modul som gir meg en typet kolleksjon tilbake.

1 <people>
2   <person category=“Programmer”>
3     <firstname>Bob</firstname>
4     <lastname>Martin</lastname>
5   </person>
6   <person category=“Programmer”>
7     <firstname>Kent</firstname>
8     <lastname>Beck</lastname>
9   </person>
10 </people>

For akkurat dette eksempelet vil jeg ha ut en objektstruktur som den nedefor (den kunne vært bedre, men det er dette koden min produserer for øyeblikket). XmlObject opprettes ved å sende inn en xml. Når parsingen er ferdig har objektet fått en people-property. People er en kolleksjon av Person – med en count-propery og en iterator. Person-klassen som er opprettet har fått properties tilsvarende det vi finner i xml’en: category, firstname og lastname.

XmlObject_spec (2)

Her er et eksempel på bruk av XmlObject, tatt fra enhetstestene jeg brukte for å utvikle løsningen.

Utdrag fra xml_object_spec.rb:
28   def setup
29     @xml_obj = XmlObject.new PEOPLE_XML
30   end
—- snip —-
66   def test__usage_sample
67     list_of_names = []
68     @xml_obj.people.each do |person|
69       list_of_names << #{person.lastname}, #{person.firstname} (#{person.category})
70     end
71     list_of_names.first.should == Martin, Bob (Programmer)
72     list_of_names.last.should == Beck, Kent (Programmer)
73   end

PEOPLE_XML er dataene gjengitt i starten av denne artikkelen. Som du ser har @xml_obj etter setup fått en people-property jeg kan bruke til å iterere og hente ut data om personene. I linje 71 og 72 verifiserer jeg at objektene inneholdt det jeg forventet.

En fullstendig kodelisting av løsningen vil forvirre mer enn jeg ønsker.., jeg vil heller trekke frem et par ting som illustrerer hva som er mulig i forhold til in-memory kodegenerering i runtime. Nedenfor ser du for eksempel en metode jeg bruker for å opprette en ny klasse basert på et XML element.

15     def define_class_and_create_instanse name
16       name = name.capitalize
17       unless XmlObject.const_defined? name.to_sym
18         eval %(
19           class #{name} < Array
20             alias :count :length
21           end
22         )
23       end
24       klass = eval XmlObject::#{name}
25       [klass, klass.new]
26     end

Metoden tar inn navnet på elementet, og sørger for at det begynner med en stor bokstav (linje 16). Hvis klassen ikke er definert fra før (linje 17) oppretter jeg en ny klasse som arver fra Array ved å evaluere strengen i linje 19 til 21. Alias (linje 20) brukes for å døpe length-propertien til Array om til “count”, som er det jeg ønsker objektet skal ha.

I linje 24 bruker jeg eval igjen for å få tak i en referanse til den nye klassen. Til slutt returnerer jeg klassen samt en ny instans (i et array á to elementer – linje 25).

Neste eksempel er metoden jeg bruker for å opprette attributter (properties) og sette verdien for et gitt objekt.

50     def add_attr name, klass, instanse, value
51       attr_name = name.to_sym
52       klass.send(:attr_accessor, attr_name) unless klass.respond_to? attr_name
53       instanse.send #{attr_name}=.to_sym, value
54     end

I linje 52 sender jeg meldingen “attr_accessor” til klassen – se på det som å kalle en statisk metode som heter attr_accessor. Denne metoden brukes til å opprette en get/set-property (reader/writer accessor i Ruby) – attr_name bestemmer navnet på propertien.

Legg også merke til at kallet til “send” er etterfulgt av en unless-statement. Hvis klassen allerede har denne propertien, ved at den responderer på property-navnet, trenger jeg ikke opprette propertien på nytt.

I linje 53 setter jeg verdien på den nye propertien ved å bruke send-metoden til en instans av klassen. Det jeg egentlig gjør er å kaller setter-metoden for propertien – navnet på den er like navnet på propertien pluss et erlik-tegn (=). Andre parameter til send er verdien som skal settes.

Dette er bare et lite utdrag av de mange metodene og teknikkene man har tilgjengelig for introspection og dynamisk metaprogrammering i Ruby. Jeg håper dette gir et lite innblikk i de enorme mulighetene man har i dynamiske språk som Ruby i forhold til mere statiske språk.

Les også: Du MÅ beherske et dynamsik språk | Hvilket dynamisk programmeirngsspråk du skal lære deg

DCI arkitekturen

Eller: Hvordan jeg fikk sansen for multippel arv.

På QCon London fikk jeg ved en tilfeldighet med meg en halv forelesning med Jim O. Coplien, som snakket om noe han kalte The DCI Architecture. DCI står for Data-Context-Interaction, og er ifølge Coplien en bedre måte å designe systemer på enn dagens “normale” bruk av objektorientering.

“Object-oriented programming was supposed to unify the perspectives of the programmer and the end user in computer code: a boon both to usability and program comprehension. While objects capture structure well, they fail to capture system action. DCI is a vision to capture the end user cognitive model of roles and interactions between them.”

Fakta:
Opphavsmannen til DCI er professor ved Univeritetet i Oslo Trygve Reenskaug. Dette er samme nordmann som formulerte model-view-controller mønsteret i 1979.

For å få en komplett forståelse av hva denne visjonen er for noe bør du klikke på referansene i slutten av blogposten. Det du får her er en liten demo av min forståelse av DCI, gjennom at jeg ved hjelp av Ruby implementerer overføring av penger mellom to kontoer. Valget av programmeringsspråk er ikke tilfeldig – for å få til DCI er jeg avhengig av støtte for multippel arv, noe Ruby har i form av mix-ins. Og for at det skal bli virkelig elegant må jeg kunne påføre arven dynamisk i runtime.

La oss anta at vi allerede har en Account-klasse i domenet vårt som representerer en bankkonto. Den er veldig enkel (eller blodfattig som mange vil si), og har bare to read-only properties: navn og balanse. I tillegg har den en en liten metode for å representere kontoen som en streng (to_s tilvarer ToString() i C#).

account.rb:
1 class Account
2   attr_reader :name, :balance
3   def initialize name, balance
4     @name, @balance = name, balance
5   end
6   def to_s
7     #{@name}\t$#{@balance}
8   end
9 end

Når vi nå skal implementere overføring av penger mellom kontoer rører vi ikke den eksisterende Account-klassen. Vi vil i stedet implementere denne nye adferden i helt egne entiteter. Det å overføre penger er i teorien et abstrakt begrep, noe som kan foregå mellom andre ting enn bare Accounts. Vi vil derfor snakke om roller i stedet – overføring av penger har to roller: sender og mottager. Først implementerer jeg senderen, som jeg kaller for MoneySource:

money_tranfer.rb (del 1):
1 require transaction/simple
2
3 module MoneySource
4   class InsufficientFundsError < StandardError; end
5   attr_writer :recipient
6
7   def transfer amount
8     validate_transfer_of amount
9     Transaction::Simple.start(self) do |trans|
10       begin       
11         remove amount
12         @recipient.receive amount
13       rescue
14         log Aborting transfer of $#{amount} from #{name}
15         trans.abort_transaction
16       end
17     end
18   end
19   def validate_transfer_of amount
20     raise InsufficientFundsError if @balance < amount
21   end
22   def remove amount
23     @balance -= amount
24     log Removing $#{amount} from #{@name}
25   end
26 end

MoneySource har en metode kalt transfer som tar et beløp som eneste parameter. Først valideres det at det er tilstrekkelig med penger for å foreta overføringen. Deretter startes det en transaksjon (“transaction-simple” en et lite Ruby-biblotek som gir grei, in-memory transaksjonsstøtte) hvor MoneySource først fjerner det gitte beløpet fra sin egen balanse, for deretter å sende det samme beløpet til en mottager. Transaksjonen avbrytes om noe av en eller annen grunn skulle gå galt.

Merk at MoneySource ikke vet noe om Account, og Account vet ikke noe om MoneySource. Antagelsen MoneySource gjør er at den har variablene @name og @balance.., det er ikke tilfeldig at Account har det samme.

MoneySource har også en property for å sette en mottager (@recipient), og antar at denne har en receive-metode. Det er nå på tide å implementere den andre rollen, nemlig MoneyDestination:

money_tranfer.rb (del 2):
28 module MoneyDestination
29   def receive amount
30     @balance += amount
31     log Adding $#{amount} to #{@name}
32   end
33 end

I design time følger vi Single Responsibility Principle – hver modul/klasse har ett klart definert ansvar (om du lurte så er Accounts ansvar er å ha en balanse) – og vi unngår som nevnt tett kobling mellom entitetene. DCI er også en løsning som følger Open-Closed Principle, og er i høy grad en smidig arkitektur. Vi forsøker å unngå polymorfisme; alt er definert ett tydelig sted – vi har ingen virtuelle metoder som normalt gjør det vanskeligere å finne frem i koden.

“A program that follows the DCI paradigm exposes its inner workings to a reader of its code.” 

I runtime derimot vil vi i DCI-arkitekturen gi objektene roller i ulike kontekster, som lar dem samhandle på nye måter. Account er et data-objekt (D’en i DCI). MoneySource og MoneyDestination er roller som definerer interaskjonen (I’en i DCI) mellom objekter i en gitt kontekst (C’en i DCI). Rollene arves altså inn når de behøves.., et gitt objekt kan bekle mange, ulike roller. Det er her multippel arv kommer inn i bildet.

Den siste modulen jeg trenger definerer selve konteksten: MoneyTransfer. Den har en execute-metode som tar tre parametre: et objekt som skal være kilde, et objekt som skal være destinasjon, og beløpet som skal overføres. Execute utvider kilden med MoneySource (linje 37: MoneySource-modulen mikses inn i source-objektet i runtime). På samme måte utvides destinasjonen med MoneyDestination. Source har nå fått en property recipient og en metode transfer (som tidligere definert i MoneySource). Disse benyttes i linje 39 og 40 til å utføre overføringen.

money_tranfer.rb (del 3):
35 module MoneyTransfer
36   def self.execute source, target, amount
37     source.extend MoneySource
38     target.extend MoneyDestination
39     source.recipient = target
40     source.transfer amount
41   end
42 end

I denne demoen er source og target Account-objekter, men de behøver ikke være det.

Her er et lite skript som bruker MoneyTransfer. De fleste detaljene er uvesentlige og er derfor utelatt.

tranfer.rb:
47 require account
48 require money_transfer
49
50 setup_accounts # details omitted
51 list_accounts # details omitted
52 source = get_account Select account to transfer money from
53 target = get_account Select account to transfer money to
54 amount = get_amount  Specify amount to transfer
55 MoneyTransfer.execute(source, target, amount)
56 list_accounts

Jeg setter opp noen kontoer, lister dem i konsollet, og ber brukeren om å spesifisere source, target og amount. Jeg kaller så MoneyTransfer.execute, og lister kontoene igjen. Her er et eksempel på en slik overføring:

Sample run #1:
C:\Users\tormar\ruby_projects\DCI>transfer.rb
0: Salery       $1000
1: Usage        $1000
2: Savings      $1000
Select account to transfer money from: 1
Select account to transfer money to: 2
Specify amount to transfer: 800
Tue Mar 16 16:24:07 +0100 2010: Removing $800 from Usage
Tue Mar 16 16:24:07 +0100 2010: Adding $800 to Savings
0: Salery       $1000
1: Usage        $200
2: Savings      $1800

Siden jeg implementerte transaksjonshåndtering vil jeg også demonstrere hva som skjer om target av en eller annen grunn skulle kaste et exception (selv om det ikke har så mye med temaet å gjøre). Den røde logge-linjen kommer fra MoneyDestination:

Sample run #2:

C:\Users\tormar\ruby_projects\DCI>transfer.rb
0: Salery       $1000
1: Usage        $1000
2: Savings      $1000
Select account to transfer money from: 0
Select account to transfer money to: 2
Specify amount to transfer: 1000
Tue Mar 16 16:28:11 +0100 2010: Removing $1000 from Salery
Tue Mar 16 16:28:11 +0100 2010: Error: not able to receive money right now
Tue Mar 16 16:28:11 +0100 2010: Aborting transfer of $1000 from Salery
0: Salery       $1000
1: Usage        $1000
2: Savings      $1000

DCI er altså en reaksjon på det “oppfinnerne” ser på som en gal bruk av objektorientering. De hevder at måten vi begynte å bruke polymorphism, coupling, cohesion, etc da vi oppdaget OO på 80- og 90-tallet strider mot objektorienteringens mål, som var å gi et bedre samsvar mellom software og folks mentale modell av virkeligheten. De vil gjøre systemets adferd mer eksplisitt ved å gjøre roller til fullverdige entiteter – løsrevet fra objektene.

Dette får meg til å tenke på Command Query Responsibility Segregation (CQRS) som er så i skuddet for tiden – er ikke det også på en måte en reaksjon på det samme? Der gjør man i alle fall adferden eksplisitt gjennom command-objekter, og skreddersyr øvrig domenemodell i forhold til dem.

En siste tankevekker: Coplien og Reenskaug er to av dem som (såvidt jeg forstod) hardnakket hevder at språk som Java og C# ikke er orntlige, objektorienterte språk. I C# dreier egentlig alt seg om design av klasser, mens objekter har en mere sentral posisjon i språk som Ruby – hvor de f.eks. kan endre karakter fullstendig under kjøring av programmet. Man bryr seg sjelden om hvilken type et objekt har i Ruby, bare hvilke egenskaper det tilbyr. Jeg vet ikke om det er så fruktbart å stå på den barikaden, men det er interessant å diskutere forskjellen.

“Roles are about objects and how they interact to achieve some purpose. For thirty years I have tried to get them into the into the main stream, but haven’t succeeded. I believe the reason is that our programming languages are class oriented rather than object oriented. So why model in terms of objects when you cannot code them? And why model at all when you cannot keep model and code synchronized?” –Trygve Reenskaug

Til slutt må jeg få gjenta det jeg sa innledningsvis, ikke døm DCI ut fra det du har sett her. Dette har vært min tolkning, så gå til kildene for en dypere forståelse, som er: The DCI Architecture: A New Vision of Object-Oriented Programming | The Common Sense of Object Oriented Programming (pdf) | DCI på wikipedia | Trygve Reenskaug (UiO)

Events i Ruby

Ruby har ikke events slik som vi er vandt med fra .Net. Det er derimot ikke særlig vanskelig å implementere noe som ligner. Jeg begynte å eksperimentere litt, og her følger en slags log over hva jeg har forsøkt. Jeg har ikke landet på noen “best practice”, og det er heller ikke noe rocket science her – men jeg tror følgende kodesnutter kan være insteressante, særlig om man ikke er så veldig erfaren med Ruby enda.

Iterasjon 1

Jeg ønsker å implementere en Counter-klasse. Den skal ha en metode som heter increment som jeg kan kalle x antall ganger, hvor x er en limit jeg setter. Når limiten er nådd vil jeg at Counter-objektet informerer meg om dette ved å fyre av et event. Her er koden for klassen, samt litt kode som viser hvordan den brukes:

1 class Counter
2   def initialize limit
3     @limit = limit
4     @count = 0
5   end
6   def on_limit_reached= delegate
7     @on_limit_reached_delegate = delegate
8   end
9   def increment
10     @count += 1
11     @on_limit_reached_delegate.call if @count == @limit
12   end
13 end
14
15 c = Counter.new(5) # create a new counter
16 c.on_limit_reached = lambda{ puts Limit reached }
17 4.times { c.increment } # count up to limit -1
18 puts Limit not reached yet
19 c.increment # one more time, limit is now reached
Output:
Limit not reached yet
Limit reached

Løsninger er altså at vi lager en closure i linje 16 (også kalt lambda, kodeblokk, anonym metode, etc) som man sender til on_limit_reached= metoden. Counter-klassen tar vare på en referanse til denne closuren. Når limiten er nådd eksekveres den så i linje 11.

Merk at jeg glemte å legge til en test for om delegaten er satt, så koden i linje 11 vil feile om den ikke har noen lyttere (lover å skjerpe meg).

Iterasjon 2

Løsningen i iterasjon 1 støtter bare én lytter – hvis flere legges til vil den bare erstatte den første. Jeg ønsker derfor å utvide Counter for dette, og denne gangen husket jeg å legge til en test for om det er noen som lytter før jeg “fyrer av eventet”:

1 class Counter
2   def initialize limit
3     @limit = limit
4     @count = 0
5   end
6   def on_limit_reached= delegate
7     @limit_reached_delegates ||= []
8     @limit_reached_delegates << delegate
9   end
10   def increment
11     @count += 1
12     if @count == @limit
13       @limit_reached_delegates.each {|d| d.call } if @limit_reached_delegates
14     end
15   end
16 end
17
18 c = Counter.new(5) # create a new counter
19 c.on_limit_reached = lambda{ puts I was informed about limit reached }
20 c.on_limit_reached = lambda{ puts I was also informed about limit reached }
21 4.times { c.increment } # count up to limit -1
22 puts Limit not reached yet
23 c.increment # one more time, limit is now reached
Output:
Limit not reached yet
I was informed about limit reached
I was also informed about limit reached

Counter har nå et array av delegater: @limit_reached_delegates. Når limiten er nådd kaller jeg alle sammen. Jeg fikk desverre ikke til å bruke += for å legge til eventer, noe som ville ha virket riktigere for C#-utviklere.

Iterasjon 3

Det er på tide å legge opp støtte for flere eventer; jeg ønsker nå å bli fortalt hver gang telleren inkrementeres, og legger derfor opp en on_increment= metode. Jeg vil også sende med verdien på counteren i eventet.

1 class Counter
2   def initialize limit
3     @count, @limit = 0, limit
4     @event_handlers = {}
5   end
6   def on_limit_reached= delegate
7     (@event_handlers[:limit_reached] ||= []) << delegate
8   end
9   def on_increment= delegate
10     (@event_handlers[:increment] ||= []) << delegate
11   end
12   def increment
13     @count += 1
14     @event_handlers[:increment].each {|d| d.call(@count) } if @event_handlers[:increment]
15     if @count == @limit
16       @event_handlers[:limit_reached].each {|d| d.call } if @event_handlers[:limit_reached]
17     end
18   end
19 end
20
21 c = Counter.new(5) # create a new counter
22 c.on_limit_reached = lambda{ puts Limit reached }
23 c.on_increment = lambda{|count| puts Counter was incremented to #{count} }
24 5.times { c.increment }
Output:
Counter was incremented to 1
Counter was incremented to 2
Counter was incremented to 3
Counter was incremented to 4
Counter was incremented to 5
Limit reached

Jeg har nå brukt en Hash(-tabell) til å holde rede på alle handlerne – denne opprettes i linje 4. Den litt hårete syntaksen på linje 7 og 10 legger inn en ny array for en gitt event-nøkkel om arrayet ikke finnes enda, før den legger delegaten til arrayet. Deretter kan jeg trigge increment-eventet hver gang increment kalles (linje 14). Legg merke til at jeg sender inn @count når jeg kaller delegaten, og kan derfor bruke den i closuren i linje 23.

Iterasjon 4

Det ble litt mye “bråk” i Counter-klassen for å holde rede på event-handlerene i iterasjon 3, og jeg forsøker derfor å trekke ut denne logikken (Single Responsibility Principle). Jeg lager en Ruby-modul som jeg kan mikse inn i Counter (vi kaller det en mixin, som er Ruby’s løsning på multippel arv, noe vi ikke har i .Net). Modulen har nå handler-hashen, og brukes også til å trigge eventene:

1 module Events
2   def add_handler event, delegate
3     @event_handlers ||= {}
4     (@event_handlers[event] ||= []) << delegate
5   end
6   def raise_event event, *args
7     @event_handlers[event].each {|d| d.call(*args) } if @event_handlers[event]
8   end
9 end
10
11 class Counter 
12   include Events
13   attr_reader :limit
14   def initialize limit
15     @count, @limit = 0, limit
16   end
17   def on_limit_reached= delegate
18     add_handler(:limit_reached, delegate)
19   end
20   def on_increment &delegate
21     add_handler(:increment, delegate)
22   end
23   def increment
24     @count += 1
25     raise_event(:increment, self, @count)
26     raise_event(:limit_reached) if @count == @limit  
27   end
28 end
29
30 c = Counter.new(5) # create a new counter
31 c.on_limit_reached = lambda{ puts Limit reached }
32 c.on_increment do |sender, count|
33   puts Counter was incremented to #{count}
34   puts #{sender.limit – count} left..
35 end
36 5.times { c.increment }
Output:
Counter was incremented to 1
4 left..
Counter was incremented to 2
3 left..
Counter was incremented to 3
2 left..
Counter was incremented to 4
1 left..
Counter was incremented to 5
0 left..
Limit reached

Jeg valgte også å endre litt på on_increment for å illustrere en annen måte å lage closures på, som nok er mere vanlig i Ruby. I stedet for å bruke lambda-metoden kan jeg nå lage en kodeblokk ved hjelp av ‘do’ og ‘end’ (do og end kan byttes ut med { og } om man foretrekker det). Jeg sender også med selve counter-objektet som et argument til handleren (‘self’ i linje 25 tilsvarer ‘this’ i C#), som jeg så kan bruke til å beregne hvor mange increments som gjenstår – fordi jeg har definert en reader for limit-variabelen (linje 13).

Iterasjon 5

En annen approch jeg ville teste ut var å opprette en generisk Event-klasse. Jeg droppet da Events-modulen, selv om jeg kunne ha brukt dem i kombinasjon. I dette eksempelet har jeg sneket inn litt dynamisk evaluering også – se om du skjønner hva jeg gjør på linje 17 og 32.

1 class Event
2   def initialize
3     @handlers = []
4   end
5   def raise *args
6     @handlers.each {|h| h.call(*args)}
7   end
8   def << handler
9     @handlers << handler
10   end
11 end
12
13 class Counter 
14   attr_reader :limit
15   def initialize limit
16     @count, @limit = 0, limit
17     events :limit_reached, :increment
18   end
19   def on_limit_reached= delegate
20     @limit_reached << delegate
21   end
22   def on_increment &delegate
23     @increment << delegate
24   end
25   def increment
26     @count += 1
27     @increment.raise(self, @count)
28     @limit_reached.raise if @count == @limit   
29   end
30   private
31   def events *attr
32     attr.each {|e| eval(@#{e} = Event.new)}
33   end
34 end
35
36 c = Counter.new(5) # create a new counter
37 c.on_limit_reached = lambda{ puts Limit reached }
38 c.on_increment do |sender, count|
39   puts Counter was incremented to #{count}
40   puts #{sender.limit – count} left..
41 end
42 5.times { c.increment }
(Output som i iterasjon 4)
Jeg håper dette fungerte som eksempler på hvordan man kan kode med events i Ruby, og at du lærte litt underveis. Spørsmål til koden mottas og besvares med største fornøyelse.

Slette/tømme MSMQ-køer med IronRuby

Utviklingsavdelingen i PSWinCom har akkurat begynt å lære seg Ruby, og jeg har derfor kodet noen scripts som jeg har dokumentert ganske grundig – sånn at det går an å lære noe av å lese dem. Dette er et faktisk IronRuby-script jeg bruker for å slette eller tømme alle private MSMQ-køer på lokal maskin (vi jobber mye med køer, og trenger ofte å rydde opp miljøet).

Så i tilfelle dette kan hjelpe andre – her er koden:

  1 =begin
  2   This is a comment :)
  3
  4   Ruby scans scripts from top to bottom. This means it should make
  5   sense when you to read it that way too.
  6
  7   The scull and bones art is contained in something called a
  8   here-document. Google it if you want to know more…
  9  
10   (I had to escape the backslashes, so it looks a bit off in the
11   source, but the output should be nice.)
12 =end
13 puts <<EOF
14                _______________
15               /               \\ 
16              /                 \\ 
17             /                   \\ 
18             |   XXXX     XXXX   |
19             |   XXXX     XXXX   |           PSWinCom MSMQ IronRuby script
20             |   XXX       XXX   |
21             |         X         |           —————————–
22             \\__      XXX      __/
23               |\\     XXX     /|                  A L L   Q U E U E S
24               | |           | |
25               | I I I I I I I |             —————————–
26               |  I I I I I I  |
27               \\_             _/            Hope you know what you’re doing
28                 \\_         _/
29                   \\_______/
30           XXX                    XXX
31          XXXXX                  XXXXX
32          XXXXXXXXX         XXXXXXXXXX
33                 XXXXX   XXXXX
34                    XXXXXXX
35                 XXXXX   XXXXX
36          XXXXXXXXX         XXXXXXXXXX
37          XXXXX                  XXXXX
38           XXX                    XXX
39
40 EOF
41
42 =begin
43   The Actions class contains a method for each of the available
44   command line options. To add another option it should be enough
45   to expand this class with a new method.
46 =end
47 class Actions
48   # You create “static” methods (we actually call it class methods)
49   # by applying ‘self.’ in front of the method name. Here self refer
50   # to the class, and it could also be written ClassName.method_name.
51   def self.list queue # method named “list”, takes one argument “queue”
52     puts queue.path
53   end
54   def self.delete queue
55     puts Deleting #{queue.path}..
56     MessageQueue.delete queue.path
57   end
58   def self.purge queue
59     puts Purging #{queue.path}..
60     queue.purge
61   end
62   def self.exit
63     puts Usage: ir all_queues.rb action
64     puts        where action in [list|delete|purge]
65     Kernel.exit(0) # This is a nice way of ending a script
66   end
67 end # class Actions ends here..
68
69 =begin
70   The rest of the script is not contained in a class. You may feel dirty
71   the first couple of times you do this, but it’s fine – I promise!
72 =end
73
74 mode = $*.shift # get the first command line option passed to this script
75 Actions.exit unless mode # call exit if no mode (if mode == nil)
76
77 =begin
78   Load the .Net dll.
79   This is the only line of the script that will look strange to a normal
80   Ruby-developer that haven’t used IronRuby.
81 =end
82 load_assembly System.Messaging
83
84 =begin
85   By including the System::Messaging namespace in this script, I’m able to
86   use the classes in the namespace directly. If I hadn’t done this I would
87   have to use System::Messaging::MessageQueue instead of just MessageQueue.
88
89   Notice that when Ruby was parsing the Actions class, which use the
90   MessageQueue class, the System.Messaging.dll was not jet loaded, and the
91   namespace not yet included. That is fine however – when actual execution
92   enters the Actions class’s queue methods (when the methods are called),
93   the namespace will be there.
94 =end
95 include System::Messaging
96
97 =begin
98   Getting queues. MessageQueue.get_private_queues_by_machine in ruby corresponds
99   to MessageQueue.GetPrivateQueuesByMachine in C#.
100 =end
101 all_queues = MessageQueue.get_private_queues_by_machine(.) # ‘.’ == local machine
102
103 # Let user know if there are no queues to perform action on
104 puts No queues available if all_queues.length == 0
105
106 =begin
107   The following line is very similar to C#’s List<T>.ForEach(Action<T> action).
108   For each queue I call the appropriate action. I do this by sending the
109   command line option (mode) as a message to the Actions class. The method
110   with the correct name will be called. The second argument will become the
111   argument to the actual method.
112 =end
113 all_queues.each {|queue| Actions.send(mode, queue) }
114
115 puts . # Just an end-of-program indicator

For å for eksempel liste alle køene eksekverer man "ir all_queues.rb list" i kommandolinjen (all_queues.rb er navnet på skriptet og ir er IronRuby). Skriptet benyttes på egen rissiko, jeg tar ikke ansvar for tapte data eller andre problemer som måtte oppstå ;)

En agurktest

Om du ikke har levd under en stein det siste året så har sansynligvis hørt om Aslak Hellesøys Cucumber.

“Cucumber lets software development teams describe how software should behave in plain text. The text is written in a business-readable domain-specific language and serves as documentation, automated tests and development-aid – all rolled into one format.

“Cucumber works with Ruby, Java, .NET, Flex or web applications written in any language. It has been translated to over 30 spoken languages.

Men du har kanskje ikke testet det selv? Jeg tenkte derfor jeg kunne gjøre en rask demonstrasjon ved å skrive en akseptansetest for tjenesten jeg lagde i blogpostene om en minimal http-server i .Net / Ruby. Normalt vil man selvsagt beskrive featurene først, og så implementere dem og bruke cucumber til å bevise at de fungerer, men for enkelhets skyld tester jeg her altså min eksisterende tjeneste. Det spiller forøvrig ingen rolle om jeg tester .Net-varianten eller Ruby-varianten – jeg vil la akseptansetesten aksessere tjenesten direkte over http, og da er jo grensesnittet uansett det samme.

Det første jeg må gjøre er å beskrive tjenesten i form av en feature med tilhørende scenarier. Virker dette litt gresk anbefaler jeg at du leser deg litt opp på Behaviour-Driven Development (BDD).

File: features\add_service.feature:
1 Feature: Add-service
2   In order to save cycles on client CPUs
3   As an IT manager
4   I want a central server that can add numbers
5
6   Scenario: Add a string of numbers
7     When I send 9,11,33,100 to the add service
8     Then the response should be “The answer is 153″
9
10   Scenario: Add a single number
11     When I send 1 to the add service
12     Then the response should be “The answer is 1″

Feature-filen er input til cucumber, som vil analysere stegene i scenariene og forsøke å eksekvere dem. Men foreløpig skjønner cucumber lite, så vi må skrive litt kode for å definere stegene.

File: features\add_service_steps.rb:
1 require open-uri
2 require spec/expectations
3
4 When /I send (.*) to the (.*) service/ do |argument, service|
5   open(http://127.0.0.1:4567/#{service}?#{argument}) do |response|
6     @data = response.read
7   end
8 end
9
10 Then /the response should be “([^\"]*)/ do |expected|
11   @data.should == expected
12 end

Sånn, det er alt som trengs. Jeg lager et “When I send X to the Y service”-steg som gjør en forespørsel til tjenesten min og sender de definerte argumentene. Cucumber tar seg av å bytte ut X og Y med argumentene og navnet på tjenesten. “Then the response should be Z”-steget gjør en assert mot det forventede svaret som er beskrevet i scenariene.

Legg merke til at det er to forskjellige DSL’er (domenespesifike språk) i bruk her – en ekstern DSL for å definere features, og en intern Ruby-DSL (intern som i “er gyldig ruby-kode) for å definere stegene.

Når jeg kjører cucumber nå får jeg følgende output:

C:\Users\tormar\ruby_projects\httpListener>cucumber
Feature: Add-service
        In order to save cycles on client CPUs
        As an IT manager
        I want a central server that can add numbers

  Scenario: Add a string of numbers
    When I send 9,11,33,100 to the add service
    Then the response should be “The answer is 153

  Scenario: Add a single number
    When I send 1 to the add service
    Then the response should be “The answer is 1

2 scenarios (2 passed)
4 steps (4 passed)
0m0.122s

Alle scenariene for Add-servicen passerte (hvis ikke hadde det dukket opp endel rødt i output). Kult?

Hvis du er alergisk mot Ruby så finnes det også et prosjekt som heter SpecFlow som er en port av cucumber til .Net. Men det er absolutt å anbefale å benytte the real thing, gjerne med IronRuby på .Net-plattformen.

En minimal http-server i Ruby

I denne oppfølgingsposten til En minimal http-server i .Net viser jeg hvordan jeg raskt kan sette opp en tilsvarende løsning i Ruby. Jeg skal altså implementere en tjeneste som lytter på http, og som responderer på ulike argumenter. Løsningen skal være enkel å utvide med flere “respondere” – det skal ikke være nødvendig å editere eksisterende kode for å håndtere nye typer forespørsler (se forrige post om du vil vite mer om oppgaven).

Ruby shipper med mange, nyttige moduler – blant annet en søt, liten tjeneste som heter WEBrick, som kan brukes ganske likt som .Net’s HttpListener egentlig. I følgende program setter jeg opp en server til å lytte på port 8081:

   1 require ‘webrick’

   2 include WEBrick

   3 

   4 #DSL method for defining responders

   5 def respond_to config

   6     key = config[:key]

   7     $server.mount_proc(key) do |request, response|

   8         response.body = yield request.query.to_s

   9     end

   10 end

   11 

   12 def load_responders

   13     responder_definitions = Dir.glob(“*.responder”)

   14     responder_definitions.each { |d| load d }

   15 end

   16 

   17 $server = HTTPServer.new( :Port => 8081 )

   18 load_responders

   19 trap(“INT”) { $server.shutdown }

   20 $server.start

Servicen opprettes i linje 17, og i neste linje kaller jeg en metode jeg har kalt load_responders. Den henter alle filer med .responder extension, og kjører innholdet. Responder-filene i sin tur benytter respond_to metoden definert fra linje 5 til å konfigurere WEBrick.

Sidenote: Jeg implementerte først en løsning med en SimpleHttpServer-klasse og en klasse for å representere respondere. Etter å ha tenkt meg litt om så jeg derimot at det bare ble en masse stafasje, og at koden ikke kommuniserte så veldig godt hva den gjorde. Enkelhet er et av de viktigste budene for smidige utviklere, og Ruby lar meg skrelle bort ganske mye. Så etter å ha slettet 30 linjer kode følte jeg meg mer komfortabel. Om du vil se en mere objektorientert løsning kan du ta en titt på Building a DSL in Ruby, part II fra bloggen Technology as if People Mattered, som var en viktig inspirasjonskilde til denne bloggposten.

Nedefor er responder-filen for add-tjenesten. Se forrige post for å se hvordan denne responderen ser ut i .net. Som du kanskje ser er dette rett og slett et kall til respond_to. Som argument til metoden sendes nøkkelen “/add”, som er det responderen skal håndtere (ref bruk av attributtet RespondTo i .Net-løsningen). Resten er en kodeblokk som tar som input argumentene fra requesten, og returnerer et svar. Denne kodeblokken brukes til å håndtere forespørselen (magien ligger i “yield” i linje 8 i programmet over).

   1 respond_to :key => “/add” do |arguments|

   2     sum = 0

   3     numbers = arguments.split(‘,’)

   4     numbers.each { |n| sum += n.to_i }

   5     “The answer is #{sum}”

   6 end

Koden i denne responderen er litt C#-ish, jeg har gjort nøyaktig det samme som jeg gjorde i C#-varianten, bare oversatt det til Ruby. For å gjøre den mere rubyesque benytte vi et par array-metoder som Ruby har arvet fra SmallTalk: map (som egentlig er en alias fro collect, men jeg liker map bedre) tar et array, kjører en gitt transformasjon på hvert element (i dette tilfellet eksplisit konvertering til integer), og returnerer et nytt array med resultatet. Dette føles nok ikke så  fremmed for .Net-utviklere lengre, nå som vi har vendt oss til Linq, som tilbyr samme funksjonalitet via Select-metoden.

Det andre trikset er metoden inject. Den kan brukes til å “samle informasjon” fra et array, i dette tilfellet summen av alle argumentene. Dermed kan spesifikasjonen av add-responderen modifiseres til å se slik ut:

   1 respond_to :key => “/add” do |arguments|

   2     numbers = arguments.split(‘,’).map {|arg| arg.to_i}

   3     “The answer is #{numbers.inject {|x,n| x+n }}”

   4 end

Resultatet er altså at jeg på 20 linjer har satt opp en dynamisk webserver som jeg kan utvide ved å legge til flere .responder-filer. Definisjonen av hver responder er veldig konsis og grei, og står på ingen måte tilbake for .Net-løsningen. I Ruby har jeg ikke behøvd å definere interface for respondere, og lastingen av dem – som er basert på fil-extension i stedet for refleksion og attributter – er mye enklere. Når du tar med i betraktning at jeg ikke engang behøver å kompilere Ruby-løsningen, så er det ikke vanskelig for meg å foretrekke denne når jeg får behov for å raskt sette opp web-tjenester av ulik art f.eks. for å simulere tjenester jeg skal integrere mot.

Sinatra entrer scenen

frank_sinatra Å bruke WEBrick til dette her er ganske “low level” (på samme måte som HttpListener var det). I .NET-verden har vi rammeverk for webutvikling på et høyere nivå som blant andre WebForms, ASP.NET MVC, FubuMVC og MonoRail (det er egentlig alle jeg vet om). Ruby har også dette; det desidert mest kjente er Ruby on Rails, som gjør deg ekstremt produktiv så sant du er villig til å følge Rails konvensjoner og måter å gjøre ting på. Ramaze er et rammeverk med mye større frihet, hvor man kan velge mellom et hav av moduler og måter å gjøre ting på. Begge disse baserer seg i hovedsak på Model-View-Controller paradigmet.

Sinatra er et tredje ruby-biblotek som er ganske nyttig til å utvikle mindre websider og tjenester. Det minner mye om det jeg har gjort i denne artikkelen, og ved å bruke Sinatra kan jeg forenkle tjenesten min ganske mye (som om den ikke var enkel nok allerede).

Sinatra-versjonen av selve tjenesten min ser slik ut:

   1 require ‘sinatra’

   2 Dir.glob(“*.responder”).each { |d| load d }

Ved å inkludere sinatra-bibloteket startes automatisk en webserver. Det eneste jeg da trenger er å dynamisk laste alle responder-filene. Jeg har slått sammen linje 13 og 14 fra det orginale skriptet, og står dermed igjen med én require og én kodelinje.

Responder-filen ser nesten ut som tidligere, men kallet til respond_to, som jeg selv definerte, har vi nå et kall til sinatras get-metode (‘get’ som i REST-metoden get):

   1 get “/add” do

   2     numbers = params.to_s.split(‘,’).map {|arg| arg.to_i}

   3     “The answer is #{numbers.inject {|x,n| x+n }}”

   4 end

Dermed har jeg gått fra 24 til 6 linjer. Det er latterlig lite!

Og den tilsvarende C#-løsningen fra forrige post var på over 170 linjer. Det finnes selvfølgelig mere optimale løsninger, men jeg har vært en C#-utvikler i åtte år, og 170 liner ++ var det jeg havnet på. Jeg har vært Ruby-utvikler på hobbybasis i et par måneder, og landet på 6 linjer. Det MÅ jo si noe om Ruby og dynamisk programmering!

Dagens sitat via SMS

Jeg fortsetter å rote med Ruby, og deler villig med alle som har lyst til å lære. Denne gangen har jeg laget et lite skript som henter et daglig oppdatert visdomsord fra nettet, og sender det til en distribusjonsliste på SMS via PSWinCom‘s SMS Gateway. Programmet illustrerer hvor enkelt det er å hente og parse en RSS-feed, bruk av konfigurasjonsfiler i YAML-format, og sending av XML til en server over TCP (ett av PSWinCom’s mange integrasjonsgrensesnitt).

ruby_quote

Det komplette programmet er listet ut nedenfor. La meg kjapt gå gjennom de ulike delene:

Metoden get_quote (linje 5 til 11) tar inn URL’en til en RSS-feed fra BrainyQuote som gir meg ett nytt sitat hver dag. Jeg åpner en connection mot adressen, og parser den med RSS-parser som er en del av Ruby’s grunninstallasjon. Jeg plukker ut det første elementet, slår sammen beskrivelsen (selve sitatet) og tittelen (hvem sitatet stammer fra), og returnerer dette (siste statement i en ruby-metode returneres automatisk).

Metoden get_sms_xml (linje 13 til 28) tar inn nødvendige parametre og bygger opp XML’en jeg skal sende til SMS Gateway’en. Dokumentasjon av dette formatet er definert i denne pdf’en. I metoden bruker jeg noe som kalles for et Here-Document – en “tradisjonsrik” måte å definere strenger over flere linjer på som du også finner i PHP, Perl, Python, Unix shells, Windows Powershell etc.

Metoden send_sms (linje 30 til 34) åpner en TCP socket på en gitt adresse og port, sender XML’en, og avslutter. Det er alt som skal til for å sende SMS gjennom systemet vårt.., så sant du har en konto vel-å-merke.

QuoteSMS.rb:
1 require rss
2 require open-uri
3 require yaml
4
5 def get_quote uri
6   open(uri) do |con|
7     rss = RSS::Parser.parse(con.read, false)
8     quote = rss.items.first
9     #{quote.description} -#{quote.title}
10   end
11 end
12
13 def get_sms_xml gw, receivers_xml, text
14 <<EOF
15 <?xml version=”1.0″?>
16 <SESSION>
17 <CLIENT>#{gw[:client]}</CLIENT>
18 <PW>#{gw[:password]}</PW>
19 <MSGLST>
20 <MSG>
21 <TEXT>#{text}</TEXT>
22 #{receivers_xml}
23 <SND>#{gw[:sender]}</SND>
24 </MSG>
25 </MSGLST>
26 </SESSION>
27 EOF
28 end
29
30 def send_sms gw, xml
31   session = TCPSocket.new(gw[:host], gw[:port])
32   session.write xml
33   session.close
34 end
35
36 settings = YAML.load_file(settings.yml)
37
38 quote = get_quote settings[:quote_uri]
39 xml = get_sms_xml(settings[:gateway],
40                   settings[:receivers].map {|r| <RCV>#{r}</RCV>},
41                   quote)
42 send_sms settings[:gateway], xml

I linje 36 leser jeg inn konfigurasjonsdata (mer om dette om litt). Deretter er det bare å kalle de tre metodene, og hey-presto(!) så er jobben gjort. Nå kan jeg for eksempel sette opp en scheduled task til å kjøre dette programmet hver dag, og “spamme” mine venner med gode sitater på telefonen.

Legg merke til linje 40 som konverterer en array av telefonnumre til en XML streng array vha. metoden map. Dette tilsvarer å bruke Select-metoden i Linq i .NET:

    1 var list = new long[] { 4700000001, 4700000002, 4700000003 };

    2  IEnumerable<String> xml = list.Select(phonenumber

    3      => string.Format(“<RCV>{0}</RCV>”, phonenumber));

Det at jeg kan skyte inn arrayet i en annen streng som i linje 22, og at elementene da “flates ut” til én streng er minst like kult. Tilsvarende får du ikke til i .NET uten å eventuelt endre definisjonen av ToString() for enumerasjonen det gjelder. Let’s not go there…

YAML Ain’t Markup Language

Som sagt har jeg valgt å bruke YAML som format på konfigurasjonsfilen til dette programmet. YAML er et serialiseringsformat som er tilgjengelig for mange programmeringsspråk. Fordelen er at det er mye mere lesbart og editeringsvennlig enn XML, som har vært det mest populære formatet i noen år nå. YAML har mer til felles med JSON.., I praksis er JSON faktisk et subset av YAML. 

settings.yml:
1
2 :gateway: 
3   :port: 9876
4   :client: demoAccount
5   :password: somePassw0rd
6   :host: 127.0.0.1
7   :sender: RubyQuote
8 :quote_uri: http://feeds.feedburner.com/brainyquote/QUOTEBR
9 :receivers:
10   - 4790000001
11   - 4790000002
12   - 4790000003

Denne settingsfilen definerer en Hash (el. dictionary om du vil) med tre nøkler med tilhørende verdier. Som nøkler har jeg brukt symboler – de tingen som begynner med kolon i Ruby – men strenger eller andre datatyper hadde fungert like fint. Nøkkelen :gateway har en ny Hash som verdi, som definerer settingene for å kontakte og bruke PSWinCom gatewayen. :sender gir for eksempel navnet jeg har valgt på avsenderen (se bildet av mobilen tidligere i artikkelen). Og nei, dette er ikke reelle brukernavn og passord…

Nøkkelen :quote_uri refererer til en streng som inneholder RSS-adressen. :receivers refererer til en array med telefonnummer. Når denne filen parses får jeg tilbake en Hash med alle disse verdiene i korrekte typer.

YAML er heller ikke begrenset til primitive typer som streng, arrays osv – man kan også serialisere og deserialisere Ruby-objekter man selv definerer, uten at det ser spesielt mer komplisert ut enn filen over. Faktisk trenger det egentlig ikke eksistere noen definisjon av objektene i programmet i det hele tatt – YAML-parseren vil konstruere nye typer om det trengs basert på det som er definert i YAML-filen. Ruby er jo tross alt et dynamisk språk. Ganske fiffig faktisk!

Konklusjon

Du har nå sett hvor enkelt det er å hente og parse en RSS feed. Du har også sett hvor smertefritt det er å sende SMS vha litt XML og PSWinCom’s SMS Gateway. Og hvis ikke du fikk lyst til å bytte ut alle XML-konfigurasjonsfilene dine med YAML så har jeg kanskje forklart litt dårlig hvor kult det er.

Objektorienteringen er kanskje ikke så mye å skryte av, men for et program på 42 linjer ville det bare ha vært i veien. Håper dette inspirerte!

Se også: Send SMS med SOAP for hvordan du enkelt kan bruke PSWinCom’s SOAP-grensesnitt for sending i .NET.


Torbjørn: La oss anta to ulike definisjoner av Template Method pattern - de to ytterpunkte...

Lars-Petter: Hei igjen. Siden du inviterer til å ta diskusjonen i bloggen, og har tatt deg t...

Torbjørn: Lars-Petter: Det er én måte å se det på. Det er absolutt fortsatt Template M...

Lars-Petter: Hei. Har du ikke i prinsippet her gått over fra Template Method (arv) til Strat...

Christian Abildsø: I alle fall i C#, så føles dette som kode som blir mer fleksibel men vanskelig...

Torbjørn: Hei Henrik, og takk for tilbudet. Ble oppmerksom på Rasberry Pi for under en uk...

Henrik Sandaker Palm: Ang. større hobby prosjekt. Du er som er en slik rakker på programmering har j...

Øivind Nilsen: Slutt å bruke mobilnummeret mitt som eksempel !...

Bjørn Einar Bjartnes: Jeg har også latt meg fascinere av Clojure, uten at jeg har kommet så veldig l...

Bjørn Einar Bjartnes: Sweet :) Jeg tror egentlig jeg liker det som det er, med musikk. Litt av utford...

 Hold deg oppdatert

Søk i bloggen

Ferske innlegg

  • Template Method del 4: Multippel arv
  • Template Method Intermesso
  • Template Method del 3: Bare funksjoner
  • Template Method del 2: På vei mot funksjonell programmering
  • Kategorier

  • .net ninja (37)
  • Bøker (17)
  • Diverse prosjekter (35)
  • DSL (10)
  • Erlang (10)
  • F# (5)
  • Hardware (1)
  • Jobb (77)
  • Julekalender (51)
  • kjempekjekt.com (23)
  • LISP/Clojure (33)
  • NNUG / community (60)
  • O/RM & databaser (10)
  • Off topic (116)
  • OO-design/clean code (30)
  • Podcasts (14)
  • Polyglot (77)
  • Ruby (27)
  • Silverlight / RIA (3)
  • Software/verktøy (20)
  • Softwareutvikling (20)
  • Testing / TDD (30)
  • the contiki strip (13)
  • User experience (3)
  • WCF (3)
  • Webutvikling (32)
  • WPF (9)
  • WTF (12)
  • Last ned Wallpaper

    Programmeringsbloggens tøffe skrivebordsbakgrunn med snippets fra ulike språk laster du ned her!

    Abonner via epost

    Om du vil kan du få alle nye blogposter tilsendt til din epost. Abonner nå, det er kjempeenkelt!

    Meta