Objekter i Processing

Fritt etter «Objects» av Daniel Shiffman: https://processing.org/tutorials/objects/


1. Objekter

Før vi begynner å utforske detaljene omkring hvordan objekt-orientert programmering (OOP) fungerer i Processing, kan det være lurt å få en overordnet forståelse av begrepet «objekt».

Forestill deg at du ikke programmerer i Processing, men at du istedet lager et program for dagen din, en liste med instruksjoner, om du vil. Den kan starte omtrent slik:

  • Våkne
  • Stå opp
  • Drikke kaffe eller te
  • Spise frokost: müsli, blåbær og soyamelk
  • Sykle til jobb eller skole
  • Padle kajakk

Dette handler om deg. Du innehar bestemte egenskaper: du ser ut på en bestemt måte, kanskje du har brunt hår, bruker briller, er ung eller gammel. Du har også evnen til å gjøre ting: som å stå opp, spise, sykle og padle kajakk.

Du er et objekt med bestemte egenskaper, og med evnen til å gjøre forskjellige ting.

Men, tenker du kanskje, hva har dette med programmering å gjøre? Egenskaper og evner? Egenskapene til objektet er variabler, evnene til objektet er funksjoner.

Objektorientert programmering er en sammensmelting av de grunnleggende elementene i programmering: data og funksjonalitet.

La oss liste opp data og funksjoner for en menneskelig objekt:

Menneskelige data (egenskaper)

  • Høyde
  • Vekt
  • Kjønn
  • Øyefarge
  • Hårfarge

Menneskelige funksjoner (evner)

  • Sove
  • Våkne
  • Spise
  • Sykle
  • Padle

OK, før vi fortsetter må vi gjøre en liten metafysisk betraktning: Strukturen over er ikke et menneske i seg selv, den beskriver bare ideen bak det å være et menneske, hva det vil si å være et menneske. Å være et menneske er å ha en kroppshøyde, ha hår, å sove, å spise osv. Denne forskjellen mellom ideen bak det å være menneske, og selve menneske i kjøtt og blod, er viktig når vi programmerer med objekter.

Ideen bak det å være menneske, selve malen for det å være menneske, kalles i programmering for en klasse. En klasse er forskjellig fra et objekt. Du er et objekt. Jeg er et objekt. Mannen som syklet forbi deg på vei til jobb er et objekt, Kristine Bonnevie og Albert Einstein er et objekter. Vi er alle sammen objekter. Vi er virkelige, eksisterende forekomster av klassen menneske, ideen bak det å være et menneske.

​Tenk på en pepperkakeform. Pepperkakeformen har formen til en pepperkake, den lager pepperkaker, men den er ikke en pepperkake selv. Pepperkakeformen er klassen. Pepperkakene er objektene.


​2. Å bruke et Objekt

Før tar for oss klassen som sådan, la oss ta en rask kikk på hvordan vi, ved å bruke objekter, kan gjøre vår kodeverden til et bedre sted å være 🙂

Se for deg en liksomkode (kode med normal tekst) for en enkel skisse som flytter et rektangel over skjermen. (Vi tenker på dette rektangelet som en «bil»).

Data (Globale variabler: variabler som skal gjelde for hele programmet)

  • ​Bilens farge
  • Bilens x-posisjon
  • Bilens y-posisjon
  • Bilens hastighet

Klargjør:

  • Gi bilen en farge
  • Gi bilen startposisjon
  • Gi bilen en hastighet

Tegn:

  • Fyll bakgrunnen med farge
  • Vis bilen i bilens posisjon med bilens farge
  • Øk bilens posisjon med bilens hastighet

