Singleton Pattern Opgaver |
|
|
|
Forudsætninger: At man har læst kapitlet: Klasser som objekter, eller på anden måde har opnået kendskab til static. Det er en forudsætning for at forstå afsnittet om implementation med anonyme klasser, at man har kendskab til indre klasser — evt. ved at have læst kapitlet: Indre klasser. |
1. Ønsker/interesser | |
Kun at tillade netop én instans af en given klasse. | |
At der er nem adgang til en sådan instans. | |
2. Eksempel | |
Hvis man lader et objekt repræsentere et debug-vindue, og man ønsker at dette objekt samtidig skal styre output til vinduet, er det naturligvis hensigtsmæssigt, at der kun findes ét sådant objekt - og dermed kun ét debug-vindue. Hvis alle klienter, som har behov for at sende debug-beskeder til programmøren laver deres eget vindue skal han aflæse forskellige vinduer for at skabe sig et overblik over programudførelsen. Vi ønsker derfor ikke at klienterne selv skal instantiere debug-vinduet. | |
Ikke alene vil det være en fordel, at der kun findes ét sådant vindue, men det vil ligeledes være en fordel, at det er globalt tilgængeligt - at enhver klient har nem adgang til det. | |
3. Løsning | |
Løsningen baserer sig på klasse-variable og -metoder. Lad os kalde den klasse vi kun vil tillade én instans af for Singleton. Hvis vi for eksemplets skyld antager, at denne klasse kun har en enkelt instans-metode råb, der udskriver en tekst med ene store bogstaver, vil klassen få følgende implementation: | |
Singleton.java
public class Singleton { /* * Klasse-variable og -metoder */ private static Singleton inst=null; public static Singleton instance() { if ( inst == null ) inst = new Singleton(); return inst; } /* * Instans-variable og -metoder */ private Singleton() { } public void råb( String s ) { System.out.println( s.toUpperCase() ); } } Main.java
public class Main { public static void main( String[] args ) { Singleton vorSingleton = Singleton.instance(); vorSingleton.råb( "Hallo, er der nogen?" ); } } HALLO, ER DER NOGEN? |
|
Der er to centrale elementer i løsningen: Konstruktoren og instance-metoden. | |
3.1 Konstruktoren | |
Konstruktoren synes ved første øjekast at være ligegyldig, endog overflødig. Vi har her anvendt en default-konstruktor, da eksemplet er meget enkelt. Hvis det eneste man har i en klasse, er en tom default-konstruktor, behøver man normalt ikke lave én - det gør Java selv. Der er dog en speciel ting ved vores konstruktor, der gør at vi ikke kan bruge Java's default-konstruktor — vores er private! | |
Når noget er private kan det, som bekendt, ikke tilgås udenfor klassen. Vores private konstruktor kan kun tilgås indefra klassen, og dermed er det kun klassen der kan lave nye instanser af Singleton, da instantiering fordrer at man kan tilgå den konstruktor man anvender. | |
At gøre konstruktoren private er vores måde at forhindre andre i at lave instanser af Singleton, men hvordan foregår instantieringen så? | |
3.2 Instans-metoden | |
Det er her instance-metoden kommer ind i billedet. Det er instance-metoden der laver instancer af klassen — nærmere bestemt én. | |
Metoden bruger en klasse-variabel til, både at huske, og holde fast i den ene instans den laver. instance-metoden består i hovedsagen af en if-sætning, der checker om vi allerede har lavet en instans ved at sammenligne inst med null. | |
4. Interaktionsdiagram | |
Interaktionen i eksemplet kan illustres med følgende sekvensdiagram: | |
5. Lazy instantiering | |
Lille plus | Sådan som vi har valgt at implementere instance-metoden, giver Singleton Pattern os et ekstra lille plus! |
Hvornår? | Hvornår instantieres Singleton'en? Det gør den første gang instance-metoden kaldes. Hvornår kaldes instance-metoden? Det gør den i en af to situationer: |
|
|
Hvis vi holder os til sidstnævnte anvendelse, vil Singleton'en ikke blive instantieret før vi skal bruge den. Vi har i princippet hele tiden adgang til den, men det er først når vi reelt bruger den - ved at kalde instance-metoden - at instantieringen sker. | |
Doven | At instantieringen ikke sker før det er strengt nødvendigt, kaldes lazy instantiering (dk.: doven instantiering). Fordelen ved lazy instantiering er at vi ikke skal "betale" de omkostninger, der er forbundet med at instansen eksisterer før strengt nødvendigt. Sådanne omkostninger kunne være diverse resourcer — så som lagerplads, netværksforbindelser osv. |
Sjældent | Om lazy instantiering er en gevinst, beror på den konkrete situation - er der nogen resourcer som man sparer på? Det er derfor lazy instantiering ovenfor generelt betegnes som et lille plus. Det er relativ sjældent at det betyder noget. |
6. Hvem "sletter" en Singleton? | |
Normalt vil garbage collectoren fjerne objekter, som ingen længere refererer til. Det vil den naturligvis også for en Singleton's vedkommende, men der er et problem. Singleton klassen opretholder selv en reference til instancen. Derfor vil den reference-løse situation aldrig opstå! | |
Man kunne evt. forestille sig at Singleton-klassen så kunne sætte sin reference til null, og på den måde give Singleton'en fri, så den kunne slettes. Men hvad så hvis andre stadig har en reference til den? | |
Hvis en eller flere klienter fortsat har en reference til instansen og andre klienter kalder instance-metoden, går det galt! instance-metoden vil tro, at der ikke findes nogen instans af Singleton — déns reference er jo null, og den vil lave en ny. Resultatet er nu to instanser af Singleton, og vores væsentligste designmål er forfejlet. | |
Jeg er ikke bekendt med nogen løsning på dette problem, men det er et af ynglingsemnerne når man diskuterer Singleton Pattern. | |
7. Varianter | |
Der findes ikke egentlige varianter af Singleton Pattern. Man kan dog variere om antallet af instanser skal være netop én, i stedet for flere. Hvis en Singleton f.eks. ønsker maksimalt tre instanser, kan den opretholde et statisk array med tre referencer, og blive ved med at lave nye instanser så længe nogen af indgangene stadig er null. Man kunne evt. forestille sig dette brugt i forbindelse med en objekt-pool. | |
8. Implementation med anonyme klasser | |
Med en anonym klasse kan man sikre at instantiering kun foregår ét sted. | |
Vi kunne f.eks. lave en instans med: | |
inst = new Singleton() { public void råb( String s ) { System.out.println( s.toUpperCase() ); } }; |
|
Dette forhindrer naturligvis ikke, at der laves flere instanser af en anonym klasse, da den sætning der foretager instantieringen kan udføres flere gange - f.eks. med en løkke, eller ved at den optræder i en fabriks-metode. At instantieringen kun kan ske ét sted, gør det dog nemmere at sikre, at den også kun sker én gang. | |
Hvis vi kombinerer løsningen med en statisk metode til at styre adgangen til vores singleton, og anvender en anonym klasse i forbindelse med instantieringen, får vi: | |
Singleton.java
public abstract class Singleton { /* * Klasse-variable og -metoder */ private static Singleton inst=null; public static Singleton instance() { if ( inst == null ) inst = new Singleton() { public void råb( String s ) { System.out.println( s.toUpperCase() ); } }; return inst; } /* * Instans-metoder */ public abstract void råb( String s ); } |
|
Det er en løsning, der i opbygning med statiske dele, ligner den løsning vi tidligere har set. Der er dog visse forskelle? | |
Først og fremmest har vi ikke længere en privat konstruktor - vi har i det hele taget ikke nogen konstruktor. Singleton-klassen kan derfor gøres abstrakt, da den nu kun optræder som superklasse til den anonyme klasse vi instantierer. Før sikrede den private konstruktor, at klienter ikke kunne lave deres egne instanser af klasse, nu er det den abstrakte klasse og information hiding (den anonyme klasse kan ikke ses udenfor klassen) der forhindrer det. | |
Som man kan se, gør en løsning med indre klasser ikke det store fra eller til, så det valg man træffer må bero på mindre nuance-forskelle. Personlig foretrækker jeg løsningen uden anonyme klasser, da den anonyme klasse hurtigt gør kildeteksten uoverskuelig, og at en løsning med indre klasser ikke gør det muligt at nedarve fra én singleton til en anden. | |
9. Er Singleton et Anti-Pattern? | |
Anti-Pattern | Mens et Design Pattern er en anbefalelsesværdig måde at løse et designmæssigt problem på (hvis løsningen ellers passer til problemet), er et Anti-Pattern en tilsvarende dårlig måde at løse et problem. |
Det forlyder at forfatterne bag bogen "Design Patterns: Elements of Reusable Object-Oriented Software", der startede hele design pattern bølgen, siden har fortrudt at de havde taget Singleton Pattern med i bogen. Det er heller ikke ualmindeligt at man i visse kredse støder på en negativ holdning til Singleton — men det skyldes ofte at man har set Singletons, der ikke burde have været Singletons! | |
Problemet er test | Problemet med Singletons er ikke så meget den globale adgang som nogle har set sig gale på, men at de tilstandsmæssigt er vanskelige i et test-scenarie. Unit tests har generelt problemer med tilstand der kan overleve fra test til test, og i den forbindelse er Singletons netop vanskelige. Vi har allerede konstateret at de reelt ikke kan "slettes" igen; hvilket ellers kunne være løsningen mellem hver unit test. |
Hver gang man får lyst til at løse et designmæssigt problem med en Singleton, skal man grundigt overveje om man har brug for den. | |
Vi skal i næste afsnit se en løsning jeg selv bruger — en løsning der ikke har de nævnte testproblemer, da det her er muligt at slette Singletons — dog kun noget, man gør i forbindelse med test! | |
10. ServiceLocator | |
Én Singleton | Hvad gør man hvis man ønsker at have en række Singletons i ens applikation, men gerne vil undgå klasser med de samme stykker kode, der arbejder med statiske metoder og variable? Samtidig vil man gerne kunne "slette" dem, når man laver tests? Løsningen kan være at anvende én central Singleton, der holder styr på gruppen af klasser; hvoraf man kun vil have netop en instans. |
Når jeg laver en større applikation (e.g. Programming Studio), anvender jeg en klasse: ServiceLocator, der holder styr på mine Singletons. I dette afsnit skal vi se nærmere på hvordan denne klasse er implementeret, og hvordan den kan anvendes. | |
Services | Jeg kalder de enkelte Singletons for services. Det er en generisk betegnelse der kan dække over meget. Alle klasser der optræder som Singletons i den samlende Singleton skal implementere følgende interface: |
Service.java
public interface Service { public void destruct(); } |
|
Interfacet kræver af de enkelte services, at de implementerer en metode der "destruerer" dem. | |
Lad os se en simpel service, der implementerer dette interface: | |
SomeService.java
public class SomeService implements Service { private SomeService() { System.out.println( "SomeService.SomeService()" ); } public void printText( String text ) { System.out.println( text ); } @Override public void destruct() { // nothing to cleanup } } |
|
Denne service har ikke behov for at "rydde op" i forbindelse med at den bliver "slettet", så destruct-metoden gør ikke noget. | |
Vi bemærker, at konstruktoren i god "Singleton-stil" er private. Vi har indsat en udskrift i konstruktoren, for senere at kunne konstatere om den bliver udført mere end én gang. | |
Selve den service som SomeService tilbyder, er uhyre simpel, idet den blot giver os mulighed for at udskrive en tekst. | |
Vi vil nu se selve ServiceLocator-klassen: | |
ServiceLocator.java
import java.lang.reflect.Constructor; import java.util.HashMap; public class ServiceLocator { private HashMap<Class<?>, Service> services; private ServiceLocator() { services = new HashMap<Class<?>, Service>(); } public void register( Service service ) { services.put( service.getClass(), service ); } @SuppressWarnings( "unchecked" ) public <T extends Service> T get( Class<T> serviceClass ) { try { if ( !services.containsKey( serviceClass ) ) { Constructor<T> defaultConstructor = serviceClass.getDeclaredConstructor(); defaultConstructor.setAccessible( true ); Service service = (Service) defaultConstructor.newInstance(); services.put( serviceClass, service ); } return (T) services.get( serviceClass ); } catch ( NoSuchMethodException e ) { // missing default constructor System.out.println( "Missing default constructor!" ); } catch ( Exception e ) { // InvocationTargetException // IllegalAccessException // InstantiationException } return (T) null; } public <T extends Service> void destruct( Class<T> serviceClass ) { if ( services.containsKey( serviceClass ) ) { Service service = services.remove( serviceClass ); service.destruct(); } } public void destructAll() { for ( Service service : services.values() ) service.destruct(); services.clear(); } /* * singleton */ private static ServiceLocator instance=null; public static ServiceLocator i() { if ( instance == null ) instance = new ServiceLocator(); return instance; } } |
|
Generisk og reflections | Klassen indeholder en del generisk programmering, samt anvendelse af reflections. Det er ikke formålet her at give en introduktion til disse emner, så vi vil kun berøre dette ganske overfladisk, og i stedet fokusere på funktionaliteten. |
Der er to måder hvorpå en service kan blive håndteret af ServiceLocator'en. Den mest brugte er, at man forespørger ServiceLocator'en efter en instans af en klasse, ved at kalde get-metoden, med den pågældende klasse som parameter. Herefter vil ServiceLocator'en i bedste "Singleton-stil" enten give os en allerede eksisterende instans eller lave en ny instans til os. | |
I mere sjældne tilfælde sker det ved at servicen bliver meldt til, vha. register-metoden. Den eneste situation, hvor jeg hidtil har brugt dette, er når jeg lader applikations vinduet optræde som en service. Her kan instantieringen ikke overlades til ServiceLocator'en. | |
Kun "slette" i testsammehæng | I test-sammenhæng kan man anvende destructAll-metoden til at "slette" alle services før man udfører en unit test. destruct-metoden kan "slette" en enkelt service, men det er tvivlsomt om man vil finde anledning til at bruge den i test-sammenhæng, og den er kun medtaget for fuldstændighedens skyld. Bemærk, at disse metoder som tidligere nævnt ikke giver meget mening udenfor et test-scenarie, af grunde som allerede er blevet berørt ovenfor i afsnittet om hvem der "sletter" en Singleton. |
Lad os se en testanvendelse, der demonstrerer gentagne kald af get-metoden, og at der kunne er tale om en og samme instans af SomeService. | |
Main.java
public class Main { public static void main( String[] args ) { ServiceLocator.i().get( SomeService.class ).printText( "Hello World" ); ServiceLocator.i().get( SomeService.class ).printText( "Hello again!" ); } } SomeService.SomeService() Hello World Hello again! |
|
Man bemærker, at SomeService's konstruktor kun kører én gang. | |
10. Relationer til andre mønstre | |
I relation til andre patterns er det primært forskellige former for fabriks-mønstre; hvor man ønsker at begrænse antallet af fabrikker til én (se evt. Abstract Factory Pattern og Builder Pattern (disse er ikke skrevet endnu)) |