Innkapsling

Innkapsling innebærer at vi skjuler detaljer om hvordan noe er konstruert, og i stedet tilbyr et forenkelt grensesnitt som gjør det mulig å bruke dette «noe» uten å bry oss om hvordan det virker på innsiden. Innkapsling har som formål å gi konstruksjonen høy modularitet.

En konstruksjon er modulær dersom den er bygget opp av «byggeklosser» som uavhengig av hverandre kan

  • forstås,
  • endres,
  • feilsøkes,
  • erstattes og
  • testes.

Hver enkelt enhet i seg selv bør også ha begrenset kompleksitet, være lett å forstå for én person, og ha étt klart og tydelig formål. I objekt-orientert programmering deler vi programmene våre opp i klasser og pakker, og vi ønsker å redusere kompleksiteten i relasjonene mellom disse.

Innkapsling hjelper til ved å skjule detaljer og redusere kompleksitetsnivået en klient til vår kode trenger å forholde seg til. Klienten har som regel egne behov og andre faktorer som øker kompleksiteten i sin kode, så jo mindre de trenger å vite om vår kode, jo bedre. Dette gjelder like sterkt dersom klienten er en fremtidig versjon av oss selv som hvis det er helt andre.


Pakker

Et større java-prosjekt er vanligvis organisert i et hiearki av pakker. Tenk på en pakke som en mappe som inneholder java-konsepter som klasser og grensesnitt, i tillegg til at den kan inneholde andre pakker. Vi bruker pakker for

I toppen av en java-fil deklarerer vi hvilken pakke filen tilhører.

package no.uib.ii.inf101.coursenotes.encapsulation;

public class MyClass {
  // ...
}

Pakkenavn er en streng som består av et eller flere ord, separert med punktum. For eksempel, no.uib.ii.inf101.coursenotes.encapsulation er et gyldig pakkenavn som kunne vært egnet for å samle all kode fra disse kursnotatene du leser akkurat nå. Pakkenavn er

  • vanligvis skrevet med kun små bokstaver, og
  • det er vanlig å bruke omvendt domenenavn som begynnelse av pakkenavnet.

For eksempel er inf101.ii.uib.no domenenavnet til våre nettsider, og det er derfor naturlig å bruke no.uib.ii.inf101 som begynnelse på pakkenavnene våre.

For å gjøre eksempelene våre litt mindre verbose bruker vi ofte kortere pakkenavn på disse nettsidene og i kurset for øvrig. Det går fint så lenge vi ikke planlegger å distribuere koden vår til bruk av andre som et tredjepartsbibliotek.

Tilgangsmodifikatorer

Tilgangsmodifikatorer er spesielle kodeord som brukes for å angi tilgangsnivået til en metode eller feltvariabel. De er:

Modifikator Klasse Pakke Subklasse Alle
public Ja Ja Ja Ja
protected Ja Ja Ja Nei
~ ingen modifikator ~
(aka «pakke-privat»/standard/default)
Ja Ja Nei Nei
private Ja Nei Nei Nei

public Betyr at metoden eller feltvariabelen er tilgjengelig overalt.

For eksempel, la oss si at vi har klassene Person og Main i henholdsvis pakke no.uib.a og no.uib.b. Klassen Person lar sine feltvariabler og metoder være public:

package no.uib.a;

public class Person {
  public String name;
  public int age;

  public Persion(String name, int age) {
    this.name = name;
    this.age = age;
  }

  public boolean isAdult() {
    return age >= 18;
  }
}
package no.uib.b;

import no.uib.a.Person;

public class Main {
  public static void main(String[] args) {
    Person p = new Person("Ola", 15);
    // Fordi feltvariablene er public, kan vi bruke (og endre!) dem her
    // (dette er sannsynligvis ikke ønskelig)
    p.name = "Kari";
    p.age = 20;

    // Fordi metoden isAdult er public, kan vi bruke den her
    boolean foo = p.isAdult();

    System.out.println(foo);
  }
}

I main-metoden lager vi et objekt av klassen Person, og vi kan bruke feltvariabelen age og metoden isAdult(), selv om metoden Main-klassen ligger i en annen pakke og heller ikke er en subklasse av Person.

Metoder og feltvariabeler som er protected er tilgjengelig for alle klasser i samme pakke, og for alle subklasser av klassen som metoden eller feltvariabelen ligger i.