For å gjøre om liksomkoden til Processing uten å bruke objekter, vil vi definere globale variabler i toppen av programmet, gi dem startverdier i setup(), og bruke (kalle på) funksjoner som flytter og viser bilen i draw(). Slik som dette:

  • ​color c; // Variabel for farge
  • float x; // x-posisjon
  • float y; // y-posisjon
  • float speed; // Hastighet, hvor mange punkter figuren skal flyttes for hver runde i draw()
  • void setup() {
    • size(200,200);
    • x = 0;
    • y = 100;
    • speed = 1;
    • c = color(0); // c får fargeverdien svart
  • }
  • void draw() {
    • background(255);
    • move(); // Kaller på funksjonen move()
    • display(); // Kaller på funksjonen display()
  • }
  • void move() {
    • x = x + speed;
    • if (x > width) { //Hvis x-verdien (øvre venstre hjørne) er større enn bredden på lerretet
      • x = 0; // Figuren flyttes til venstre kant 
    • }
  • }
  • void display() {
    •   fill(c); // Svart
    •   rect(x, y, 30, 10); // Tegn «bil»
  • }

    Med objektorientering blir det mulig å ta variablene og funksjonene ut av hovedprogrammet, og  inn i et bil-objekt.

Et bil-objekt vil vite om dataene (egenskapene) sine: farge, posisjon og hastighet. Bil-objektet vet også hva det kan gjøre: kjøre og vises.

Inne i et objekt brukes ikke betegnelsene «variabel» og «funksjon». Variablene kalles i stedet «felt», og funksjonene kalles «metoder». I denne teksten brukes begrepene litt om hverandre.

Hvis vi bruker objekter på liksom-koden vi startet med, blir det seende slik ut:

Data (globale variabler):

  • Objektet Bil

Klargjør:

  • Initialiser, gi verdier til objektet Bil

Tegn:

  • Fyll bakgrunnen med farger
  • Vis objektet Bil
  • Flytt objektet Bil


Legg merke til at vi har fjernet alle de globale variablene fra de første eksemplet. Istedet for å ha egne variabler for bilens farge, posisjon og hastighet, har vi nå bare én variabel, en Bil-variabel!

Og istedet for å gi startverdier (initialisere) de tre variablene, initialiserer vi nå bare en ting, objektet Bil. Hvor ble det av variablene? De eksisterer fortsatt, men nå lever de lykkelig inne i objektet Bil (og vil bli definert i klassen («pepperkakeformen») Bil, som vi kommer til om et lite øyeblikk).

Nå skriver vi om liksom-koden til Processing-kode. Denne gang med objekter. Da kan det se slik ut (på engelsk…):

​Car myCar; // Vi kunne like gjerne brukt norske navn, bare vi holder oss borte fra æ,ø og å: Bil myBil;

  • void setup() {
    • myCar = new Car();
  • }
  • void draw() {
    • background(255);
    • myCar.drive();
    • myCar.display();
  • ​}

Vi kommer til å ta for oss koden over i detalj om et lite øyeblikk, men først tar vi en kikk på hvordan vi skriver koden for klassen Car.


3. Koding av klassen (pepperkakeformen)

Det enkle bil-eksempelet over demonstrerer hvordan bruken av objekter i Processing gjør koden ryddig og lettlest. Jobben blir å skrive objekt-malen, pepperkakeformen dvs klassen Car.

Når du første gang lærer om objektorientert programmering kan de være nyttig å ta et program uten objekter, og så skrive det om til et program med objekter. Vi kommer nå til å gjøre nettopp dette med bil-eksempelet, gjenskape nøyaktig det samme programresultatet på en objektorientert måte.

Alle klasser må inkludere fire deler:

  • Navn
  • Data
  • Konstruktør
  • Metoder (funksjoner)

(Teknisk sett er det bare navnet som er nødvendig, men skal det være noen hensikt med å programmere objektorientert, må alle fire med).

​Her  er hvordan vi kan ta elementene fra en enkel skisse uten objekter, og plassere dem i en bil-klasse. Fra denne klassen kan vi så lage bil-objekter. Objektene følger malen (klassen), men har ulik farge (c), posisjon (x-pos og ypos), og hastighet (xspeed).

Picture
https://processing.org/tutorials/objects/

Navn: Klassen starter med ordet «class» etterfulgt av et hvilket som helst navn som du velger. Deretter skriver vi all klasse-koden innenfor to klammeparenteser. I følge tradisjonen bruker vi stor forbokstav i klassenavn.

Data: Klassens data er en samling av variabler. Disse kalles ofte instansvarabler fordi hvert objekt har sitt sett variabler (der verdiene kan være ulike).

