Kreative designmønstre i Core Java

1. Introduksjon

Designmønstre er vanlige mønstre som vi bruker når vi skriver programvaren vår. De representerer etablerte beste praksis utviklet over tid. Disse kan da hjelpe oss med å sikre at koden vår er godt designet og godt bygget.

Creational Patterns er designmønstre som fokuserer på hvordan vi får forekomster av gjenstander. Vanligvis betyr dette hvordan vi konstruerer nye forekomster av en klasse, men i noen tilfeller betyr det å skaffe en allerede konstruert forekomst klar til bruk.

I denne artikkelen skal vi se på noen vanlige mønstre for skapelsesdesign. Vi får se hvordan de ser ut og hvor vi kan finne dem i JVM eller andre kjernebiblioteker.

2. Fabrikkmetode

Fabrikkmetodemønsteret er en måte for oss å skille ut konstruksjonen av en forekomst fra klassen vi konstruerer. Dette er slik at vi kan abstrakte bort den eksakte typen, slik at klientkoden vår i stedet kan fungere når det gjelder grensesnitt eller abstrakte klasser:

klasse SomeImplementation implementerer SomeInterface {// ...} 
public class SomeInterfaceFactory {public SomeInterface newInstance () {return new SomeImplementation (); }}

Her trenger klientkoden vår aldri å vite om Noe implementering, og i stedet fungerer det mht Noen grensesnitt. Enda mer enn dette, skjønt, vi kan endre typen som returneres fra fabrikken, og klientkoden trenger ikke å endres. Dette kan til og med omfatte dynamisk valg av type ved kjøretid.

2.1. Eksempler i JVM

Muligens de mest kjente eksemplene på dette mønsteret JVM er samlingsbyggingsmetodene på Samlinger klasse, som singleton (), singletonList (), og singletonMap (). Disse returnerer alle forekomster av riktig samling - Sett, Liste, eller Kart - men den nøyaktige typen er irrelevant. I tillegg har Stream.of () metoden og den nye Set.of (), Liste over(), og Map.ofEntries () metoder tillater oss å gjøre det samme med større samlinger.

Det er mange andre eksempler på dette også, inkludert Charset.forName (), som vil returnere en annen forekomst av Charset klasse avhengig av navnet du har bedt om, og ResourceBundle.getBundle (), som vil laste inn en annen ressurspakke, avhengig av navnet du oppgir.

Ikke alle disse trenger å gi forskjellige forekomster, heller. Noen er bare abstraksjoner for å skjule indre arbeid. For eksempel, Calendar.getInstance () og NumberFormat.getInstance () returner alltid samme forekomst, men de eksakte detaljene er irrelevante for klientkoden.

3. Abstrakt fabrikk

Abstract Factory-mønsteret er et skritt utover dette, der fabrikken som brukes, også har en abstrakt basetype. Vi kan da skrive koden vår i form av disse abstrakte typene, og velge den konkrete fabrikkinstansen på en eller annen måte ved kjøretid.

For det første har vi et grensesnitt og noen konkrete implementeringer for funksjonaliteten vi faktisk vil bruke:

grensesnitt FileSystem {// ...} 
klasse LocalFileSystem implementerer FileSystem {// ...} 
klasse NetworkFileSystem implementerer FileSystem {// ...} 

Deretter har vi et grensesnitt og noen konkrete implementeringer for fabrikken for å oppnå det ovennevnte:

grensesnitt FileSystemFactory {FileSystem newInstance (); } 
klasse LocalFileSystemFactory implementerer FileSystemFactory {// ...} 
klasse NetworkFileSystemFactory implementerer FileSystemFactory {// ...} 

Vi har deretter en annen fabrikkmetode for å oppnå den abstrakte fabrikken som vi kan få den faktiske forekomsten gjennom:

klasse Eksempel {statisk FileSystemFactory getFactory (String fs) {FileSystemFactory fabrikk; if ("local" .equals (fs)) {fabrikk = ny LocalFileSystemFactory (); annet hvis ("nettverk" .equals (fs)) {fabrikk = nytt NetworkFileSystemFactory (); } returnere fabrikken; }}

Her har vi en FileSystemFactory grensesnitt som har to konkrete implementeringer. Vi velger den nøyaktige implementeringen ved kjøretid, men koden som bruker den, trenger ikke å bry seg om hvilken forekomst som faktisk brukes. Disse returnerer deretter hver sin konkrete forekomst av Filsystem grensesnitt, men igjen, koden vår trenger ikke å bry seg nøyaktig hvilken forekomst av dette vi har.

Ofte får vi fabrikken selv ved å bruke en annen fabrikkmetode, som beskrevet ovenfor. I vårt eksempel her, getFactory () metoden er i seg selv en fabrikkmetode som returnerer et abstrakt FileSystemFactory som deretter brukes til å konstruere en Filsystem.

3.1. Eksempler i JVM

Det er mange eksempler på dette designmønsteret som brukes i hele JVM. De mest sett er rundt XML-pakkene - for eksempel DocumentBuilderFactory, TransformerFabrikk, og XPathFactory. Disse har alle en spesiell newInstance () fabrikkmetode for å la koden vår få en forekomst av den abstrakte fabrikken.

Internt bruker denne metoden en rekke forskjellige mekanismer - systemegenskaper, konfigurasjonsfiler i JVM og Service Provider Interface - for å prøve å bestemme nøyaktig hvilken konkret forekomst som skal brukes. Dette gjør at vi kan installere alternative XML-biblioteker i applikasjonen vår hvis vi ønsker det, men dette er gjennomsiktig for alle koder som faktisk bruker dem.

Når koden vår har kalt newInstance () metode, vil den ha en forekomst av fabrikken fra det aktuelle XML-biblioteket. Denne fabrikken konstruerer deretter de faktiske klassene vi vil bruke fra det samme biblioteket.

For eksempel, hvis vi bruker JVM standard Xerces-implementering, får vi en forekomst av com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl, men hvis vi i stedet ville bruke en annen implementering, så ringe newInstance () ville returnere det i stedet.

4. Byggmester

Byggmønsteret er nyttig når vi ønsker å konstruere et komplisert objekt på en mer fleksibel måte. Det fungerer ved å ha en egen klasse som vi bruker for å bygge vårt kompliserte objekt og la klienten lage dette med et enklere grensesnitt:

klasse CarBuilder {private String make = "Ford"; privat strengmodell = "Fiesta"; private int-dører = 4; privat strengfarge = "Hvit"; offentlig bilbygg () {returner ny bil (merke, modell, dører, farge); }}

Dette lar oss individuelt oppgi verdier for gjøre, modell, dører, og farge, og så når vi bygger Bil, blir alle konstruktørargumentene løst til de lagrede verdiene.

4.1. Eksempler i JVM

Det er noen veldig viktige eksempler på dette mønsteret i JVM. De StringBuilder og StringBuffer klasser er byggere som lar oss konstruere en lang String ved å skaffe mange små deler. Jo nyere Stream.Builder klasse tillater oss å gjøre nøyaktig det samme for å konstruere en Strøm:

Stream.Builder builder = Stream.builder (); builder.add (1); builder.add (2); if (condition) {builder.add (3); builder.add (4); } builder.add (5); Stream stream = builder.build ();

5. Lat initialisering

Vi bruker Lazy Initialization-mønsteret for å utsette beregningen av noe verdi til det trengs. Noen ganger kan dette involvere individuelle data, og andre ganger kan dette bety hele objekter.

Dette er nyttig i en rekke scenarier. For eksempel, Hvis fullstendig konstruksjon av et objekt krever databasetilgang eller nettverkstilgang, og vi kanskje aldri trenger å bruke det, kan det å utføre disse samtalene føre til at applikasjonen vår underpresterer. Alternativt, hvis vi beregner et stort antall verdier som vi kanskje aldri trenger, kan dette føre til unødvendig minnebruk.

Vanligvis fungerer dette ved å ha et objekt til å være den dovne omslaget rundt dataene vi trenger, og få dataene beregnet når de er tilgjengelige via en getter-metode:

klasse LazyPi {privat leverandørkalkulator; privat dobbel verdi; offentlig synkronisert Double getValue () {if (value == null) {value = calculator.get (); } returverdi; }}

Å beregne pi er en kostbar operasjon og en som vi kanskje ikke trenger å utføre. Ovennevnte vil gjøre det første gang vi ringer getValue () og ikke før.

5.1. Eksempler i JVM

Eksempler på dette i JVM er relativt sjeldne. Imidlertid er Streams API introdusert i Java 8 et godt eksempel. Alle operasjonene som utføres på en strøm er lat, slik at vi kan utføre dyre beregninger her og vite at de bare kalles om nødvendig.

Derimot, den faktiske generasjonen av selve strømmen kan også være lat. Stream.generate () tar en funksjon for å ringe når neste verdi er nødvendig, og blir alltid ringt når det er nødvendig. Vi kan bruke dette til å laste dyre verdier - for eksempel ved å ringe HTTP API - og vi betaler bare kostnadene når et nytt element faktisk er nødvendig:

Stream.generate (ny BaeldungArticlesLoader ()) .filter (artikkel -> article.getTags (). Inneholder ("java-streams")). Kart (artikkel -> article.getTitle ()) .findFirst ();

Her har vi en Leverandør som vil ringe HTTP for å laste inn artikler, filtrere dem basert på de tilknyttede kodene, og deretter returnere den første matchende tittelen. Hvis den aller første lastede artikkelen samsvarer med dette filteret, trenger du bare å foreta en enkelt nettverksanrop, uavhengig av hvor mange artikler som faktisk er til stede.

6. Objektbasseng

Vi bruker Object Pool-mønsteret når vi konstruerer en ny forekomst av et objekt som kan være dyrt å lage, men å bruke en eksisterende forekomst er et akseptabelt alternativ. I stedet for å konstruere en ny forekomst hver gang, kan vi i stedet konstruere et sett av disse foran og deretter bruke dem etter behov.

Selve objektgruppen eksisterer for å administrere disse delte objektene. Den sporer dem også slik at hver enkelt bare brukes på ett sted samtidig. I noen tilfeller blir hele settet med objekter konstruert bare i starten. I andre tilfeller kan bassenget opprette nye forekomster etter behov, hvis det er nødvendig

6.1. Eksempler i JVM

Hovedeksemplet på dette mønsteret i JVM er bruk av trådbassenger. An ExecutorService vil administrere et sett med tråder og vil tillate oss å bruke dem når en oppgave må utføres på en. Å bruke dette betyr at vi ikke trenger å lage nye tråder, med alle kostnadene som er involvert, når vi trenger å gyte en asynkron oppgave:

ExecutorService pool = Executors.newFixedThreadPool (10); pool.execute (ny SomeTask ()); // Kjører på en tråd fra bassengbassenget. Utfør (ny AnotherTask ()); // Kjører på en tråd fra bassenget

Disse to oppgavene blir tildelt en tråd som du kan kjøre fra trådgruppen. Det kan være den samme tråden eller en helt annen, og det spiller ingen rolle for koden vår hvilke tråder som brukes.

7. Prototype

Vi bruker prototypemønsteret når vi trenger å lage nye forekomster av et objekt som er identisk med originalen. Den opprinnelige forekomsten fungerer som vår prototype og blir vant til å konstruere nye forekomster som da er helt uavhengige av originalen. Vi kan da bruke disse, men det er nødvendig.

Java har et nivå av støtte for dette ved å implementere Klonbar markørgrensesnitt og deretter bruke Object.clone (). Dette vil gi en grunne klone av objektet, opprette en ny forekomst og kopiere feltene direkte.

Dette er billigere, men har ulempen at alle felt i objektet vårt som har strukturert seg selv, vil være den samme forekomsten. Dette betyr da at endringer i disse feltene også skjer i alle tilfeller. Vi kan imidlertid alltid overstyre dette selv om det er nødvendig:

offentlig klasse Prototype implementerer Cloneable {private Map contents = new HashMap (); public void setValue (String key, String value) {// ...} public String getValue (String key) {// ...} @ Override public Prototype clone () {Prototype result = new Prototype (); this.contents.entrySet (). forEach (entry -> result.setValue (entry.getKey (), entry.getValue ())); returresultat; }}

7.1. Eksempler i JVM

JVM har noen få eksempler på dette. Vi kan se disse ved å følge klassene som implementerer Klonbar grensesnitt. For eksempel, PKIXCertPathBuilderResult, PKIXBuilderParameters, PKIX-parametere, PKIXCertPathBuilderResult, og PKIXCertPathValidatorResult er alle Klonbar.

Et annet eksempel er java.util.Date klasse. Spesielt dette overstyrer Gjenstand.klone () metode for å kopiere over et ekstra forbigående felt også.

8. Singleton

Singleton-mønsteret brukes ofte når vi har en klasse som bare noen gang skal ha en forekomst, og denne forekomsten skal være tilgjengelig fra hele applikasjonen. Vanligvis klarer vi dette med en statisk forekomst som vi får tilgang til via en statisk metode:

offentlig klasse Singleton {privat statisk Singleton-forekomst = null; offentlig statisk Singleton getInstance () {if (forekomst == null) {forekomst = ny Singleton (); } returnere forekomst; }}

Det er flere varianter av dette avhengig av de nøyaktige behovene - for eksempel om forekomsten opprettes ved oppstart eller ved første gangs bruk, om tilgang til den må være trådsikker, og om det trenger å være en annen forekomst per tråd.

8.1. Eksempler i JVM

JVM har noen eksempler på dette med klasser som representerer kjernedeler av selve JVMRuntime, Desktop, og Sikkerhetssjef. Disse har alle tilgangsmetoder som returnerer den enkelte forekomsten av den respektive klassen.

I tillegg fungerer mye av Java Reflection API med singleton-forekomster. Den samme faktiske klassen returnerer alltid den samme forekomsten av Klasse, uavhengig av om den er tilgjengelig med Class.forName (), Strengklasse, eller gjennom andre refleksjonsmetoder.

På en lignende måte kan vi vurdere Tråd forekomst som representerer den gjeldende tråden for å være en singleton. Det kommer ofte til å være mange forekomster av dette, men per definisjon er det en enkelt forekomst per tråd. Ringer Thread.currentThread () hvor som helst som kjøres i samme tråd, vil alltid returnere samme forekomst.

9. Oppsummering

I denne artikkelen har vi sett på forskjellige designmønstre som brukes til å lage og skaffe forekomster av objekter. Vi har også sett på eksempler på disse mønstrene som brukes i kjernen JVM også, slik at vi kan se dem i bruk på en måte som mange applikasjoner allerede drar nytte av.


$config[zx-auto] not found$config[zx-overlay] not found