For eksempel, la oss si at vi har klassene Person, Wizard og Main i henholdsvis pakke no.uib.a, no.uib.b og no.uib.c. Klassen Person lar sin feltvariabel age være protected. Dette innebærer at den kan brukes av klassen Wizard som er en subklasse av Person, men ikke i klassen Main som ikke er en subklasse av Person:

package no.uib.a;

public class Person {
  public String name;
  protected int age;

  public Persion(String name, int age) {
    this.name = name;
    this.age = age;
  }

  public int getAge() {
    return age;
  }
}
package no.uib.b;

import no.uib.a.Person;

public class Wizard extends Person {
  /** Give one year of this wizard's lifetime to the person */
  public void giveAYearTo(Person p){
    this.age -= 1;
    p.age += 1;
  }
}
package no.uib.c;

import no.uib.a.Person;
import no.uib.b.Wizard;

public class Main {
  public static void main(String[] args) {
    Person kari = new Person("Kari", 20);
    Wizard harry = new Wizard("Harry", 20);
    // Vi kan IKKE gjøre dette, siden age er protected:
    // harry.age += 1;
    // kari.age -= 1;

    // Men vi kan gjøre dette, siden giveAYearTo er public:
    harry.giveAYearTo(kari);

    System.out.println("Harry is now " + harry.getAge() + " years old");
    System.out.println("Kari is now " + kari.getAge() + " years old");
  }
}

Dersom en metode eller feltvariabel ikke har angitt hverken public, protected eller private, innebærer dette såkalt «pakke-privat» innkapsling (også kjent som standard/default innkapsling). Det betyr at metoden eller variabelen er tilgjengelig innefor samme pakke, men ikke utenfor.

For eksempel, dersom vi har klassene Person og PersonFactory i pakke no.uib.a, mens vi har klassen Main i pakken no.uib.b, vil en pakke-privat konstruktør til Person være tilgjengelig i PersonFactory, men ikke i Main:

package no.uib.a;

public class Person {
  private String name;

  // Se: konstruktøren er pakke-privat
  Person(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }
}
package no.uib.a;

/**
 * A factory object can only create the same person once. This could be
 * useful for preventing the existence of two equal but different objects.
 */
public class PersonFactory {
  private Map<String, Person> persons = new HashMap<>();

  public Person getOrCreatePerson(String name) {
    if (persons.containsKey(name)) {
      return persons.get(name);
    }
    else {
      // Siden PersonFactory er i samme pakke som Person,
      // er det OK at vi kaller Person sin konstruktør her:
      Person p = new Person(name);
      persons.put(name, p);
      return p;
    }
  }
}
package no.uib.b;

public class Main {
  public static void main(String[] args) {
    // Dette vil IKKE fungere, siden Person-konstruktøren er pakke-privat,
    // samtidig som Main ikke er i samme pakke som Person:
    // Person ola1 = new Person("Ola");
    // Person ola2 = new Person("Ola");
    // System.out.println(ola1 == ola2); // false

    // Dette vil derimot fungere, siden getOrCreatePerson er public:
    PersonFactory personFactory = new PersonFactory();
    Person kari1 = personFactory.getOrCreatePerson("Kari");
    Person kari2 = personFactory.getOrCreatePerson("Kari");
    System.out.println(kari1 == kari2); // true
  }
}

En variabel eller metode som er private er kun tilgjengelig i den samme klassen hvor den ble definert. Dette er den strengeste innkapslingen, og bør alltid være standardvalget for variabler og metoder med mindre det er en god grunn til å velge noe annet.

Dersom vi har to klasser Person og Main i samme pakke, kan vi ikke bruke private feltvariabeler fra Person i Main:

class Person {
  private String name;

  Person(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }
}

public class Main {
  public static void main(String[] args) {
    Person p = new Person("Ola");
    // Dette vil IKKE fungere, siden name er private:
    // p.name = "Kari";

    // Dette vil derimot fungere, siden getName er public:
    System.out.println(p.getName());
  }
}

Tommelfingerregeler for tilgangsmodifikatorer:

Dersom feltvariablene dine ikke er private, blir de en del av informasjonen klienter til koden din må forholde seg til. Dette kan fungere fint helt til du ønsker å endre hvordan du implementerer klassen din; da vil du ødelegge klientene sin kode om du gjør endringer med variabelen.

Man kan alltid oppnå det samme som å ha en eksponert felvariabel ved å la feltvariabelen i stedet være private og heller eksponere en getter og en setter. En getter er en metode som returnerer verdien til en feltvariabel, mens en setter er en metode som endrer verdien til en feltvariabel. For eksempel:

// Denne klassen har en public feltvariabel
class Person {
    public String name;
}

// Denne klassen har gettere og settere for feltvariabelen
// i stedet, mens feltvariabelen er private. Dette gir bedre
// innkapsling og fleksibilitet for fremtidige endringer.
class Person {
  private String name;

  public String getName() {
    return this.name;
  }

  public void setName(String name) {
    this.name = name;
  }
}

Dersom du bruker gettere og settere i stedet for å eksponere feltvariabler, øker du innkapslingen av implementasjonen din, og gir deg selv mulighet til å maskere endringer i fremtiden.

Gettere og settere har også andre fordeler som man kan velge å benytte seg av.

  • I en setter kan du validere at feltvariabelen får en gyldig tilstand (for eksempel kan vi sjekke at verdien ikke er null).
  • I en setter kan du konvertere input til et foretrukket format (for eksempel endre en streng til kun små bokstaver).
  • I en getter kan du konvertere svaret til et foretrukket format (for eksempel endre en streng til kun små bokstaver eller noe helt annet).
  • Du kan ha ulike tilgangsnivåer på getter og setter. For eksempel kan getter være public mens setter er pakke-privat.
  • Du kan la klassen implementere ulike grensesnitt, hvor for eksempel et av grensesnittene kun har gettere. Les mer om dette i avsnittet om restriktive typer og innkapsling med grensesnitt

Ulempene med gettere og settere er at de tar opp plass i klassen vår, og at syntaksen blir litt mer verbos. Ikke veldig alvorlige ulemper altså, men plass og lesbarhet har likevel verdi. Derfor kan det være greit å eksponere variabler i sjeldne tilfeller. Da bør klassen feltvariablene tilhører enten

  • være en enkel «hjelpeklasse» som ikke er public og som kun blir brukt svært lokalt – slik at det er naturlig å endre klientkoden samtidig som man endrer hjelpeklassen; eller

  • man har tenkt svært nøye gjennom saken i samråd med en erfaren utvikler, og funnet ut at klassen aldri i fremtiden noensinne vil ha behov for endre å seg.

I tillegg til å kunne bruke tilgangsmodifikatorer på metoder og feltvariabler, kan vi også bruke dem på klasser, grensesnitt og lignende. Å ha en klasse som ikke er public vil skjule hele klassen og typen den definerer fra dem som ikke har høyt nok tilgangsnivå.

Merk at det kun er mulig å ha én public klasse per fil, og at denne klassen må ha samme navn som filen. Det er uansett god stil at man kun har én klasse per fil, samme om den er public eller ikke (unntak for nøstede klasser).

Restriktive typer og innkapsling med grensesnitt

Det er mulig å styrke innapslingen ytterligere ved å benytte restriktive typer. Dette innebærer at klienten benytter en variabeltype som kun inneholder et lite utvalg av metodene som egentlig er tilgjengelige på objekter i den aktuelle klassen. Denne restriktive typen er typisk definert av et grensesnitt som er implementert av klassen direkte eller indirekte.

For eksempel, la oss si at vi har en klasse Person som har public metoder getName og setName. Noen klienter av Person -objekter er kun interessert i getName. Vi kan da la Person implementere et «restriktivt grensesnitt» ReadOnlyPerson som kun inneholder getName, og la de nevnte klientene benytte ReadOnlyPerson-variabler i stedet for Person-variabler.

public interface ReadOnlyPerson {
  String getName();
}
public class Person implements ReadOnlyPerson {
  private String name;

  public Person(String name) {
    this.name = name;
  }

  @Override
  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}
public class Main {
  public static void main(String[] args) {
    // Klient benytter restriktivt grensesnitt som type,
    // dette øker innkapslingen og dermed modulariteten:
    ReadOnlyPerson p = new Person("Ola");

    // Dette vil IKKE fungere, siden ReadOnlyPerson ikke definerer setName:
    // p.setName("Kari");

    // Dette VIL fungere, siden getName er definert i ReadOnlyPerson:
    System.out.println(p.getName());
  }
}

Mønseteret med å bruke restriktive grensesnitt kan som eksempelet over demonstrerer benyttes for å lage «read-only» -type for en gitt klasse. Da oppnår man en slags «kvasi-uforanderlighet» for objektene i den aktuelle klassen, noe som reduserer relasjons-kompleksiteten mellom klassene betydelig.

Gode råd

For implementøren (deg som skriver en klasse):

For klienten (deg som bruker objekter i andre klasser):