12. Objektno programiranje

12.1. Objekti

Objekti so podobni zapisom, le da imajo atribute in metode. Metode se lahko sklicujejo na atribute, kakor tudi na druge metode (z uporabo this ali self).

Torej so objekti neke vrste rekurzivni zapisi. Rekurzivni, ker se lahko objekt skliče sam nase.

12.1.1. Objekti v OCamlu

V OCamlu in nekaterih drugih lahko objekte naredimo nesporedno

object (this)val x = ...
   val y = ...
   method f = ...
   method g = ...
end

Funkcionalnost objekta (njegove metode) opišemo s tipom zapisa. S podtipi lahko objekte vmeščamo v hierarhijo.

Več primerov bomo obdelali na vajah, poglejmo primere:

(* Objekt lahko definiramo neposredno tako, da podamo njegove
   atribute in metode. *)
let p =
  object
    (* atributi, v OCamlu se imenujejo "instance variable" *)
    val mutable x = 10
    val mutable y = 20

    (* metode *)
    method get_x = x

    method get_y = y

    method move dx dy =
      x <- x + dx ;
      y <- y + dy
  end

(* Če želimo klicati eno eno metodo iz druge, moramo podati tudi ime,
   s katerim se objekt sklicuje sam nase. *)
let q =
  object (this) (* namesto "this" bi lahko uporabili kako drugo ime *)
    val mutable x = 10
    val mutable y = 20

    method get_x = x

    method get_y = y

    method move dx dy =
      x <- this#get_x + dx ;  (* tu se sklicujemo na metodo get_x *)
      y <- y + dy
  end

(* Funkcija, ki sprejme začetni vrednosti atributov x in y ter
   vrne ustrezno inicializiran objekt. To je zelo podobno konstruktorjem
   v Javi, a je le funkcija. *)
let make_point x0 y0 =
  object
    val mutable x = x0
    val mutable y = y0

    method get_x = x

    method get_y = y

    method move dx dy =
      x <- x + dx ;
      y <- y + dy
  end

(* S tako funkcijo naredimo nov objekt. *)
let r = make_point 1 3

(* OCaml pozna tudi razrede, ki jih definiramo, kot da bi bili funkcije, ki
   vračajo objekte (vendar to niso). *)
class point x0 y0 =
  object
    val mutable x = x0
    val mutable y = y0

    method get_x = x

    method get_y = y

    method move dx dy =
      x <- x + dx ;
      y <- y + dy
  end

(* Z "new" naredimo nov objekt danega razreda. *)
let s = new point 1 3

(* Primerjajmo tipa objektov r in s:

   r : < get_x : int; get_y : int; move : int -> int -> unit >
   s : point

   Tip r je podoben tipu zapisa, saj našteje metode (ne pa atributov, ki so
   popolnoma skriti). Tip s je kar ime razreda, ki mu s pripada.

   Ker razred point definira natanko iste metode, kot tip r, sta r in s
   pravzaprav istega tipa, "point" je samo okrajšava.
*)

(* Poglejmo še, kako deluje dedovanje in podrazredi. *)

type color = { r : float; g : float ; b : float }

let pink = { r = 1.0; g = 0.75; b = 0.75 }

(* Razred color_point podeduje razred point. *)
class color_point x0 y0 (c0 : color) =
object
  inherit point x0 y0 (* dedovanje *)

  val color = c0

  method get_color = color
end

Poglej Tudi

OCaml ima zelo bogat nabor konstrukcij za objektno programiranje, več o njih preberite v dokumentaciji Objects in OCaml.

12.2. Objektno programiranje z razredi

