JavaRush /Java Blog /Random-TW /Java 中的設計模式
Viacheslav
等級 3

Java 中的設計模式

在 Random-TW 群組發布
模式或設計模式是開發人員工作中經常被忽略的部分,使得程式碼難以維護和適應新的需求。我建議你看看它是什麼以及它在JDK中是如何使用的。當然,所有以某種形式存在的基本模式已經在我們身邊存在了很長時間。讓我們在這篇評論中看看它們。
Java 中的設計模式 - 1
內容:

範本

職位空缺中最常見的要求之一是「了解模式」。首先,有必要回答一個簡單的問題——“什麼是設計模式?” Pattern從英文翻譯過來就是「模板」。也就是說,這是我們做某事的某種模式。在程式設計中也是如此。有一些既定的最佳實踐和方法來解決常見問題。每個程式設計師都是架構師。即使你只創建幾個類甚至一個類,這也取決於你的程式碼在不斷變化的需求下能存活多久,被其他人使用有多方便。這就是模板知識會有所幫助的地方,因為...... 這將使您能夠快速了解如何最好地編寫程式碼而不需要重寫它。如你所知,程式設計師都是懶惰的人,立即寫出好的東西比重做幾次要容易)模式也可能看起來類似於演算法。但他們有差別。該演算法由描述必要操作的特定步驟組成。模式僅描述方法,但不描述實作步驟。圖案不一樣,因為... 解決不同的問題。通常區分以下類別:
  • 生成式

    這些模式解決了使物件創建靈活的問題

  • 結構性

    這些模式解決了有效建立物件之間聯繫的問題

  • 行為的

    這些模式解決了物件之間有效互動的問題

要考慮範例,我建議使用線上程式碼編譯器 repl.it。
Java 中的設計模式 - 2

創作模式

讓我們從物件生命週期的開始——物件的建立開始。生成模板有助於更方便地創建對象,並在此過程中提供靈活性。其中最著名的是「建造者」。此模式可讓您逐步建立複雜的物件。在 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 中的設計模式 - 3
也就是說,根據環境(操作系統),我們將獲得某個工廠來創建相容的元素。作為透過其他人創建的方法的替代方法,我們可以使用「原型」模式。它的本質很簡單——新的物體是按照現有物體的形象和相似性創建的,即 根據他們的原型。在 Java 中,每個人都遇到過這種模式 - 這是介面的使用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 中的設計模式 - 4

結構模式

隨著物件的創建,它變得更加清晰。現在是研究結構模式的時候了。他們的目標是建立易於支援的類別層次結構及其關係。第一個也是眾所周知的模式是「代理」(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
Java 中的設計模式 - 5
有關詳細信息,請參閱文章“ Java AWT 中的模式”。在結構模式中,我還想提一下「Facade」模式。它的本質是將使用此 API 背後的庫/框架的複雜性隱藏在方便簡潔的介面後面。例如,您可以使用 JPA 中的 JSF 或 EntityManager 作為範例。還有另一種模式稱為「享元」。它的本質是,如果不同的物件具有相同的狀態,那麼它可以被泛化,並且不是儲存在每個物件中,而是儲存在一個地方。然後每個物件將能夠引用一個公共部分,這將減少儲存的記憶體成本。這種模式通常涉及預先快取或維護物件池。有趣的是,我們從一開始就知道這種模式:
Java 中的設計模式 - 6
同樣的道理,這裡可以包含一個字串池。您可以閱讀有關此主題的文章:「享元設計模式」。
Java 中的設計模式 - 7

行為模式

因此,我們弄清楚瞭如何建立物件以及如何組織類別之間的連接。剩下的最有趣的事情是提供改變物件行為的靈活性。行為模式將幫助我們做到這一點。最常被提及的模式之一是「策略」模式。這就是《 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);
  }
}
在我們之前 - TreeSetTreeSet它具有維持元素順序的行為,即 對它們進行排序(因為它是一個 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
    }
  }
}
Java 中的設計模式 - 8

結論

正如我們從評論中看到的,模式多種多樣。他們每個人都解決自己的問題。了解這些模式可以幫助您隨時了解如何編寫系統,使其靈活、可維護且不易更改。最後,一些更深入研究的連結: #維亞切斯拉夫
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION