© 1999-2003, Flemming Koch Jensen
Alle rettigheder forbeholdt
Indre klasser

 

 

Stærkt koblede klasser En stærk kobling mellem klasser er grundlæggende et problem. Vores design skulle gerne have klasser med stærke bindinger, og svage koblinger. Har man to klasser med stærke koblinger, vil man normalt forsøge at samle dem i en klasse - at gøre kobling til binding. Det er ikke altid muligt, og i de situationer kan man i stedet samle klasserne i en package, og give dem mulighed for at arbejde sammen under package scope.
Det bedste af begge verdener Vi skal i dette kapitel se på en anden løsning. Hvis man samler to klasser med stærke koblinger, kan de begrebsmæssige kvaliteter i at have to klasser gå tabt. Vi vil i stedet gerne have det bedste af begge verdener. Dette er til dels muligt med indre klasser: At samle klasserne, men stadig have to. Indre klasser har andre kvaliteter, men deres primære formål er at håndtere stærke koblinger i relation til indkapsling, så vi kan opretholde et sundt design.
   
  I forbindelse med den grundlæggende programmering har vi set hvordan man kan lave nestede sætnings-strukturer, der opbygges ved anvendelse af tuborg-paranteser; hvor sætninger placeres inden i hinanden - F.eks.:
if ( x < 5 ) {
  if ( x < 3 ) {
    y = 3;
  } else {
    y = 2;
  }
}
Her er tuborg-paranteserne syntaktisk overflødige, men er medtaget for at understrege den nestede struktur. Vi har en if-sætning inden i en anden if-sætning, og i den indre if-sætning er der yderligere nestet to sætninger, der assigner forskellige værdier til y.
   
