模式或設計模式是開發人員工作中經常被忽略的部分,使得程式碼難以維護和適應新的需求。我建議你看看它是什麼以及它在JDK中是如何使用的。當然,所有以某種形式存在的基本模式已經在我們身邊存在了很長時間。讓我們在這篇評論中看看它們。
內容:
也就是說,根據環境(操作系統),我們將獲得某個工廠來創建相容的元素。作為透過其他人創建的方法的替代方法,我們可以使用「原型」模式。它的本質很簡單——新的物體是按照現有物體的形象和相似性創建的,即 根據他們的原型。在 Java 中,每個人都遇到過這種模式 - 這是介面的使用
有關詳細信息,請參閱文章“ Java AWT 中的模式”。在結構模式中,我還想提一下「Facade」模式。它的本質是將使用此 API 背後的庫/框架的複雜性隱藏在方便簡潔的介面後面。例如,您可以使用 JPA 中的 JSF 或 EntityManager 作為範例。還有另一種模式稱為「享元」。它的本質是,如果不同的物件具有相同的狀態,那麼它可以被泛化,並且不是儲存在每個物件中,而是儲存在一個地方。然後每個物件將能夠引用一個公共部分,這將減少儲存的記憶體成本。這種模式通常涉及預先快取或維護物件池。有趣的是,我們從一開始就知道這種模式:
同樣的道理,這裡可以包含一個字串池。您可以閱讀有關此主題的文章:「享元設計模式」。
範本
職位空缺中最常見的要求之一是「了解模式」。首先,有必要回答一個簡單的問題——“什麼是設計模式?” Pattern從英文翻譯過來就是「模板」。也就是說,這是我們做某事的某種模式。在程式設計中也是如此。有一些既定的最佳實踐和方法來解決常見問題。每個程式設計師都是架構師。即使你只創建幾個類甚至一個類,這也取決於你的程式碼在不斷變化的需求下能存活多久,被其他人使用有多方便。這就是模板知識會有所幫助的地方,因為...... 這將使您能夠快速了解如何最好地編寫程式碼而不需要重寫它。如你所知,程式設計師都是懶惰的人,立即寫出好的東西比重做幾次要容易)模式也可能看起來類似於演算法。但他們有差別。該演算法由描述必要操作的特定步驟組成。模式僅描述方法,但不描述實作步驟。圖案不一樣,因為... 解決不同的問題。通常區分以下類別:-
生成式
這些模式解決了使物件創建靈活的問題
-
結構性
這些模式解決了有效建立物件之間聯繫的問題
-
行為的
這些模式解決了物件之間有效互動的問題
創作模式
讓我們從物件生命週期的開始——物件的建立開始。生成模板有助於更方便地創建對象,並在此過程中提供靈活性。其中最著名的是「建造者」。此模式可讓您逐步建立複雜的物件。在 Java 中,最著名的例子是StringBuilder
:
class Main {
public static void main(String[] args) {
StringBuilder builder = new StringBuilder();
builder.append("Hello");
builder.append(',');
builder.append("World!");
System.out.println(builder.toString());
}
}
創建物件的另一種眾所周知的方法是將創建轉移到單獨的方法中。這個方法就像一個物件工廠。這就是為什麼該模式被稱為“工廠方法”。以Java為例,其效果可以在類別中看到java.util.Calendar
。該類別本身Calendar
是抽象的,創建它的方法如下getInstance
:
import java.util.*;
class Main {
public static void main(String[] args) {
Calendar calendar = Calendar.getInstance();
System.out.println(calendar.getTime());
System.out.println(calendar.getClass().getCanonicalName());
}
}
這通常是因為物件創建背後的邏輯可能很複雜。例如,在上面的例子中,我們訪問基類Calendar
,並且創建了一個類別GregorianCalendar
。如果我們查看建構函數,我們可以看到根據條件創建不同的實作Calendar
。但有時一種工廠方法是不夠的。有時您需要建立不同的物件以使它們組合在一起。另一個範本將幫助我們完成這個任務 - “抽象工廠”。然後我們需要在一個地方創建不同的工廠。同時,優點是實現細節對我們來說並不重要,即 我們得到哪個具體工廠並不重要。最主要的是它創建了正確的實作。超級例子:
java.lang.Cloneable
:
class Main {
public static void main(String[] args) {
class CloneObject implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return new CloneObject();
}
}
CloneObject obj = new CloneObject();
try {
CloneObject pattern = (CloneObject) obj.clone();
} catch (CloneNotSupportedException e) {
//Do something
}
}
}
正如您所看到的,呼叫者不知道clone
. 也就是說,基於原型創建物件是物件本身的責任。這很有用,因為它不會將使用者與模板物件的實作連結起來。好吧,這個清單中的最後一個是「單例」模式。它的目的很簡單——為整個應用程式提供物件的單一實例。這種模式很有趣,因為它經常顯示多執行緒問題。要更深入地了解,請查看這些文章:
- 《Java 中正確的單例》
- 《JAVA 中的單例實現》
- “單例模式”
結構模式
隨著物件的創建,它變得更加清晰。現在是研究結構模式的時候了。他們的目標是建立易於支援的類別層次結構及其關係。第一個也是眾所周知的模式是「代理」(Proxy)。代理與真實物件具有相同的接口,因此客戶端透過代理或直接工作沒有區別。最簡單的例子是java.lang.reflect.Proxy:import java.util.*;
import java.lang.reflect.*;
class Main {
public static void main(String[] arguments) {
final Map<String, String> original = new HashMap<>();
InvocationHandler proxy = (obj, method, args) -> {
System.out.println("Invoked: " + method.getName());
return method.invoke(original, args);
};
Map<String, String> proxyInstance = (Map) Proxy.newProxyInstance(
original.getClass().getClassLoader(),
original.getClass().getInterfaces(),
proxy);
proxyInstance.put("key", "value");
System.out.println(proxyInstance.get("key"));
}
}
正如您所看到的,在範例中我們有原始的 - 這是HashMap
實作介面的範例Map
。接下來,我們建立一個代理來取代客戶端部分的原始代理HashMap
,該代理呼叫put
和方法get
,在呼叫期間添加我們自己的邏輯。正如我們所看到的,模式中的互動是透過介面發生的。但有時替代品還不夠。然後就可以使用「 Decorator 」模式了。裝飾器也稱為包裝器或包裝器。代理和裝飾器非常相似,但是如果你看一下範例,你會發現差異:
import java.util.*;
class Main {
public static void main(String[] arguments) {
List<String> list = new ArrayList<>();
List<String> decorated = Collections.checkedList(list, String.class);
decorated.add("2");
list.add("3");
System.out.println(decorated);
}
}
與代理商不同,裝飾器將自身包裝在作為輸入傳遞的內容周圍。代理既可以接受需要代理的內容,也可以管理代理物件的生命週期(例如,建立代理物件)。還有另一個有趣的模式——「適配器」。它類似於裝飾器 - 裝飾器將一個物件作為輸入並傳回該物件的包裝器。不同之處在於,目標不是改變功能,而是使一個介面適應另一個介面。Java 有一個很清楚的例子:
import java.util.*;
class Main {
public static void main(String[] arguments) {
String[] array = {"One", "Two", "Three"};
List<String> strings = Arrays.asList(array);
strings.set(0, "1");
System.out.println(Arrays.toString(array));
}
}
在輸入處我們有一個陣列。接下來,我們建立一個適配器,將陣列引入介面List
。當使用它時,我們實際上是在使用一個陣列。因此,添加元素是行不通的,因為...... 原始數組無法更改。在這種情況下我們會得到UnsupportedOperationException
. 開發類別結構的下一個有趣方法是複合模式。有趣的是,使用一個介面的一組特定元素被排列在特定的樹狀層次結構中。透過呼叫父元素上的方法,我們可以在所有必要的子元素上呼叫該方法。此模式的一個主要範例是 UI(無論是 java.awt 還是 JSF):
import java.awt.*;
class Main {
public static void main(String[] arguments) {
Container container = new Container();
Component component = new java.awt.Component(){};
System.out.println(component.getComponentOrientation().isLeftToRight());
container.add(component);
container.applyComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
System.out.println(component.getComponentOrientation().isLeftToRight());
}
}
正如我們所看到的,我們已經在容器中添加了一個元件。然後我們要求容器應用組件的新方向。容器知道它由哪些元件組成,因此將該命令的執行委託給所有子元件。另一個有趣的模式是“橋”模式。之所以這樣稱呼它,是因為它描述了兩個不同類別層次結構之間的連結或橋樑。這些層次結構之一被視為抽象,另一個被視為實作。之所以突出顯示這一點,是因為抽象本身不執行操作,而是將此執行委託給實作。當存在「控制」類別和多種類型的「平台」類別(例如,Windows、Linux 等)時,通常會使用此模式。透過這種方法,這些層次結構之一(抽象)將接收對另一個層次結構(實現)的物件的引用,並將主要工作委託給它們。因為所有實作都遵循一個公共接口,所以它們可以在抽象內互換。在 Java 中,一個明顯的例子是java.awt
:
行為模式
因此,我們弄清楚瞭如何建立物件以及如何組織類別之間的連接。剩下的最有趣的事情是提供改變物件行為的靈活性。行為模式將幫助我們做到這一點。最常被提及的模式之一是「策略」模式。這就是《 Head First. Design Patterns 》一書中對模式的研究的開始。使用「策略」模式,我們可以在物件內儲存我們將如何執行操作,即 裡面的物件儲存了一個可以在程式碼執行期間更改的策略。這是我們在使用比較器時經常使用的模式:import java.util.*;
class Main {
public static void main(String[] args) {
List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
Comparator<String> comparator = Comparator.comparingInt(String::length);
Set dataSet = new TreeSet(comparator);
dataSet.addAll(data);
System.out.println("Dataset : " + dataSet);
}
}
在我們之前 - TreeSet
。TreeSet
它具有維持元素順序的行為,即 對它們進行排序(因為它是一個 SortedSet)。此行為有一個預設策略,我們在 JavaDoc 中看到:按「自然順序」排序(對於字串,這是字典順序)。如果您使用無參數建構函數,就會發生這種情況。但如果我們想改變策略,我們可以通過Comparator
。在這個例子中,我們可以將集合建立為new TreeSet(comparator)
,然後儲存元素的順序(儲存策略)將變更為比較器中指定的順序。有趣的是,有幾乎相同的模式稱為「狀態」。「狀態」模式表示,如果我們在主物件中有一些行為依賴於該物件的狀態,那麼我們可以將狀態本身描述為一個物件並更改狀態物件。並將呼叫從主對象委託給狀態。透過學習 Java 語言的基礎知識,我們了解到的另一種模式是「命令」模式。這種設計模式表明不同的命令可以表示為不同的類別。此模式與策略模式非常相似。但在策略模式中,我們重新定義如何執行特定操作(例如,排序TreeSet
)。在「命令」模式中,我們重新定義將執行什麼動作。當我們使用線程時,模式命令每天都伴隨著我們:
import java.util.*;
class Main {
public static void main(String[] args) {
Runnable command = () -> {
System.out.println("Command action");
};
Thread th = new Thread(command);
th.start();
}
}
如您所見,command 定義了將在新執行緒中執行的操作或命令。「責任鏈」模式也值得考慮。這個圖案也非常簡單。此模式表示,如果需要處理某些內容,那麼您可以在鏈中收集處理程序。例如,這種模式經常用在 Web 伺服器中。在輸入處,伺服器收到用戶的一些請求。然後該請求將通過處理鏈。此處理程序鏈包括過濾器(例如,不接受來自 IP 位址黑名單的請求)、驗證處理程序(僅允許授權使用者)、請求標頭處理程序、快取處理程序等。但Java中有一個更簡單、更容易理解的例子java.util.logging
:
import java.util.logging.*;
class Main {
public static void main(String[] args) {
Logger logger = Logger.getLogger(Main.class.getName());
ConsoleHandler consoleHandler = new ConsoleHandler(){
@Override
public void publish(LogRecord record) {
System.out.println("LogRecord обработан");
}
};
logger.addHandler(consoleHandler);
logger.info("test");
}
}
如您所見,處理程序已新增至記錄器處理程序清單。當記錄器收到要處理的訊息時,每個此類訊息都會通過logger.getHandlers
該記錄器的處理程序鏈(來自 )。我們每天看到的另一個模式是“迭代器”。它的本質是分離一個物件的集合(即代表資料結構的類別。例如List
)並遍歷這個集合。
import java.util.*;
class Main {
public static void main(String[] args) {
List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
Iterator<String> iterator = data.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
正如您所看到的,迭代器不是集合的一部分,而是由遍歷集合的單獨類別表示。迭代器的使用者甚至可能不知道它正在迭代哪個集合,即 他正在參觀什麼藏品?值得考慮「訪客」模式。訪客模式與迭代器模式非常相似。此模式可協助您繞過物件的結構並對這些物件執行操作。它們在概念上有所不同。迭代器遍歷集合,這樣使用迭代器的客戶端並不關心集合裡面是什麼,只有序列中的元素是重要的。訪客意味著我們所訪問的物件存在一定的層次結構或結構。例如,我們可以使用單獨的目錄處理和單獨的檔案處理。Java 對此模式有一個開箱即用的實現,其形式如下java.nio.file.FileVisitor
:
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.*;
class Main {
public static void main(String[] args) {
SimpleFileVisitor visitor = new SimpleFileVisitor() {
@Override
public FileVisitResult visitFile(Object file, BasicFileAttributes attrs) throws IOException {
System.out.println("File:" + file.toString());
return FileVisitResult.CONTINUE;
}
};
Path pathSource = Paths.get(System.getProperty("java.io.tmpdir"));
try {
Files.walkFileTree(pathSource, visitor);
} catch (AccessDeniedException e) {
// skip
} catch (IOException e) {
// Do something
}
}
}
有時需要一些物件對其他物件的變化做出反應,那麼「觀察者」模式就會幫助我們。最方便的方法是提供訂閱機制,允許某些物件監視並回應其他物件中發生的事件。這種模式經常用於對不同事件做出反應的各種偵聽器和觀察者。作為一個簡單的例子,我們可以回顧 JDK 第一個版本中該模式的實作:
import java.util.*;
class Main {
public static void main(String[] args) {
Observer observer = (obj, arg) -> {
System.out.println("Arg: " + arg);
};
Observable target = new Observable(){
@Override
public void notifyObservers(Object arg) {
setChanged();
super.notifyObservers(arg);
}
};
target.addObserver(observer);
target.notifyObservers("Hello, World!");
}
}
還有另一種有用的行為模式──「中介者」。它很有用,因為在複雜的系統中,它有助於消除不同物件之間的連接,並將物件之間的所有互動委託給某個作為中介的物件。這種模式最引人注目的應用之一是 Spring MVC,它使用了這種模式。您可以在這裡閱讀更多相關內容:「Spring:中介模式」。您經常可以在範例中看到相同的內容java.util.Timer
:
import java.util.*;
class Main {
public static void main(String[] args) {
Timer mediator = new Timer("Mediator");
TimerTask command = new TimerTask() {
@Override
public void run() {
System.out.println("Command pattern");
mediator.cancel();
}
};
mediator.schedule(command, 1000);
}
}
該範例看起來更像是命令模式。而「Mediator」模式的本質就隱藏在「a」的實作中Timer
。定時器裡面有一個任務佇列TaskQueue
,有一個執行緒TimerThread
。作為此類的客戶,我們不與它們交互,而是與Timer
對象交互,該對象響應我們對其方法的調用,訪問它作為中介的其他對象的方法。從外觀上看,它可能與“Facade”非常相似。但不同之處在於,當使用 Facade 時,組件不知道 Facade 的存在並相互通訊。當使用「Mediator」時,元件知道並使用中介,但彼此不直接聯繫。值得考慮的是「模板方法」模式,該模式從其名稱就可以看出來。最重要的是,程式碼的編寫方式是為程式碼的使用者(開發人員)提供一些演算法模板,允許重新定義其步驟。這使得程式碼使用者不必編寫整個演算法,而只需考慮如何正確執行該演算法的一個或另一個步驟。例如,Java 有一個抽象類AbstractList
,它通過List
. 然而,迭代器本身使用葉子方法,例如:get
, set
, remove
。這些方法的行為由後代的開發人員決定AbstractList
。因此, - 中的迭代器AbstractList
是迭代工作表的演算法的模板。特定實現的開發人員AbstractList
透過定義特定步驟的行為來改變本次迭代的行為。我們分析的最後一個模式是「快照」(Momento)模式。它的本質是保存物件的某種狀態並具有恢復該狀態的能力。JDK 中最知名的範例是物件序列化,即 java.io.Serializable
。讓我們來看一個例子:
import java.io.*;
import java.util.*;
class Main {
public static void main(String[] args) throws IOException {
ArrayList<String> list = new ArrayList<>();
list.add("test");
// Save State
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(stream)) {
out.writeObject(list);
}
// Load state
byte[] bytes = stream.toByteArray();
InputStream inputStream = new ByteArrayInputStream(bytes);
try (ObjectInputStream in = new ObjectInputStream(inputStream)) {
List<String> listNew = (List<String>) in.readObject();
System.out.println(listNew.get(0));
} catch (ClassNotFoundException e) {
// Do something. Can't find class fpr saved state
}
}
}
結論
正如我們從評論中看到的,模式多種多樣。他們每個人都解決自己的問題。了解這些模式可以幫助您隨時了解如何編寫系統,使其靈活、可維護且不易更改。最後,一些更深入研究的連結:- 最令人驚訝的模式資源:refactoring.guru
- 影片播放清單“物件導向程式設計中的設計模式”
- 設計模式練習
- 型式測試
- Udemy 上的模式課程
- Coursera 上的模式課程
GO TO FULL VERSION