1. Pengenalan
Corak Reka Bentuk adalah corak umum yang kami gunakan semasa menulis perisian kami . Mereka mewakili amalan terbaik yang dibangunkan dari masa ke masa. Ini kemudian dapat membantu kita memastikan bahawa kod kami dirancang dan dibina dengan baik.
Corak Penciptaan adalah corak reka bentuk yang memfokuskan pada bagaimana kita memperoleh contoh objek . Biasanya, ini bermaksud bagaimana kita membina contoh baru kelas, tetapi dalam beberapa kes, ini bermaksud mendapatkan contoh yang sudah siap dan siap untuk kita gunakan.
Dalam artikel ini, kita akan melihat semula beberapa corak reka bentuk kreasi yang biasa. Kami akan melihat rupa mereka dan di mana mencarinya di JVM atau perpustakaan teras lain.
2. Kaedah Kilang
Corak Kaedah Kilang adalah cara untuk kita memisahkan pembinaan contoh dari kelas yang kita bina. Ini supaya kita dapat menghilangkan jenis yang tepat, yang membolehkan kod pelanggan kita berfungsi dari segi antara muka atau kelas abstrak:
class SomeImplementation implements SomeInterface { // ... }
public class SomeInterfaceFactory { public SomeInterface newInstance() { return new SomeImplementation(); } }
Di sini, kod pelanggan kami tidak perlu tahu mengenai SomeImplementation , dan sebaliknya, ia berfungsi dari segi SomeInterface . Lebih dari itu, kita boleh mengubah jenis yang dikembalikan dari kilang kita dan kod pelanggan tidak perlu diubah . Ini termasuk merangkumi pemilihan jenis secara dinamik pada waktu runtime.
2.1. Contoh dalam JVM
Mungkin contoh yang paling terkenal dari corak ini JVM adalah kaedah pembinaan koleksi di kelas Koleksi , seperti singleton () , singletonList () , dan singletonMap (). Ini semua menunjukkan kembali koleksi yang sesuai - Set , List , atau Map - tetapi jenis yang tepat tidak relevan . Selain itu, kaedah Stream.of () dan kaedah Set.of () , List.of () , dan Map.ofEntries () yang baru membolehkan kami melakukan perkara yang sama dengan koleksi yang lebih besar.
Terdapat banyak contoh-contoh lain ini juga, termasuk Charset.forName () , yang akan kembali contoh yang berbeza daripada Set aksara kelas bergantung kepada nama yang meminta, dan ResourceBundle.getBundle () , yang akan memuatkan sumber yang berbeza bundle bergantung pada nama yang disediakan.
Tidak semua ini perlu memberikan contoh yang berbeza. Sebilangannya hanyalah abstraksi untuk menyembunyikan kerja dalaman. Contohnya, Calendar.getInstance () dan NumberFormat.getInstance () selalu mengembalikan contoh yang sama, tetapi perincian yang tepat tidak berkaitan dengan kod pelanggan.
3. Kilang Abstrak
Corak Abstrak Kilang adalah satu langkah di luar ini, di mana kilang yang digunakan juga mempunyai jenis asas abstrak. Kami kemudian dapat menulis kod kami dari segi jenis abstrak ini, dan memilih contoh kilang konkrit entah bagaimana pada waktu proses.
Pertama, kami mempunyai antara muka dan beberapa pelaksanaan konkrit untuk fungsi yang sebenarnya ingin kami gunakan:
interface FileSystem { // ... }
class LocalFileSystem implements FileSystem { // ... }
class NetworkFileSystem implements FileSystem { // ... }
Seterusnya, kami mempunyai antara muka dan beberapa pelaksanaan konkrit agar kilang memperoleh perkara di atas:
interface FileSystemFactory { FileSystem newInstance(); }
class LocalFileSystemFactory implements FileSystemFactory { // ... }
class NetworkFileSystemFactory implements FileSystemFactory { // ... }
Kami kemudian mempunyai kaedah kilang lain untuk mendapatkan kilang abstrak di mana kita dapat memperoleh contoh sebenarnya:
class Example { static FileSystemFactory getFactory(String fs) { FileSystemFactory factory; if ("local".equals(fs)) { factory = new LocalFileSystemFactory(); else if ("network".equals(fs)) { factory = new NetworkFileSystemFactory(); } return factory; } }
Di sini, kami mempunyai antara muka FileSystemFactory yang mempunyai dua pelaksanaan konkrit. Kami memilih pelaksanaan yang tepat pada waktu runtime, tetapi kod yang menggunakannya tidak perlu peduli contoh mana yang sebenarnya digunakan . Ini kemudian masing-masing mengembalikan contoh konkrit yang berbeza dari antara muka FileSystem , tetapi sekali lagi, kod kami tidak perlu menjaga dengan tepat contoh mana yang kami ada.
Selalunya, kita memperoleh kilang itu sendiri menggunakan kaedah kilang lain, seperti yang dijelaskan di atas. Dalam contoh kami di sini, kaedah getFactory () adalah kaedah kilang yang mengembalikan FileSystemFactory yang kemudian digunakan untuk membina FileSystem .
3.1. Contoh dalam JVM
Terdapat banyak contoh corak reka bentuk ini yang digunakan di seluruh JVM. Yang paling biasa dilihat adalah sekitar pakej XML - contohnya, DocumentBuilderFactory , TransformerFactory, dan XPathFactory . Ini semua mempunyai kaedah kilang baruInstance () khas untuk membolehkan kod kami memperoleh contoh kilang abstrak .
Secara dalaman, kaedah ini menggunakan sejumlah mekanisme yang berbeza - sifat sistem, fail konfigurasi di JVM, dan Antaramuka Penyedia Perkhidmatan - untuk mencuba dan memutuskan dengan tepat contoh konkrit mana yang akan digunakan. Ini kemudian membolehkan kita memasang perpustakaan XML alternatif dalam aplikasi kita jika kita mahu, tetapi ini jelas untuk mana-mana kod yang benar-benar menggunakannya.
Setelah kod kami memanggil kaedah newInstance () , ia akan mempunyai contoh kilang dari perpustakaan XML yang sesuai. Kilang ini kemudian membina kelas sebenar yang ingin kita gunakan dari perpustakaan yang sama.
Sebagai contoh, jika kita menggunakan pelaksanaan Xerces lalai JVM, kita akan mendapat contoh com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl , tetapi jika kita ingin menggunakan pelaksanaan yang berbeza, maka panggil newInstance () akan mengembalikannya secara telus.
4. Pembina
Corak Pembina berguna apabila kita ingin membina objek yang rumit dengan cara yang lebih fleksibel. Ia berfungsi dengan mempunyai kelas berasingan yang kami gunakan untuk membina objek yang rumit dan membolehkan pelanggan membuat ini dengan antara muka yang lebih sederhana:
class CarBuilder { private String make = "Ford"; private String model = "Fiesta"; private int doors = 4; private String color = "White"; public Car build() { return new Car(make, model, doors, color); } }
Ini membolehkan kita memberikan nilai-nilai untuk membuat , model , pintu , dan warna secara individu , dan ketika kita membina Kereta , semua argumen konstruktor dapat diselesaikan ke nilai yang tersimpan.
4.1. Contoh dalam JVM
There are some very key examples of this pattern within the JVM. The StringBuilder and StringBuffer classes are builders that allow us to construct a long String by providing many small parts. The more recent Stream.Builder class allows us to do exactly the same in order to construct a Stream:
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. Lazy Initialization
We use the Lazy Initialization pattern to defer the calculation of some value until it's needed. Sometimes, this can involve individual pieces of data, and other times, this can mean entire objects.
This is useful in a number of scenarios. For example, if fully constructing an object requires database or network access and we may never need to use it, then performing those calls may cause our application to under-perform. Alternatively, if we're computing a large number of values that we may never need, then this can cause unnecessary memory usage.
Typically, this works by having one object be the lazy wrapper around the data that we need, and having the data computed when accessed via a getter method:
class LazyPi { private Supplier calculator; private Double value; public synchronized Double getValue() { if (value == null) { value = calculator.get(); } return value; } }
Computing pi is an expensive operation and one that we may not need to perform. The above will do so on the first time that we call getValue() and not before.
5.1. Examples in the JVM
Examples of this in the JVM are relatively rare. However, the Streams API introduced in Java 8 is a great example. All of the operations performed on a stream are lazy, so we can perform expensive calculations here and know they are only called if needed.
However, the actual generation of the stream itself can be lazy as well. Stream.generate() takes a function to call whenever the next value is needed and is only ever called when needed. We can use this to load expensive values – for example, by making HTTP API calls – and we only pay the cost whenever a new element is actually needed:
Stream.generate(new BaeldungArticlesLoader()) .filter(article -> article.getTags().contains("java-streams")) .map(article -> article.getTitle()) .findFirst();
Here, we have a Supplier that will make HTTP calls to load articles, filter them based on the associated tags, and then return the first matching title. If the very first article loaded matches this filter, then only a single network call needs to be made, regardless of how many articles are actually present.
6. Object Pool
We'll use the Object Pool pattern when constructing a new instance of an object that may be expensive to create, but re-using an existing instance is an acceptable alternative. Instead of constructing a new instance every time, we can instead construct a set of these up-front and then use them as needed.
The actual object pool exists to manage these shared objects. It also tracks them so that each one is only used in one place at the same time. In some cases, the entire set of objects gets constructed only at the start. In other cases, the pool may create new instances on demand if it's necessary
6.1. Examples in the JVM
The main example of this pattern in the JVM is the use of thread pools. An ExecutorService will manage a set of threads and will allow us to use them when a task needs to execute on one. Using this means that we don't need to create new threads, with all of the cost involved, whenever we need to spawn an asynchronous task:
ExecutorService pool = Executors.newFixedThreadPool(10); pool.execute(new SomeTask()); // Runs on a thread from the pool pool.execute(new AnotherTask()); // Runs on a thread from the pool
These two tasks get allocated a thread on which to run from the thread pool. It might be the same thread or a totally different one, and it doesn't matter to our code which threads are used.
7. Prototype
We use the Prototype pattern when we need to create new instances of an object that are identical to the original. The original instance acts as our prototype and gets used to construct new instances that are then completely independent of the original. We can then use these however is necessary.
Java has a level of support for this by implementing the Cloneable marker interface and then using Object.clone(). This will produce a shallow clone of the object, creating a new instance, and copying the fields directly.
This is cheaper but has the downside that any fields inside our object that have structured themselves will be the same instance. This, then, means changes to those fields also happen across all instances. However, we can always override this ourselves if necessary:
public class Prototype implements 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())); return result; } }
7.1. Examples in the JVM
The JVM has a few examples of this. We can see these by following the classes that implement the Cloneable interface. For example, PKIXCertPathBuilderResult, PKIXBuilderParameters, PKIXParameters, PKIXCertPathBuilderResult, and PKIXCertPathValidatorResult are all Cloneable.
Another example is the java.util.Date class. Notably, this overrides the Object.clone() method to copy across an additional transient field as well.
8. Singleton
The Singleton pattern is often used when we have a class that should only ever have one instance, and this instance should be accessible from throughout the application. Typically, we manage this with a static instance that we access via a static method:
public class Singleton { private static Singleton instance = null; public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
There are several variations to this depending on the exact needs — for example, whether the instance is created at startup or on first use, whether accessing it needs to be threadsafe, and whether or not there needs to be a different instance per thread.
8.1. Examples in the JVM
The JVM has some examples of this with classes that represent core parts of the JVM itself — Runtime, Desktop, and SecurityManager. These all have accessor methods that return the single instance of the respective class.
Additionally, much of the Java Reflection API works with singleton instances. The same actual class always returns the same instance of Class, regardless of whether it's accessed using Class.forName(), String.class, or through other reflection methods.
Dengan cara yang serupa, kami mungkin menganggap contoh Thread yang mewakili thread semasa sebagai singleton. Sering kali terdapat banyak contoh ini, tetapi menurut definisi, terdapat satu contoh per untaian. Memanggil Thread.currentThread () dari mana sahaja yang dijalankan dalam thread yang sama akan selalu mengembalikan instance yang sama.
9. Ringkasan
Dalam artikel ini, kami telah melihat pelbagai corak reka bentuk yang berbeza yang digunakan untuk membuat dan mendapatkan contoh objek. Kami juga telah melihat contoh corak ini seperti yang digunakan dalam JVM teras, jadi kami dapat melihatnya digunakan dengan cara yang banyak manfaat dari aplikasi.