Nestede klasser Indre klasser er klassernes "svar" på nestede sætninger, og indre klasser betegnes derfor også som nestede klasser. Vi vil dog vælge betegnelsen indre klasse, da den lægger sig tættere op ad den mest anvendte engelske betegnelse, i forbindelse med Java: Inner classes.
Man laver indre klasser ved i kildeteksten at placere erklæringen af klasser inden i hinanden (man kan tilsvarende lave indre interface's). Lad os se et helt generelt eksempel:
class A {

  // variable og metoder
  
  class B {
  
    // variable og metoder
    
    class C {
      
      // variable og metoder
      
    }
  }
}
Ydre klasse Ligesom vi betegner en klasse der er erklæret i en anden klasse, som en indre klasse, vil vi betegne den klasse hvori den er erklæret, som dens ydre klasse. I vores eksempel gælder der, at:
 
A er en ydre klasse i forhold til både B og C
B er en indre klasse i forhold til A, og en ydre klasse i forhold til C
C er en indre klasse i forhold til både A og B
Direkte adgang I A optræder erklæringen af B på samme niveau som dens variable og metoder. Scope-mæssigt har B den samme adgang til både variable og metoder i A, som disse. Det betyder at metoderne i B endog kan tilgå de private dele af A!
Flere niveauer Vi ser at nestningen kan fortsætte i flere niveauer. Det betyder at en indre klasse har denne ubegrænsede adgang til både variable og metoder i alle dens ydre klasser. Det er f.eks. også muligt fra C at tilgå de private dele af A.
I dette kapitel vil vi hovedsagelig problematisere adgangen til datakernen, da adgangen til private metoder normalt ikke spiller den store rolle.
 
Når man oversætter en kildetekst, som ovenstående, laves der en class-file for hver klasse - dvs. tre i vores eksempel. Disse filer får navnene:
A.class
A$B.class
A$B$C.class
I navngivningen af class-filerne anvendes $-tegnet som seperator i angivelsen af klasse-stien, indtil den indre klasse.
 
Eksistens-afhængighed Instanser af en indre klasse kan kun eksistere i forbindelse med en instans af en ydre klasse - mere præcist, den ydre klasse, der er umiddelbart udenom den indre klasse. F.eks. kan instanser af C kun eksisterer i sammenhæng med en instans af B.
   
  Hvordan skal man se på instanser af nestede klasser, i forhold til deres tilhørende instans af den ydre klasse? Et første forsøg kunne være følgende:
Figur 1:
Som satellit-objekt
Den indre klasse arbejder ikke op mod et interface Det lille objekt er en instans af en indre klasse (f.eks. B); hvor det store objekt er en instans af en ydre klasse (f.eks. A). Man ser hvorledes metoderne i det lille objekt har direkte adgang til datakernen i det store objekt. Ved første øjekast vil man her se et brud på indkapslingen, og formålet med figuren er da også at problematisere netop dette punkt - den indre klasse arbejder ikke op mod et interface (den ydre klasses interface). Dette er grundlæggende et problem, men det er nok overdrevet i figuren. Den afspejler nemlig ikke det meget nære forhold der er på klasse-niveau mellem den indre og ydre klasse.
   
  Lad os derfor se en anden figur, der placerer instansen af den indre klasse som en del af instansen af den ydre:
Figur 2:
Som del af objekt
  Her er instansen af den indre klasse igen det lille objekt. Når det store objekt laver en instans af den indre klasse, udvider den sit interface med den funktionalitet som det lille objekt tilbyder udadtil. Den røde indramning illustrerer at det samlede "objekt", opretholder indkapslingen - der er ingen udefra der har direkte adgang til datakernerne.
Del-interface Med dette perspektiv indtræder instansen af den indre klasse som et del-interface af det samlede objekt, og det er nu mere naturligt at den har adgang til datakernen i instansen af den ydre klasse, det har de andre metoder i interfacet jo også.
  I figuren anvender vi vores sædvanlige "æble med kerne"-model af et objekt, men illustrationen er måske knap så god, da man ikke rigtig kan se at hele det lille objekts interface retter sig "udad", og naturligvis ikke "dækker" dele af det store objekts interface.
   
  Uanset hvordan man vælger at se på tingene er det spørgsmålet om hvilke konsekvenser "brudet på indkapslingen" reelt får, som har betydning for vores holdning til indre klasser.
  Når vi skal vurdere det, må vi se på hvorfor vi egentlig lægger vægt på indkapsling. En af de vigtigste grunde, er at afgrænse hvorfra datakernen kan tilgås. Det har primært til formål, at lette søgningen efter fejl. Hvis en variabel f.eks. antager "forkerte" værdier, skal vi ved indkapsling kun søge i selve den klasse hvor datakernen er erklæret. Enhver tilgang til den pågældende variabel vil ske fra en af klassens metoder, og en af disse må frivilligt eller ufrivilligt have del i den fejl der opstår. En indre klasse optræder i selve klassens erklæring på linie med dens metoder, og det er derfor tilsvarende afgrænset hvor vi skal søge efter fejl. Vi har dermed stadig denne vigtige kvalitet af indkapsling bevaret.
 
Vi vil senere vende tilbage til hvilke problemer der kan være med at anvende indre klasser. Først vil vi dog se på nogle af de muligheder der er for at anvende indre klasser.

 

2. Adgangs-klasser

Differentieret interface Adgangsklasser, eller fortolkende klasser, giver en særlig adgang til datakernen, som man kan bruge til at differentiere hvilket interface man lader forskellige objekter arbejde med. Et godt eksempel på dette er Iterator Pattern; hvor iteratoren kan opnår den ønskede adgang til aggregatet's datakerne, ved at optræde som indre klasse i denne. Denne anvendelse er vist i kapitlet om Iterator Pattern, og man bør derfor se dette kapitels eksempel.

 

3. Observer-klasser

Listeners Indre klasser bruges ofte i forbindelse med listeners (Observere i Observer Pattern's forstand) i GUI-programmering. Hvis man laver en almindelig klasse til at håndtere events (f.eks. til at være ActionListener på en knap), får man ofte det problem, at listener-objektet har behov for at ændre tilstanden af en eller flere widgets. Det kan i den forbindelse være bekvemt at listener-objektet har adgang til en frames datakerne, og dermed de widgets og andre data, som skal berøres af en event.
Lad os se et eksempel:
Vi vil lave følgende frame
Figur 3:
Ombytter-frame
Når der trykkes på Byt-knappen skal indholdet af de to tekstfelter byttes om.
Vi laver det med en indre klasse, der optræder som ActionListener på denne knap:
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

class OmbytterFrame extends JFrame {
  private JTextField left, right;
  
  public OmbytterFrame( String title ) {
    super( title );
    
    getContentPane().setLayout( new FlowLayout() );
    
    left = new JTextField( 8 );
    getContentPane().add( left );
    
    JButton ombytKnap = new JButton( "Byt" );
    ombytKnap.addActionListener( new Ombytter() );
    getContentPane().add( ombytKnap );
    
    right = new JTextField( 8 );
    getContentPane().add( right );
    
    pack();
    setVisible( true );
  }
  
  private class Ombytter implements ActionListener {
    
    public void actionPerformed( ActionEvent e ) {
      String temp = left.getText();
      left.setText( right.getText() );
      right.setText( temp );
    }
  }
}
Vi ser hvordan den indre klasse udnytter sin adgang til framens datakerne, til at tilgå de to tekstfelter.
I det konkrete eksempel ville det have været bedre at Ombytter-objektet havde fået kendskab til de to tekstfelter direkte - via dens konstruktor - men det illustrerer hvordan en eventhandler (typisk en lille klasse) bekvemt kan laves med en indre klasse.
Blob-risiko Det skal bemærkes at den tredie løsning, at lade selve framen være ActionListener på knappen, ofte vil føre til en actionPerformed-metode, der blobber, efterhånden som den evt. også skal tage sig af events fra andre widgets end eksemplets ene knap.

 

4. Lokale klasser

Lokale i metoder Lokale klasser er klasser der erklæres lokalt i en metode, svarende til lokale variable. De har samme adgang til ydre klasser som den indeholdende metode. De kan kun tilgå lokale variable der er erklæret final. At lokale klasser ikke har adgang til lokale variable (inkl. parametre) er logisk, da instanser af den lokale klasse overlever retunering fra metoden. Efter en sådan returnering ville tilgang til de lokale variable ikke give mening - de eksisterer ikke længere!
Sammensat sætning Lokale klasser kan erklæres i en sammensat sætning. Ligesom man kan erklære lokale variable i f.eks. en løkke, kan man også erklære lokale klasser i diverse konstrolstrukturer, der anvender en sammensat sætning.
Vi vil ikke her give eksempler på det, da teknikken helt svarer til den der anvendes for indre klasser i øvrigt.

 

5. this

Som for instanser af en hvilken som helst anden klasse, refererer this i en instans af en indre klasse til objektet selv, og ikke en instans af en ydre klasse.
  Da man scope-mæssigt befinder sig inde i en ydre klasse, har man dog mulighed for tilgå dennes this. Dette gøre ved at angive den ydre klasses navn. I vores eksempel med klasserne A, B og C, har en instans af C, følgende tre this'er at gøre godt med:
A.this
B.this
this
A.this er en reference til instansen af A, mens B.this refererer til instansen af B.

 

6. Anonyme klasser

  Anonyme klasser, er klasser, hvis erklæring optræder i forbindelse med en instantiering og som derfor ikke behøver noget navn. Det betyder samtidig at en anonym klasse kun kan anvendes ét sted i kildeteksten.
Små engangs-subklasser Når man anvender anonyme klasser, er det normalt fordi man i én enkelt situation ønsker at lave en instans af en subklasse; hvor subklassen har et beskedent omfang. Det er også bydende nødvendigt at subklassen er lille, da en sådan "inline-erklæring" af en klasse nemt får alvorlige konsekvenser for læsbarheden.
  Lad os se et generelt eksempel:
Først har vi den super-klasse: Super, vi ved én enkelt lejlighed ønsker at lave en mindre subklasse af:
class Super {
  
  public String toString() {
    return "Jeg er super";
  }
}
Dernæst har vi klassen A, der i sin getS-metode, ønsker at lave en instans af en subklasse til Super:
class A extends Super {

  public Super getS() {

    Super s = new Super() {
      public String toString() {
        return "Jeg er anonym";
      }
    };
    
    return s;
  }
}
  Bemærk i getS-metoden, at semikolon efter erklæring af klassen ikke syntaktisk har noget med den anonyme klasse at gøre, det er blot afslutningen af en lang assignment-sætning.
Bemærk også starten med:
... new Super() ...
Dette svarer til den sædvanlige instantieringssyntaks og man har derfor også mulighed for at angive parametre til superklassens konstruktor. En anonym klasse kan nemlig ikke selv have nogen konstruktor (Man kan dog bruge initialiseringsblokke, se evt. kapitlet Klasser som objekter vedr. initialiseringsblokke)
 
Endelig har vi testanvendelsen, der ud over at kalde toString-metoden, også udskriver den anonyme klasses "navn":
public class Main {

  public static void main( String[] argv ) {
    
    A a = new A();
    
    Super s = a.getS();
    
    System.out.println( s.getClass().getName() );
    System.out.println( s );
  }
}
A$1
Jeg er anonym
Tildeles genererede navne Vi ser, at den anonyme klasse får tildelt et genereret navn: A$1. Ved oversættelse af ovenstående eksempel, laves der også en class-file for den anonyme klasse, og denne får ligeledes navnet: A$1.class. Navnet A$1 opstår ved at man først tager klasse-stien indtil den anonyme klasse (her kun A), sætter et $ efter, og tilføjer et fortløbende nummer. Hvis vi f.eks. havde haft tre anonyme klasser i getS-metoden, ville disse have fået navnene: A$1, A$2 og A$3.
Uanvendeligt Det skal bemærkes at dette genererede navn på ingen måde kan anvendes i selve programmet. Man vil derfor ikke kunne lave ting, som new A$1(). Anonymiteten er reel!
goto Hver gang jeg ser anonyme klasser kommer jeg til at tænke på goto. Min umiddelbare mening om anonyme klasser er nemlig, at de er rigtig smarte og rigtig ulæselige - altså lidt i stil med, hvad man kan mene om goto. Svarende til goto, kan man derfor anlægge den praksis, at anonyme klasser i nogle situationer kan være betydelig nemmere at anvende end et mere læseligt alternativ, og kun bør anvendes i disse situationer.
Singleton Pattern Implementationen af en singleton er en mulig anvendelse af anonyme klasser - se kapitlet om Singleton Pattern for et eksempel på dette, samt en diskussion af fordele/ulemper.

 

7. Nedarvning og indre klasser

Indre klasser nedarves på samme måde som variable og metoder. I den forbindelse gælder der de sædvanlige regler for betydningen af public, private, protected og package scope.
Bemærk, at man også kan lave subklasser af de klasser man arver fra en superklasse. Dette er dog ikke en egenskab der specielt knytter sig til nedarvning af klasser, da den også gælder for en superklasse. Erklærer man en indre klasse i en klasse, kan man i samme klasse (den ydre) nedarve fra den indre.
Vi har her et generelt eksempel på sidstnævnte situation:
class A {

  // variable og metoder
  
  class B {
  
    // variable og metoder
    
  }
  
  class C extends B {
    
    // variable og metoder
    
  }
}

 

8. Læsbarhed, overskuelighed ...

Indre klasser er ikke harmløse i praksis. Det primære problem kan beskrives med forskellige ord der alle kredser om det samme: læsbarhed, overskuelighed, kompleksitet osv.
Tekstuelt problem Indre klasser giver på udemærket vis mulighed for at udtrykke sammenhænge mellem klasser, men på rent tekstuelt niveau vanskeliggør de læsning af kildeteksten. Den tekstuelle kompleksitet øges altid ved nestning, og en uhæmmet anvendelse af indre klasser vil hurtigt gøre det vanskeligt at vedligeholde programmet.
Blob klasser Ikke alene gør indre klasser det til en større udfordring at læse en kildetekst, de har også en tendens til at gøre klasserne større. Med indkapsling søger vi bla. at begrænse det "område" af kildetekst, hvor en given fejl kan gemme sig. Hvis ens gennemsnitlige klasse-størrelse uden indre klasser er f.eks. 60 linier, og man i hver klasse placerer to indre klasser, vil den gennesnitlige størrelse nu være på 180 linier. Dette er naturligvis et meget firekantet regnestykke, men det illustrerer problemet.
At klasserne vokser i omfang, kræver naturligvis at den kode man placerer i de indre klasser, ellers ikke ville blive placeret i den ydre klasse. Hvis man bruger indre klasser til at opdele funktionaliteten, i en klasse, i mindre dele har man ikke øget klassens omfang. Dette er ofte tilfælde i forbindelse med eventhandlere i GUI-programmering.
Bryder ideal Et design med indre klasser er mere kompliceret end et uden. Vi skal tænke os bedre om når vi begynder at skabe den stærke kobling, der er mellem en indre og en ydre klasse. En løsning med to klasser, der er henvist til at kommunikere med hinandens interface er mere ren og enkel. Når man vælger at bruge indre klasser, har man givet køb på en af idealerne i objektorienteret programmering - at man kun programmerer op mod et interface!
   
Små indre klasser For alle disse problemer er der en ting der hjælp - små indre klasser. Det gælder generelt at indre klasser bør være beskedne i omfang. Det viser sig da også, at de i de fleste tilfælde, blive det helt af sig selv. Man vil kun sjældent få den idé at lave egentlig store indre klasser.
Dokumentation En anden ting der hjælper er dokumentation - både i form af kommentarer i kildeteksten, men også i almindelighed. Problemerne med indre klasser i kildeteksten kommer stærkest til udtryk hvis man er henvist til læse sig til de indre klassers betydning alene vha. kildeteksten. Man bør derfor gøre ekstra meget ud af at dokumentere de indre klasser.

 

9. Indre klasser vs. packages

I forbindelse med nestning er vi normalt vant til at det er en nødvendighed. Når man f.eks. ser tre nestede løkker, søger man ikke at erstattet dem med en løsning der i stedet bruger en række på hinanden følgende løkker - ganske enkelt fordi det ligger i løkkers natur at man normalt ikke kan foretage sådanne ændringer, uden at ændre funktionaliteten. Sådan er det generelt med nestning - vi bruger det fordi det er nødvendigt.
I modsætning til f.eks. nestede løkker, kan indre klasser til dels erstattes med en anden konstruktion, nemlig packages. I hvilken udstrækning kan packages erstatte indre klasser? Hvad er fordele og ulemper?

 

9.1 Hierarkisk sammenhæng mellem klasser

Indre klasse giver os mulighed for at udtrykke en hierarkisk sammenhæng mellem klasser - En indre klasse hører ind under en anden klasse (den ydre), og giver måske kun mening i sammenhæng med denne.
Vil vi udtrykke vores første eksempel
class A {

  // variable og metoder
  
  class B {
  
    // variable og metoder
    
    class C {
      
      // variable og metoder
      
    }
  }
}
med packages, kan det gøres med subpackages. Vi laver en package A, med en subpackage B, med en subpackage C, og vi placerer de tre klasser i deres tilhørende package. Umiddelbart ser det pænt ud, men nu begynder problemerne. Hvordan er tilgangen klasserne imellem?
Her kommer packages til kort. Sammenhængen mellem subpackages er nærmest ikke eksisterende! F.eks. skal vi i klasse B, fordi den befinder sig i subpackage B, importere package A for overhovedet at kunne arbejde med klassen A. Det eneste vi har opnået, er at navngivningen er nestet, som vi kender det fra indre klasser (navnene bliver dog ikke helt de samme. F.eks. A.B.B for klassen B; hvis man befinder sig udenfor package A).
Konklusionen må derfor være: Packages kan ikke tilbyde et alternativ til den hierarkiske sammenhæng, som kan udtrykkes med indre klasser.

 

9.2 Adgang til ydre klassers datakerne

Dropper man at udtrykke den hierarkiske sammenhæng i implementationen, og i stedet placerer alle tre klasse: A, B og C i samme package A, for i det mindste at udtrykke, at disse klasser har et særligt forhold til hinanden, kan vi give klasserne adgang til hinandens datakerne med protected eller package scope (vi vil i det følgende lade protected repræsentere dem begge).
I modsætning til en implementation med indre klasser, har vi nu mulighed for at differentiere mellem hvilke dele af datakernen de "indre klasser" får adgang til, ved at angive dem som protected. Det betyder at packages giver os bedre mulighed for at styre hvilke dele af en datakerne der er adgang til. Til gengæld mister vi noget andet. Alle klasserne er nu "lige", og hvis vi f.eks. gøre dele af B's datakerne protected med tanke på C's adgang, vil nu også A have adgang til disse dele af B.
Konklusionen må derfor være: Packages giver til dels en bedre mulighed for at styre hvilke dele af en datakerne andre klasser har adgang til, men den hierarkiske sammenhæng i denne adgang er borte.

 

9.3 Er packages et alternativ?

Ikke et direkte alternativ Ser man på de to konklusioner ovenfor, er packages ikke direkte et alternativ til indre klasser, men det betyder ikke at packages er ude af billedet. Dette skyldes den måde mange anvender indre klasser på. Hvis man kun bruger en indre klasse til at beskrive en begrebsmæssig sammenhæng mellem klasser; hvor man ikke udnytter den adgang den indre klasse har til den ydre klasses datakerne, bør man ikke anvende en indre klasse, men evt. i stedet bruge én package. Vi kan dog samtidig konkludere, at packages skal have et større semantisk indhold, hvis de skal fremstå som et direkte alternativ til indre klasser.
At bruge indre klasser uden at anvende adgangen til datakernen er som at købe en Diablo Lambourgini, og kun bruge den til at hente morgenbrød om søndagen. Hvis man alligevel ikke vil bruge den direkte adgang, så fjerne den - så er der nemlig taler om en anden konstruktion, der primært retter sig mod modellering af begrebssammenhænge.

 

10. Statiske indre klasser

static Man gør en indre klasser static på samme måde som klasse-variable og klasse-metoder. Statiske indre klasser er på sin vis som almindelige indre klasser. De dele af den ydre klasse's datakerne som de har adgang til, er kun de statiske - andet ville naturligvis ikke give mening.
Det er på sin vis mere nærliggende at bruge statiske indre klasser til at beskrive en hierarkisk sammenhæng mellem klasser, end med almindelige indre klasser. Da instanser af indre klasser ikke har adgang til datakernen i nogen instans af ydre klasser, vil denne egenskab nemlig ikke dominere forholdet mellem klasserne. På den anden side udtrykker statiske indre klasser ikke nogen afhængighed mellem klasserne - f.eks. at en instans af en indre klasse kun giver mening i sammenhæng/kontekst med en instans af en ydre klasse.
En statisk indre klasse er mere uafhængig af sin ydre klasse, end en almindelig indre klasse, og på denne måde er den ikke så kraftfuld som en almindelig indre klasse.