Skabelsesmønstre i Core Java

1. Introduktion

Designmønstre er almindelige mønstre, som vi bruger, når vi skriver vores software. De repræsenterer etablerede bedste praksis udviklet over tid. Disse kan derefter hjælpe os med at sikre, at vores kode er godt designet og godt bygget.

Skabelsesmønstre er designmønstre, der fokuserer på, hvordan vi opnår forekomster af objekter. Typisk betyder dette, hvordan vi konstruerer nye forekomster af en klasse, men i nogle tilfælde betyder det at få en allerede konstrueret instans klar til brug.

I denne artikel vil vi revidere nogle almindelige skabelsesmønstre. Vi ser, hvordan de ser ud, og hvor de kan findes inden for JVM eller andre kernebiblioteker.

2. Fabriksmetode

Fabriksmetodemønsteret er en måde for os at adskille konstruktionen af ​​en instans fra den klasse, vi konstruerer. Dette er så vi kan abstrakte væk den nøjagtige type, så vores klientkode i stedet fungerer i form af grænseflader eller abstrakte klasser:

klasse SomeImplementation implementerer SomeInterface {// ...} 
offentlig klasse SomeInterfaceFactory {offentlig SomeInterface newInstance () {returner ny SomeImplementation (); }}

Her behøver vores klientkode aldrig at vide om Noget implementering, og i stedet fungerer det i form af SomeInterface. Endnu mere end dette dog vi kan ændre den type, der returneres fra vores fabrik, og klientkoden behøver ikke at ændre. Dette kan endda omfatte dynamisk valg af typen ved kørsel.

2.1. Eksempler i JVM

Muligvis de mest kendte eksempler på dette mønster JVM er samlingen bygger metoder på Samlinger klasse, ligesom singleton (), singletonList ()og singletonMap (). Disse returnerer alle forekomster af den relevante samling - Sæt, Liste, eller Kort - men den nøjagtige type er irrelevant. Derudover er Stream.of () metode og den nye Sæt af(), Liste af()og Map.ofEntries () metoder giver os mulighed for at gøre det samme med større samlinger.

Der er også mange andre eksempler på dette, herunder Charset.forName (), som returnerer en anden forekomst af Charset klasse afhængigt af det valgte navn, og ResourceBundle.getBundle (), der indlæser en anden ressourcegruppe afhængigt af det angivne navn.

Ikke alle disse har brug for at give forskellige forekomster. Nogle er bare abstraktioner for at skjule indre arbejde. For eksempel, Calendar.getInstance () og NumberFormat.getInstance () returner altid den samme forekomst, men de nøjagtige detaljer er irrelevante for klientkoden.

3. Abstrakt fabrik

Abstrakt fabriksmønster er et skridt ud over dette, hvor den anvendte fabrik også har en abstrakt basistype. Vi kan derefter skrive vores kode i form af disse abstrakte typer og vælge den konkrete fabriksinstans på en eller anden måde ved runtime.

For det første har vi en grænseflade og nogle konkrete implementeringer af den funktionalitet, vi faktisk vil bruge:

grænseflade FileSystem {// ...} 
klasse LocalFileSystem implementerer FileSystem {// ...} 
klasse NetworkFileSystem implementerer FileSystem {// ...} 

Dernæst har vi en grænseflade og nogle konkrete implementeringer til fabrikken for at opnå ovenstående:

grænseflade FileSystemFactory {FileSystem newInstance (); } 
klasse LocalFileSystemFactory implementerer FileSystemFactory {// ...} 
klasse NetworkFileSystemFactory implementerer FileSystemFactory {// ...} 

Vi har derefter en anden fabriksmetode til at opnå den abstrakte fabrik, hvorigennem vi kan få den faktiske forekomst:

klasse Eksempel {statisk FileSystemFactory getFactory (String fs) {FileSystemFactory fabrik; if ("local" .equals (fs)) {fabrik = ny LocalFileSystemFactory (); ellers hvis ("netværk" .equals (fs)) {fabrik = ny NetworkFileSystemFactory (); } returnere fabrik }}

Her har vi en FileSystemFactory interface, der har to konkrete implementeringer. Vi vælger den nøjagtige implementering ved kørsel, men koden, der bruger den, behøver ikke at være opmærksom på, hvilken instans der faktisk bruges. Disse returnerer derefter hver en anden konkret forekomst af Filsystem interface, men igen, vores kode behøver ikke at være ligeglad med hvilken instans af dette vi har.

Ofte opnår vi selve fabrikken ved hjælp af en anden fabriksmetode som beskrevet ovenfor. I vores eksempel her er getFactory () metoden er i sig selv en fabriksmetode, der returnerer et abstrakt FileSystemFactory det bruges derefter til at konstruere en Filsystem.

3.1. Eksempler i JVM

Der er masser af eksempler på dette designmønster, der bruges i hele JVM. De mest almindelige er omkring XML-pakker - for eksempel DocumentBuilderFactory, TransformerFabrik, og XPathFactory. Disse har alle en speciel newInstance () fabriksmetode, så vores kode kan få en forekomst af den abstrakte fabrik.

Internt bruger denne metode en række forskellige mekanismer - systemegenskaber, konfigurationsfiler i JVM og Service Provider Interface - til at prøve og beslutte nøjagtigt, hvilken konkret instans der skal bruges. Dette giver os derefter mulighed for at installere alternative XML-biblioteker i vores applikation, hvis vi ønsker det, men dette er gennemsigtigt for enhver kode, der rent faktisk bruger dem.

Når vores kode har kaldt newInstance () metode, vil den derefter have en forekomst af fabrikken fra det relevante XML-bibliotek. Denne fabrik konstruerer derefter de faktiske klasser, vi vil bruge fra det samme bibliotek.

For eksempel, hvis vi bruger JVM-standard Xerces-implementeringen, får vi en forekomst af com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl, men hvis vi i stedet ville bruge en anden implementering, så ring newInstance () ville transparent returnere det i stedet.

4. Bygger

Builder-mønsteret er nyttigt, når vi vil konstruere et kompliceret objekt på en mere fleksibel måde. Det fungerer ved at have en separat klasse, som vi bruger til at opbygge vores komplicerede objekt og lade klienten oprette dette med en enklere grænseflade:

klasse CarBuilder {private String make = "Ford"; privat strengmodel = "Fiesta"; private int-døre = 4; privat strengfarve = "Hvid"; offentlig bilbygning () {returner ny bil (mærke, model, døre, farve); }}

Dette giver os mulighed for individuelt at angive værdier for lave, model, døreog farveog derefter når vi bygger Bil, alle konstruktørargumenter løses til de lagrede værdier.

4.1. Eksempler i JVM

Der er nogle meget vigtige eksempler på dette mønster inden for JVM. Det StringBuilder og StringBuffer klasser er bygherrer, der giver os mulighed for at konstruere en lang Snor ved at levere mange små dele. Jo nyere Stream.Builder klasse tillader os at gøre nøjagtigt det samme for at konstruere en Strøm:

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

5. Lazy initialisering

Vi bruger Lazy Initialization mønster til at udskyde beregningen af ​​en værdi, indtil det er nødvendigt. Nogle gange kan dette involvere individuelle data, og andre gange kan det betyde hele objekter.

Dette er nyttigt i en række scenarier. For eksempel, hvis fuldstændig konstruktion af et objekt kræver database- eller netværksadgang, og vi muligvis aldrig behøver at bruge det, kan udførelse af disse opkald få vores applikation til at udføre mindre. Alternativt, hvis vi beregner et stort antal værdier, som vi måske aldrig har brug for, kan dette medføre unødvendigt hukommelsesforbrug.

Typisk fungerer dette ved at have et objekt til at være den dovne indpakning omkring de data, vi har brug for, og at dataene beregnes, når de åbnes via en getter-metode:

klasse LazyPi {privat leverandørberegner; privat dobbelt værdi; offentlig synkroniseret Double getValue () {if (værdi == null) {værdi = calculator.get (); } returværdi }}

Computing pi er en dyr operation, og vi behøver muligvis ikke at udføre. Ovenstående vil gøre det første gang vi kalder getValue () og ikke før.

5.1. Eksempler i JVM

Eksempler på dette i JVM er relativt sjældne. Imidlertid er Streams API introduceret i Java 8 et godt eksempel. Alle operationer, der udføres på en strøm, er dovne, så vi kan udføre dyre beregninger her og vide, at de kun kaldes, hvis det er nødvendigt.

Imidlertid, den faktiske generation af selve strømmen kan også være doven. Stream.generate () tager en funktion til at ringe, når den næste værdi er nødvendig, og kaldes kun, når det er nødvendigt. Vi kan bruge dette til at indlæse dyre værdier - for eksempel ved at foretage HTTP API-opkald - og vi betaler kun omkostningerne, når der faktisk er behov for et nyt element:

Stream.generate (ny BaeldungArticlesLoader ()) .filter (artikel -> article.getTags (). Indeholder ("java-streams")). Kort (artikel -> article.getTitle ()) .findFirst ();

Her har vi en Leverandør der vil foretage HTTP-opkald for at indlæse artikler, filtrere dem baseret på de tilknyttede tags og derefter returnere den første matchende titel. Hvis den allerførste indlæste artikel matcher dette filter, skal der kun foretages et enkelt netværksopkald, uanset hvor mange artikler der faktisk er til stede.

6. Objektpool

Vi bruger Object Pool-mønsteret, når vi konstruerer en ny forekomst af et objekt, der kan være dyrt at oprette, men genbrug af en eksisterende forekomst er et acceptabelt alternativ. I stedet for at konstruere en ny forekomst hver gang, kan vi i stedet konstruere et sæt af disse foran og derefter bruge dem efter behov.

Den aktuelle objektpulje findes til at styre disse delte objekter. Det sporer dem også, så hver enkelt kun bruges ét sted på samme tid. I nogle tilfælde konstrueres hele sæt objekter kun i starten. I andre tilfælde kan puljen oprette nye forekomster efter behov, hvis det er nødvendigt

6.1. Eksempler i JVM

Det vigtigste eksempel på dette mønster i JVM er brugen af ​​trådpuljer. En ExecutorService administrerer et sæt tråde og giver os mulighed for at bruge dem, når en opgave skal udføres på en. Brug af dette betyder, at vi ikke behøver at oprette nye tråde med alle de involverede omkostninger, når vi har brug for at gyde en asynkron opgave:

ExecutorService-pool = Executors.newFixedThreadPool (10); pool.execute (ny SomeTask ()); // Kører på en tråd fra poolpoolen. Udfør (ny AnotherTask ()); // Kører på en tråd fra poolen

Disse to opgaver får en tråd, som de kan køre fra trådpuljen. Det kan være den samme tråd eller en helt anden, og det betyder ikke noget for vores kode, hvilke tråde der bruges.

7. Prototype

Vi bruger prototypemønsteret, når vi har brug for at oprette nye forekomster af et objekt, der er identisk med originalen. Den oprindelige forekomst fungerer som vores prototype og bliver vant til at konstruere nye forekomster, der derefter er helt uafhængige af originalen. Vi kan derefter bruge disse, men det er nødvendigt.

Java har et niveau af support til dette ved at implementere Klonabel markørgrænseflade og derefter bruge Object.clone (). Dette vil producere en lav klon af objektet, oprette en ny forekomst og kopiere felterne direkte.

Dette er billigere, men har ulempen, at eventuelle felter i vores objekt, der har struktureret sig selv, vil være den samme forekomst. Dette betyder, at ændringer i disse felter også sker på tværs af alle forekomster. Vi kan dog altid tilsidesætte dette selv, hvis det er nødvendigt:

offentlig klasse prototype implementerer Klonbar {privat kortindhold = nyt 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 (post -> result.setValue (entry.getKey (), entry.getValue ())); returresultat }}

7.1. Eksempler i JVM

JVM har et par eksempler på dette. Vi kan se disse ved at følge de klasser, der implementerer Klonabel interface. For eksempel, PKIXCertPathBuilderResult, PKIXBuilderParameters, PKIX-parametre, PKIXCertPathBuilderResultog PKIXCertPathValidatorResult er alle Klonabel.

Et andet eksempel er java.util.Date klasse. Især dette tilsidesætter Objekt.klon () metode til også at kopiere over et yderligere forbigående felt.

8. Singleton

Singleton-mønsteret bruges ofte, når vi har en klasse, der kun nogensinde skal have en forekomst, og denne forekomst skal være tilgængelig fra hele applikationen. Typisk administrerer vi dette med en statisk forekomst, som vi får adgang til via en statisk metode:

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

Der er flere variationer til dette afhængigt af de nøjagtige behov - for eksempel om forekomsten oprettes ved opstart eller ved første brug, om adgang til den skal være trådsikker, og om der skal være en anden forekomst pr. Tråd.

8.1. Eksempler i JVM

JVM har nogle eksempler på dette med klasser, der repræsenterer kernedele af selve JVMRuntime, Desktop, og SecurityManager. Disse har alle adgangsmetoder, der returnerer den enkelte forekomst af den respektive klasse.

Derudover fungerer meget af Java Reflection API med singleton-forekomster. Den samme aktuelle klasse returnerer altid den samme forekomst af Klasse, uanset om det åbnes ved hjælp af Class.forName (), Streng.klasseeller gennem andre refleksionsmetoder.

På en lignende måde kan vi overveje Tråd instans, der repræsenterer den aktuelle tråd for at være en singleton. Der vil ofte være mange forekomster af dette, men pr. Definition er der en enkelt forekomst pr. Tråd. Ringer Thread.currentThread () hvor som helst, der udføres i samme tråd, returnerer altid den samme forekomst.

9. Resume

I denne artikel har vi kigget på forskellige forskellige designmønstre, der bruges til at skabe og opnå forekomster af objekter. Vi har også set på eksempler på disse mønstre, som de også bruges inden for kernen JVM, så vi kan se dem i brug på en måde, som mange applikationer allerede har gavn af.


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