Mnogi objektni jeziki pa poznajo mehanizem razredov (Java, C++, C#). Pogljemo še enkrat primer točke:

let p =
  object
    val mutable x = 10
    val mutable y = 20

    method get_x = x

    method get_y = y

    method move dx dy =
      x <- x + dx ;
      y <- y + dy
  end

Zgornja koda naredi le eno točko. Če želimo narediti več točk, lahko napišemo funkcijo, ki sprejme začetno pozicijo in vrne objekt. Lahko pa defiramo razred, na primer v Javi:

public class Point {
    private int x ;
    private int y ;

    public Point(int x0, int y0) {
        this.x = x0;
        this.y = y0;
    }

    public int get_x() { return this.x; }

    public int get_y() { return this.y; }

    public void move(int dx, int dy) {
        this.x += dx;
        this.y += dy;
    }
}

Sedaj tvorimo nove točke s konstruktorjem: new Point(10, 20).

Tudi v OCamlu lahko definiramo razred:

class point x0 y0 =
  object
    val mutable x = x0
    val mutable y = y0

    method get_x = x

    method get_y = y

    method move dx dy =
      x <- x + dx ;
      y <- y + dy
  end

In tvorimo novo točko s konstruktorjem: new point 10 20.

Razredi niso samo bližnjica za konstruktorje, ampak omogočajo še mnoge druge mehanizme:

  • enkapsulacija

  • konstruktor

  • dedovanje

  • vmesniki

  • prekrivanje metod

  • preobteževanje metod

  • generične metode in razredi

  • abstrakne metode in razredi

Vsi ti mehanizmi lahko delo z razedi naredijo precej zapleteno. Verjetno je eden od razlogov za kompleksnost ta, da programski jeziki kot so Java, C# in C++ vse mehanizme za organizacijo kode izražajo s pomočjo razredov. Tako Java ne pozna modulov in funktorjev, algebrajskih podatkovnih tipv, ali parametričnega polimorfizma – vse to nadomešča z razredi.

Naredimo kratek pregled osnovnih mehanizmov objektnega programiranja z razredi in jih primerjajmo s koncepti, ki smo jih spoznali do sedaj.

12.2.1. Enakpsulacija

Enakpsulacija je skrivanje stanja objekta, se pravi, da naredimo atribute (lahko pa tudi metode) nedostopne zunaj razreda. V Javi to naredimo z določilom private.

V OCamlu so atributi vedno privatni. Metodo naredimo privatno z določilom private.

Pojem enkapsulacija včasih zajema tudi idejo, da objekt poleg stanja (atributov) s seboj nosi tudi metode za delo z njimi.

Skrivanje definicij lahko implementiramo tudi z lokalnimi definicijami in signaturami, ki zakrijejo implementacijo.

12.2.2. Konstruktor

Konstruktor je del kode, ki nastavi začetne vrednosti atributov objekta. Običajno konstruktor sprejme argumente, se pravi, da je to funkcija.

V Javi je ime konstruktorja enako imenu razreda, prav tako v C++ in OCamlu.

12.2.3. Dedovanje

En razred lahko deduje od drugega:

public class A extends B { ... }

To pomeni, da ima A vse, kar ima B (torej atribute in metode).

OCaml dopušča večkratno dedovanje, ko en razred deduje hkrati od večih nadrazredov.

12.2.4. Vmesniki

Vmesniki predpisujejo funkcionalnost, ki jo mora imeti objekt. V Javi vmesnik definiramo z

interface I { ... }

in od razreda zahtevamo, da zadosti vmesniku I (lahko tudi večim)

class C implements I { ... }

O vmesnikih smo že govorili.

12.2.5. Prekrivanje

Razred lahko nekatere od podedovanih metod prekrije z lastnimi definicijami.

Tu se pojavi vprašanje, kako dostopati do prekritih metod, saj jih včasih potrebujemo. V Javi to naredimo s super.imeMetode(⋯). Podobno vprašnje se pojavi pri konstruktorjih: kako iz konstruktorja pokličemo konstruktor iz nadrazreda?

12.2.6. Preobteževanje

Če imamo več metod z istim imenom f, pravimo, da smo f preobtežili (angl. overload). Metode se morajo med seboj razlikovati po števili argumentov ali njihovih tipih, sicer iz klice metode ne moremo razbrati, katero različico želimo.

OCaml ne pozna preobteževanja. Lahko pa uporabimo module in jih odpremo lokalno z let open M in ali M.(⋯).

12.2.7. Generične metode in razredi

Generične metode in razredi so parametrizirani z razredi ali vmesniki, takole:

public class A<B> {
  ⋯
}

Definicija razreda A je parametrizirana z razredom B.

V C++ v ta namen uporabljamo predloge (angl. templates).

V OCaml poznamo dva mehanizma za generično programiranje: parametrični polimorfizem in module.

12.2.8. Abstraktne metode in razredi

Abstraktne (v C++ se imenujejo virtualne) so metode, ki jih ne implementiramo, ampak samo deklariramo. Se pravi, povemo njihov tip ne pa tudi implementacije.

Če razred vsebuje abstraktno metodo, mora biti tudi sam deklariran kot abstrakten. Objektov abstraktnega razreda ne moremo ustvarjati z new, saj so neke vrste „nedokončani“ razredi.

Abstraktni razred je neke vrste mešanica implementacije in speficikacije (specifikacija sestoji iz abstraktnih metod).

Razredi v OCamlu so lahko abstraktni, pravi se jim virtualni razredi.