Konstruktør: Konstruktøren er en spesiell funksjon i klassen. Konstruktøren lager selve objektene. Her gir du instruksjonene for hvordan objektet skal settes opp. Den fungerer på samme måte som setup()-funksjonen, bare at her blir den brukt til å lage et objekt inne i programmet hver gang et nytt objekt blir opprettet fra klassen. Konstruktøren har alltid samme navn som klassen. Den blir aktivert med denne setningen:

  • Car myCar = new Car();
  • Car er typen (tilsvarende int, float etc, men her er typen et Car-objekt),
  • myCar er navnet på objektet (kan være hva som helst).
  • newCar() er kallet på konstruktøren som oppretter objektet «myCar» av typen Car.

Funksjonalitet: Vi legger til funksjonalitet (evner) til objektet ved å skrive metoder (funksjoner).

Legg merke til at klasse-koden er en egen kodeblokk som kan bli plassert hvor som helst utenfor setup() odraw().

  • void setup() {
    • // kode her
  • }
  • void draw() {
    • // kode her
  • }
  • class Car {
    • // kode her
  • }

​​4. Bruke et objekt: detaljene

​Tidligere tok vi en rask kikk på hvordan et objekt kan forenkle innholdet i setup() og draw()

  • // Steg 1. Deklarer et objekt.
  • Car myCar; // Reserver plass i minnet: Type: Car. Navn: myCar
  • void setup() {
    •  // Steg 2. Initialiser objektet
    •  myCar = new Car(); // Lag objektet
  • }
  • void draw() {
    • background(255);
    • // Steg 3. Kall opp metodene til objektet.
    • myCar.drive(); // myCar sin drive-metode
    • myCar.display(); //myCar sin display-metode
  • }

La oss se på detaljene bak de tre stegene over for å lære hvordan du kan bruke objekter i din programskisse.

Steg 1. Deklarer et objekt.
En variabel blir alltid deklarert ved å spesifisere hvilken type og gi den et navn. Med en primitiv datatype (slik som en heltallsvariabel), vil det se slik ut:

  • // Variabeldeklarasjon
  • int var; // Datatype: int. Navn: «var»

Primitive datatyper er enkeltstående biter med informasjon, f. eks. heltalsvariabel (int), desiamaltallsvariabel (float), tegnvarianel (char). Det å deklarere en variabel som inneholder et objekt er omtrent det samme. Den eneste forskjellen er at her er det navnet på klassen som er datatypen. Vi finner selv på navnet, i dette tilfellet «Car».

Objekter er forresten ikke primitive datatyper, de betraktes som komplekse datatyper. De lagrer mange biter med informasjon, både data og funksjonalitet. Primitive datatyper lagrer bare data.

Steg 2. Initialisere  et objekt.
For å initialisere en variabel (dvs gi den en startverdi), bruker vi en tildelingsoperasjon, dvs.: «variabel» er lik «noe». Med en primitiv type, ser det slik ut:

  • // Variabelinitialisering
  • var = 10; // variabelen «var» gis verdien 10;

Det å initialisere et objekt er litt mer omstendelig. Istedet for ganske enkelt å tildele en verdi, må vi nå konstruere objektet. Et objekt blir laget ved hjelp av ordet new

 // Objektinitialisering
myCar = new Car(); // Ordet «new» brukes for å lage objektet

I eksemplet over er «myCar» navnet på objektvariabelen. Likhetstegnet viser at vi setter dette lik noe. Dette «noe», er en ny forekomst av objektet «Car». Hva vi egentlig gjør her er å initialisere et Car-objekt. Når du initialiserer en primitiv variabel, slik som heltallsvariabel, så setter du variabelen lik et tall. Men et objekt kan inneholde mange biter med data.

Tenker vi tilbake på Car-klassen, husker vi at kodelinjen over kaller på klassens konstruktør, en spesiell funksjon ved navn «Car()» som initialiserer objektets variabler og klargjør objektet for bruk.

En ting til: hvis du hadde glemt å initialisere den primitive variabelen «var» (dvs. gitt den verdien 10), ville Processing ha gitt den en standardverdi: 0. Et objekt (slik som»myCar») har imidlertid ingen standardverdi. Hvis du glemmer å initialisere et objekt, vil Processing gi det verdien «null». «Null» betyr her «ingenting», ikke tallet null, ikke minus en, men rett å slett ingenting, absolutt tomhet. Hvis du støter på en feilmelding som sier «NullPointExeption» (en ganske vanlig feil), skyldes den sannsynligvis at du har glemt å initialisere et objekt.

