Creationele ontwerppatronen in Core Java

1. Inleiding

Ontwerppatronen zijn veelvoorkomende patronen die we gebruiken bij het schrijven van onze software. Ze vertegenwoordigen gevestigde best practices die in de loop van de tijd zijn ontwikkeld. Deze kunnen ons vervolgens helpen ervoor te zorgen dat onze code goed is ontworpen en goed is gebouwd.

Creationele patronen zijn ontwerppatronen die zich richten op hoe we exemplaren van objecten verkrijgen. Dit betekent meestal hoe we nieuwe instanties van een klasse construeren, maar in sommige gevallen betekent dit dat we een reeds geconstrueerde instantie klaar hebben staan ​​om te gebruiken.

In dit artikel gaan we enkele veelvoorkomende creatieve ontwerppatronen opnieuw bekijken. We zullen zien hoe ze eruit zien en waar we ze kunnen vinden in de JVM of andere kernbibliotheken.

2. Fabrieksmethode

Het Factory Method-patroon is voor ons een manier om de constructie van een instantie te scheiden van de klasse die we aan het construeren zijn. Dit is zodat we het exacte type kunnen abstraheren, waardoor onze klantcode in plaats daarvan kan werken in termen van interfaces of abstracte klassen:

klasse SomeImplementation implementeert SomeInterface {// ...} 
openbare klasse SomeInterfaceFactory {openbare SomeInterface newInstance () {retourneer nieuwe SomeImplementation (); }}

Hier hoeft onze klantcode nooit iets te weten SomeImplementation, en in plaats daarvan werkt het in termen van SomeInterface. Maar zelfs meer dan dit, we kunnen het type dat door onze fabriek wordt geretourneerd wijzigen en de klantcode hoeft niet te veranderen. Dit kan zelfs het dynamisch selecteren van het type tijdens runtime inhouden.

2.1. Voorbeelden in de JVM

Misschien wel de meest bekende voorbeelden van dit patroon van de JVM zijn de methoden voor het opbouwen van collecties op de Collecties klasse, zoals singleton (), singletonList (), en singletonMap (). Dit zijn allemaal terug te keren exemplaren van de juiste verzameling - Set, Lijst, of Kaart - maar het exacte type is niet relevant. Bovendien is het Stroom van() methode en de nieuwe Set.van (), Lijst van(), en Map.ofEntries () methoden stellen ons in staat hetzelfde te doen met grotere collecties.

Er zijn ook tal van andere voorbeelden, waaronder Charset.forName (), die een ander exemplaar van de Tekenset klasse afhankelijk van de gevraagde naam, en ResourceBundle.getBundle (), die een andere bronbundel laadt, afhankelijk van de opgegeven naam.

Deze hoeven ook niet allemaal verschillende instanties te bieden. Sommige zijn slechts abstracties om innerlijke werkingen te verbergen. Bijvoorbeeld, Calendar.getInstance () en NumberFormat.getInstance () retourneer altijd dezelfde instantie, maar de exacte details zijn niet relevant voor de clientcode.

3. Abstracte fabriek

Het Abstract Factory-patroon is een stap verder, waarbij de gebruikte fabriek ook een abstract basistype heeft. We kunnen dan onze code schrijven in termen van deze abstracte typen, en de concrete fabrieksinstantie op de een of andere manier tijdens runtime selecteren.

Ten eerste hebben we een interface en enkele concrete implementaties voor de functionaliteit die we daadwerkelijk willen gebruiken:

interface Bestandssysteem {// ...} 
class LocalFileSystem implementeert FileSystem {// ...} 
class NetworkFileSystem implementeert FileSystem {// ...} 

Vervolgens hebben we een interface en enkele concrete implementaties voor de fabriek om het bovenstaande te verkrijgen:

interface FileSystemFactory {FileSystem newInstance (); } 
klasse LocalFileSystemFactory implementeert FileSystemFactory {// ...} 
klasse NetworkFileSystemFactory implementeert FileSystemFactory {// ...} 

We hebben dan een andere fabrieksmethode om de abstracte fabriek te verkrijgen waarmee we het daadwerkelijke exemplaar kunnen verkrijgen:

klasse Voorbeeld {statische FileSystemFactory getFactory (String fs) {FileSystemFactory factory; if ("local" .equals (fs)) {factory = new LocalFileSystemFactory (); else if ("netwerk" .equals (fs)) {fabriek = nieuw NetworkFileSystemFactory (); } terugkeer fabriek; }}

Hier hebben we een FileSystemFactory interface met twee concrete implementaties. We selecteren de exacte implementatie tijdens runtime, maar de code die er gebruik van maakt, hoeft niet uit te maken welke instantie daadwerkelijk wordt gebruikt. Deze retourneren vervolgens elk een ander concreet exemplaar van het Bestandssysteem interface, maar nogmaals, het hoeft onze code niet uit te maken welk exemplaar we hebben.

Vaak verkrijgen we de fabriek zelf via een andere fabrieksmethode, zoals hierboven beschreven. In ons voorbeeld hier, de getFactory () methode is zelf een fabrieksmethode die een abstract retourneert FileSystemFactory dat wordt vervolgens gebruikt om een Bestandssysteem.

3.1. Voorbeelden in de JVM

Er zijn tal van voorbeelden van dit ontwerppatroon dat in de JVM wordt gebruikt. De meest voorkomende zijn rond de XML-pakketten - bijvoorbeeld DocumentBuilderFactory, TransformerFactory, en XPathFactory. Deze hebben allemaal een speciale newInstance () factory-methode om onze code toe te staan ​​een exemplaar van de abstracte fabriek te verkrijgen.

Intern gebruikt deze methode een aantal verschillende mechanismen - systeemeigenschappen, configuratiebestanden in de JVM en de serviceproviderinterface - om te proberen te beslissen welke concrete instantie er precies moet worden gebruikt. Hierdoor kunnen we desgewenst alternatieve XML-bibliotheken in onze applicatie installeren, maar dit is transparant voor elke code die ze daadwerkelijk gebruikt.

Zodra onze code het newInstance () methode, heeft het dan een exemplaar van de fabriek uit de juiste XML-bibliotheek. Deze fabriek bouwt vervolgens de feitelijke klassen die we willen gebruiken uit diezelfde bibliotheek.

Als we bijvoorbeeld de standaard JVM-implementatie van Xerces gebruiken, krijgen we een exemplaar van com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl, maar als we in plaats daarvan een andere implementatie wilden gebruiken, dan bellen newInstance () zou dat in plaats daarvan transparant teruggeven.

4. Bouwer

Het Builder-patroon is handig als we een gecompliceerd object op een meer flexibele manier willen construeren. Het werkt door een aparte klasse te hebben die we gebruiken voor het bouwen van ons gecompliceerde object en de klant in staat te stellen dit te maken met een eenvoudigere interface:

class CarBuilder {private String make = "Ford"; private String model = "Fiesta"; private int deuren = 4; private String color = "White"; public Car build () {nieuwe auto inleveren (merk, model, deuren, kleur); }}

Dit stelt ons in staat om individueel waarden voor te geven maken, model-, deuren, en kleur, en dan wanneer we het Auto, worden alle constructorargumenten omgezet naar de opgeslagen waarden.

4.1. Voorbeelden in de JVM

Er zijn enkele zeer belangrijke voorbeelden van dit patroon binnen de JVM. De StringBuilder en StringBuffer klassen zijn builders waarmee we een long kunnen construeren Draad door veel kleine onderdelen aan te bieden. Hoe recenter Stream.Builder class stelt ons in staat om precies hetzelfde te doen om een Stroom:

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

5. Luie initialisatie

We gebruiken het Lazy Initialization-patroon om de berekening van een bepaalde waarde uit te stellen totdat deze nodig is. Soms kan het gaan om individuele stukjes data, en soms om hele objecten.

Dit is handig in een aantal scenario's. Bijvoorbeeld, als het volledig construeren van een object database- of netwerktoegang vereist en we het misschien nooit hoeven te gebruiken, kan het uitvoeren van die aanroepen ervoor zorgen dat onze applicatie ondermaats presteert. Als we echter een groot aantal waarden berekenen die we misschien nooit nodig hebben, kan dit onnodig geheugengebruik veroorzaken.

Dit werkt meestal doordat één object de luie wikkel is rond de gegevens die we nodig hebben, en de gegevens worden berekend wanneer ze worden geopend via een getter-methode:

klasse LazyPi {particuliere leveranciercalculator; privé Dubbele waarde; publiek gesynchroniseerd Double getValue () {if (waarde == null) {waarde = calculator.get (); } winstwaarde; }}

Het berekenen van pi is een dure operatie en een die we misschien niet hoeven uit te voeren. Het bovenstaande zal dit doen bij de eerste keer dat we bellen getValue () en niet eerder.

5.1. Voorbeelden in de JVM

Voorbeelden hiervan in de JVM zijn relatief zeldzaam. De Streams API die in Java 8 is geïntroduceerd, is echter een goed voorbeeld. Alle bewerkingen die op een stream worden uitgevoerd, zijn lui, dus we kunnen hier dure berekeningen uitvoeren en weten dat ze alleen worden gebeld als dat nodig is.

Echter, de daadwerkelijke generatie van de stream zelf kan ook lui zijn. Stream.generate () neemt een functie om aan te roepen wanneer de volgende waarde nodig is en wordt alleen aangeroepen wanneer dat nodig is. We kunnen dit gebruiken om dure waarden te laden - bijvoorbeeld door HTTP API-aanroepen te doen - en we betalen alleen de kosten wanneer een nieuw element daadwerkelijk nodig is:

Stream.generate (nieuwe BaeldungArticlesLoader ()) .filter (artikel -> artikel.getTags (). Bevat ("java-streams")) .map (artikel -> artikel.getTitle ()) .findFirst ();

Hier hebben we een Leverancier die HTTP-oproepen zullen doen om artikelen te laden, ze filteren op basis van de bijbehorende tags en vervolgens de eerste overeenkomende titel retourneren. Als het allereerste artikel dat wordt geladen overeenkomt met dit filter, hoeft er slechts één netwerkoproep te worden gedaan, ongeacht hoeveel artikelen er daadwerkelijk aanwezig zijn.

6. Objectenpool

We zullen het Object Pool-patroon gebruiken bij het construeren van een nieuwe instantie van een object dat mogelijk duur is om te maken, maar het hergebruiken van een bestaande instantie is een acceptabel alternatief. In plaats van elke keer een nieuw exemplaar te construeren, kunnen we in plaats daarvan een set van deze van tevoren construeren en ze vervolgens gebruiken als dat nodig is.

De eigenlijke objectpool bestaat om deze gemeenschappelijke objecten te beheren. Het volgt ze ook zodat ze allemaal maar op één plaats tegelijk worden gebruikt. In sommige gevallen wordt de volledige set objecten pas aan het begin geconstrueerd. In andere gevallen kan de pool indien nodig nieuwe instanties op aanvraag maken

6.1. Voorbeelden in de JVM

Het belangrijkste voorbeeld van dit patroon in de JVM is het gebruik van threadpools. Een ExecutorService beheert een reeks threads en stelt ons in staat om ze te gebruiken wanneer een taak op een thread moet worden uitgevoerd. Door dit te gebruiken, hoeven we geen nieuwe threads te maken, met alle bijbehorende kosten, wanneer we een asynchrone taak moeten spawnen:

ExecutorService pool = Executors.newFixedThreadPool (10); pool.execute (nieuwe SomeTask ()); // Draait op een thread uit de pool pool.execute (new AnotherTask ()); // Draait op een thread uit de pool

Deze twee taken krijgen een thread toegewezen waarop ze vanuit de threadpool kunnen worden uitgevoerd. Het kan dezelfde thread zijn of een totaal andere, en het maakt voor onze code niet uit welke threads worden gebruikt.

7. Prototype

We gebruiken het prototype-patroon wanneer we nieuwe instanties van een object moeten maken die identiek zijn aan het origineel. De oorspronkelijke instantie fungeert als ons prototype en wordt gebruikt om nieuwe instanties te construeren die vervolgens volledig onafhankelijk zijn van het origineel. We kunnen deze dan gebruiken, maar het is nodig.

Java heeft hiervoor een mate van ondersteuning door de implementatie van het Te klonen marker interface en vervolgens gebruiken Object.clone (). Dit zal een ondiepe kloon van het object produceren, een nieuwe instantie maken en de velden rechtstreeks kopiëren.

Dit is goedkoper, maar heeft het nadeel dat alle velden in ons object die zichzelf hebben gestructureerd, dezelfde instantie zijn. Dit betekent dus dat wijzigingen in die velden ook in alle instanties plaatsvinden. We kunnen dit echter altijd zelf opheffen als dat nodig is:

public class Prototype implementeert Cloneable {private Map content = new HashMap (); public void setValue (String-sleutel, String-waarde) {// ...} public String getValue (String-sleutel) {// ...} @Override public Prototype clone () {Prototype-resultaat = nieuw prototype (); this.contents.entrySet (). forEach (entry -> result.setValue (entry.getKey (), entry.getValue ())); resultaat teruggeven; }}

7.1. Voorbeelden in de JVM

De JVM heeft hiervan een paar voorbeelden. We kunnen deze zien door de klassen te volgen die het Te klonen koppel. Bijvoorbeeld, PKIXCertPathBuilderResult, PKIXBuilderParameters, PKIXParameters, PKIXCertPathBuilderResult, en PKIXCertPathValidatorResult zijn alle Te klonen.

Een ander voorbeeld is de java.util.Date klasse. Met name dit heeft voorrang op de Voorwerp.kloon () methode om ook over een extra tijdelijk veld te kopiëren.

8. Singleton

Het Singleton-patroon wordt vaak gebruikt als we een klasse hebben die maar één instantie mag hebben, en deze instantie moet vanuit de hele applicatie toegankelijk zijn. Meestal beheren we dit met een statische instantie waartoe we toegang hebben via een statische methode:

openbare klasse Singleton {privé statische Singleton-instantie = null; openbare statische Singleton getInstance () {if (instance == null) {instance = new Singleton (); } terugkeerinstantie; }}

Hierop zijn verschillende variaties, afhankelijk van de exacte behoeften - bijvoorbeeld of de instantie bij het opstarten of bij het eerste gebruik wordt gemaakt, of de toegang threadsafe moet zijn en of er per thread een andere instantie moet zijn.

8.1. Voorbeelden in de JVM

De JVM heeft hiervan enkele voorbeelden met klassen die kernonderdelen van de JVM zelf vertegenwoordigenRuntime, Desktop, en Beveiligingsmanager. Deze hebben allemaal accessormethoden die de enkele instantie van de respectieve klasse retourneren.

Bovendien werkt een groot deel van de Java Reflection API met singleton-instances. Dezelfde feitelijke klasse retourneert altijd hetzelfde exemplaar van Klasse, ongeacht of het wordt geopend met Class.forName (), String.class, of via andere reflectiemethoden.

Op een vergelijkbare manier zouden we de Draad instantie die de huidige thread vertegenwoordigt als een singleton. Hiervan zullen vaak veel gevallen zijn, maar per definitie is er één exemplaar per thread. Roeping Thread.currentThread () vanaf elke locatie die in dezelfde thread wordt uitgevoerd, retourneert altijd dezelfde instantie.

9. Samenvatting

In dit artikel hebben we verschillende ontwerppatronen bekeken die worden gebruikt voor het maken en verkrijgen van exemplaren van objecten. We hebben ook gekeken naar voorbeelden van deze patronen die ook in de kern-JVM worden gebruikt, zodat we ze in gebruik kunnen zien op een manier waar veel toepassingen al van profiteren.