Steg 3. Bruke et objekt
Når du har deklarert og initialisert et objekt, kan vi begynne å bruke det. Å bruke et objekt innebærer å kalle på funksjonene som vi har bygget inn i objektet. Et menneskelig objekt kan spise, en bil kan kjøre, en hund kan bjeffe. Å kalle på en funksjon som befinner seg inne i et objekt gjøres ved å bruke punktum mellom objektnavn og funksjonsnavn.

objektnavn.funksjon(argumenter);

I tilfellet Car er har verken drive() eller display() argumenter, derfor blir det seende slik ut:

  • // Funksjoner kalles på med punktum
  • myCar.drive();
  • nyCar.display();

​5. Konstruktør-argumenter

I eksemplene over ble bil-objektet initialisert ved å bruke ordet «new» etterfulgt av klassens konstruktør.

  • Car myCar = new Car();

Dette var en nyttig forenkling for å lære det grunnleggende ved objektorientert programmering (OOP). Det er imidlertid et ganske alvorlig problem med koden over. Hva hvis vi vil lage to bil-objekter?

  • // Lage to bil-objekter
  • Car myCar1 = new Car;
  • Car myCar2 = new Car;

Dette gir oss det vil ha. Koden produserer to biler, en lagres i variabelen myCar1 og en i myCar2. Men, hvis du studerer klassen Car, vil du se at disse to bilene er identiske: begge to er hvite, begge starter midt på lerretet og begge har hastigheten 1. På norsk betyr koden over:

Lag en ny bil.

Hva vi istedet ønsker å si er :

Lag en ny rød bil, i posisjon (0,10) med hastighet 1.

Slik at vi også kan si:

Lag en ny blå bil, i posisjon (0,100) med en hastighet 2.

​Vi kan gjøre dette ved å plassere argumenter inne i konstruktør-metoden Car().

  • Car myCar = new Car(color(255, 0, 0), 0, 100, 2);

Konstruktøren må skrives om for å få med disse argumentene:

  • ​Car(color tempC, float tempXpos, float tempYpos, float tempXspeed) {
    • c = tempC;
    • xpos = tempXpos;
    • ypos = tempYpos;
    • xspeed = tempXspeed;
  • }

Bruken av konstruktørargumenter kan nok virke litt forvirrende. Denne merkelige koden ser jo helt overflødig ut. «Et argument i konstruktøren for for hver eneste variabel?»

​Forvirrende eller ei, dette en viktig ferdighet å lære, fordi det er nettopp dette som gjør objektorientering så nyttig.

La oss derfor se på hvordan parametre virker i denne sammenhengen. Studer figuren under og følg pilene. 


Picture
https://processing.org/tutorials/objects/

Når objektet «f» av typen «Frog» opprettes i tredje linje, sendes verdien 100 til konstruktøren. Konstruktøren har nemlig en parameter for tungelengde. Variabelen «tempTongueLength» holder tak i verden inntil den er overlevert til den egentlige variabelen «tongueLength» som klassen bruker for å opprette objektet.

Argumenter er  lokale variable som brukes inne i en funksjon. Disse variablene fylles med verdier når det gjøres et kall på funksjonen. I eksemplene her har de kun en hensikt: å initialisere variablene inne i et objekt. Det er disse variablene som teller, bilens faktiske farge, den faktiske posisjonen etc. Konstruktørargumentene er bare midlertidige. De er kun til for å sende en verdi fra der objektet lages videre til objektet selv.

Dette gjør det mulig for oss å lage en mengde ulike objekter med den samme konstruktøren. Du må gjerne bruke ordet «temp» i argumentnavnet du også . Dette er hensiktsmessig for å minne deg selv på hva som egentlig skjer her. Det er midlertidige variabler vi har med å gjøre. Du vil også se programmerere som bruker understrek «_» eller «in» forut for variabelnavnet for å markere at dette er midlertidige variabler. Variabler som brukes for å sende verdier inn i objektet. Du kan kalle disse variablene hva du vil. Du bør imidlertid alltid velge variabelnavn som er beskrivende. Du bør også holde deg til én type navnsetting.

Nå kan vi ta for oss den samme bil-skissen med flere forekomster av objektet Car, hver med unike egenskaper. 

// Eksempel: To Car objektert
Fungerende eksempel her på Daniel Shiffmann sin resssursside for boka «Learning Processing»

  • Car myCar1;
  • Car myCar2; // To objekter!
  • void setup() {
  •      size(200, 200);
  •     // Parametre puttes inne i mellom parentesene når objektet konstrueres
  •     myCar1 = new Car(color(255, 0, 0) , 0, 100, 2);
  •     myCar2 = new Car(color(0, 0, 255), 0, 10, 1);
  • }
  • void draw() {
  •     background(255);
  •     myCar1.drive();
  •     myCar1.display();
  •     myCar2.drive();
  •     myCar2.display();
  • }
  • // Selv om det nå er to objekter trenger vi bare én klasse
  • // Uansett hvor mange kaker vi lager  (med ulik glasur og pynt), så trenger vi bare en kakeform.
  • class Car {
    • color c;
    • float xpos;
    • float ypos;
    • float xspeed;
    • // Konstruktøren defineres med argumenter 
    • Car(color tempC, float tempXpos, float tempYpos, float tempXspeed) {
      • c = tempC;
      • xpos = tempXpos;
      • ypos = tempYpos;
      • xspeed = tempXspeed;
    • }
    • void display() {
      • stroke(0);
      • fill(c);
      • rectMode(CENTER);
      • rect(xpos, ypos, 20, 10);
    • }
    • void drive() {
      • xpos = xpos + xspeed;
      • if (xpos > width) {
        • xpos = 0;
      • }
    • }
  • }

6. Objekter er også datatyper!

​Hvis dette er ditt første møte med objektorientet programmering, er det viktig å ta det steg for steg. I eksemplene over er det bare en klasse og en eller to objekter. Men her er det det egentlig ingen begrensinger. En Processing-skisse kan inneholde så mange klasser og så mange objekter du måtte ønske.

Hvis du for eksempel skal programmere et Space Invaders-spill, vill du kanskje ha tre klasser: en romskipsklasse, en fiendeklasse og en kule-klasse.

I tillegg er det viktig å være klar over at klasser er datatyper på samme måte som heltallsvariabler (int) og desimaltallsvariabler (float). Eneste forskjellen er at klasser ikke er primitive datatyper, de er komplekse: de kan inneholde mange typer data, data som også kan være andre objekter (objekter inne i objekter).

La oss for eksempel anta at du nettopp har fullført programmeringen av en «Gaffel-» og en Kniv»-klasse. Når du fortsetter med en «Bordplassering»-klasse er det naturlig at du inkluderer både et «Gaffel»-objekt og et «Kniv»-objekt i den klassen. Dette er absolutt fornuftig og ganske vanlig i objektorientert programmering.

  • ​class PlaceSetting {
    • Fork fork;
    • Spoon spoon;
    • PlaceSetting() {
      • Fork = new Fork();
      • spoon = new Spoon();
    • }
  • }
  • ​class Fork() {
    • // Kode her
  • }
  • class Spoon() {
    • // Kode her
  • }

​Objekter kan, på samme måte som andre datatyper, også sendes inn som argumenter i en funksjon. I Space Invaders-eksempelet vil vi sannsynligvis ha en funksjon inne i «Fiende»-klassen for å avgjøre om vi har truffet fienden med et skudd.

  • class Enemy {
    • // Kode her
    • void hit(Bullet b) {
      • // Kode for å sjekke om fienden ble truffet av b, et objekt av typen Bullet definert av Bullet-klassen
    • ​}
  • }

Når en primitiv verdi (int, float, etc) sendes inn i en funksjon, lages en kopi av verdien. Objekter er ikke primitive, de er komplekse. Der er det annerledes og litt mer intuitivt: Hvis det gjøres endringer på et objekt etter at det er sendt inn i en funksjon, vil endringene påvirke objektet over alt i programmet der objektet er blitt brukt. Dette er kalles å sende inn en referanse istedet for en kopi. En referanse til det aktuelle objektet sendes inn i funksjonen.


skolekoding.no
Stein Olav Kivle