掌握設計模式 -- 裝飾模式

裝飾模式(Decorator Pattern)

裝飾模式是一種結構型設計模式,旨在在不改變原有對象結構的情況下動態地爲對象添加功能。通過將對象封裝到一系列裝飾器類中,可以以靈活和透明的方式擴展功能。

如果要擴展功能,裝飾模式提供了比繼承更有彈性的替代方案,裝飾模式強調的是功能的擴展和靈活組合

裝飾模式強調的是擴展對象的功能擴展功能的組合。比如,對象 A,需要擴展 X、Y 的功能,這些擴展功能按不同的順序組合可以實現不同的效果,先執行 X 再執行 Y 擴展功能或者先執行 Y 再執行 X 擴展功能。而繼承實現的只是單一的順序擴展功能,並且繼承是單一的。看後面的例子就很好理解了。

結構

通常包含一個核心對象若干裝飾對象,這些裝飾對象通過引用同一接口或抽象類實現功能的疊加。

裝飾模式包含以下主要角色:

  1. Component(組件接口)
    定義核心對象的公共接口,允許動態添加行爲。

  2. ConcreteComponent(具體組件)
    具體實現了 Component 接口的類,表示被裝飾的核心對象。

  3. Decorator(裝飾器抽象類)
    實現 Component 接口,同時持有一個 Component 的引用,表示對該對象的包裝。

  4. ConcreteDecorator(具體裝飾器)
    在裝飾器抽象類的基礎上,添加具體的功能。

代碼示例

實現對Socket報文的多層加密,加密順序可隨意組合。示例中使用到了國密 SM4 加密和 Base64 編碼。Socket 報文可以先加密再編碼,也可以先編碼在加密,跟根據不同組合來實現不同的增強。

類圖

定義核心接口

public interface SocketStream {
    void writeData(String data) throws IOException;
    String readData() throws IOException;
    void close() throws IOException;
}

被裝飾的核心對象

public class SimpleSocketStream implements SocketStream {
    private Socket socket;
    private OutputStream outputStream;
    private InputStream inputStream;
    public SimpleSocketStream(Socket socket){
        this.socket = socket;
        try {
            this.outputStream = socket.getOutputStream();
            this.inputStream = socket.getInputStream();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public void writeData(String data) throws IOException {
        outputStream.write(data.getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        socket.shutdownOutput();
    }

    @Override
    public String readData() throws IOException {
        // 讀取客戶端請求並解密
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        String message = reader.readLine();
        return message;
    }

    @Override
    public void close() throws IOException {
        if(inputStream!=null)
            inputStream.close();
        if(outputStream!=null)
            outputStream.close();
        if(socket!=null)
            socket.close();
    }
}

抽象裝飾器

public abstract class SocketStreamDecorator implements SocketStream{
    private SocketStream socketStream;

    public SocketStreamDecorator(SocketStream socketStream){
        this.socketStream = socketStream;
    }

    @Override
    public void writeData(String data) throws IOException {
        socketStream.writeData(data);
    }

    @Override
    public String readData() throws IOException {
        return socketStream.readData();
    }

    @Override
    public void close() throws IOException {
        socketStream.close();
    }
}

具體裝飾器 1--SM4 加密處理

public class SM4CipherSocketStreamDecorator extends SocketStreamDecorator{

    public SM4CipherSocketStreamDecorator(SocketStream socketStream) {
        super(socketStream);
    }

    @Override
    public void writeData(String data) throws IOException {
        // 加密
        String sm4Encrypt = SM4EncryptUtil.sm4Encrypt(data);
        System.out.println("--SM4加密數據:"+sm4Encrypt);
        super.writeData(sm4Encrypt);
    }

    @Override
    public String readData() throws IOException {
        String readData = super.readData();
        // 解密
        System.out.println("--SM4解密前數據:"+readData);
        String sm4Decrypt = SM4EncryptUtil.sm4Decrypt(readData);
        return sm4Decrypt;
    }
}

工具類

public class SM4EncryptUtil {
    // 祕鑰
    private static final String key = "1234567812345678";

    static {
        // 添加安全提供者(SM2,SM3,SM4等加密算法,CBC、CFB等加密模式,PKCS7Padding等填充方式,不在Java標準庫中,由BouncyCastleProvider實現)
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * 輸入:待加密的字符串,16或24或32位字符串密碼
     * 輸出:16進制字符串或Base64編碼的字符串密文(常用)
     */
    public static String sm4Encrypt(String encrypt) {
        String cipherString = null;
        try {
            // 指定加密算法
            String algorithm = "SM4";
            // 創建密鑰規範
            SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), algorithm);
            // 獲取Cipher對象實例(BC中SM4默認使用ECB模式和PKCS5Padding填充方式,因此下列模式和填充方式無需指定)
            Cipher cipher = Cipher.getInstance(algorithm + "/ECB/PKCS5Padding");
            // 初始化Cipher爲加密模式
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
            // 獲取加密byte數組
            byte[] cipherBytes = cipher.doFinal(encrypt.getBytes(StandardCharsets.UTF_8));
            // 輸出爲Base64編碼
            cipherString = Base64.getEncoder().encodeToString(cipherBytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cipherString;
    }

    /**
     * 輸入:密文,16或24或32位字符串密碼
     * 輸出:明文
     */
    public static String sm4Decrypt(String cipherString) {
        String plainString = null;
        try {
            // 指定加密算法
            String algorithm = "SM4";
            // 創建密鑰規範
            SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), algorithm);
            // 獲取Cipher對象實例(BC中SM4默認使用ECB模式和PKCS5Padding填充方式,因此下列模式和填充方式無需指定)
            Cipher cipher = Cipher.getInstance(algorithm + "/ECB/PKCS5Padding");
            // 初始化Cipher爲解密模式
            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
            // 獲取加密byte數組
            byte[] cipherBytes = cipher.doFinal(Base64.getDecoder().decode(cipherString));
            // 輸出爲字符串
            plainString = new String(cipherBytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return plainString;
    }

}

具體裝飾器 2--Base64 編碼處理

public class Base64SocketStreamDecorator extends SocketStreamDecorator{

    public Base64SocketStreamDecorator(SocketStream socketStream) {
        super(socketStream);
    }

    @Override
    public void writeData(String data) throws IOException {
        String encode = this.encode(data);
        System.out.println("--base64編碼後的數據:" + encode);
        super.writeData(encode);
    }

    @Override
    public String readData() throws IOException {
        String readData = super.readData();
        System.out.println("--base64解密前的數據:" + readData);
        String decode = this.decode(readData);
        return decode;
    }
    /**
     * base64編碼
     */
    public String encode(String str) {
        if (str == null || str.isEmpty()) {
            return "";
        }
        // String 轉 byte[]
        byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
        // 編碼(base64字符串)
        return Base64.getEncoder().encodeToString(bytes);
    }

    /**
     * base64解碼
     */
    public String decode(String base64Str) {
        if (base64Str == null || base64Str.isEmpty()) {
            return "";
        }
        // 編碼
        byte[] base64Bytes = Base64.getDecoder().decode(base64Str);
        // byte[] 轉 String(解碼後的字符串)
        return new String(base64Bytes, StandardCharsets.UTF_8);
    }

}

測試代碼

測試 Socket 服務端代碼

public class SM4SocketServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("Server started...");

        while (true) {
            try (Socket socket = serverSocket.accept()) {
                System.out.println("Client connected...\n");

                // 加密數據流
                SocketStream socketStreamDecorator = new Base64SocketStreamDecorator(
                                                        new SM4CipherSocketStreamDecorator(
                                                          new SimpleSocketStream(socket)));
                // 讀取客戶端請求並解密
                String message = socketStreamDecorator.readData();
                System.out.println("服務端讀取數據: " + message);

                // 處理請求並加密響應
                String response = "我來自服務端!";
                System.out.println("服務端發送數據: " + response);
                socketStreamDecorator.writeData(response);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

測試 Socket 客戶端代碼

public class SM4SocketClient {

    public static void main(String[] args) throws IOException {
        // 連接到服務器
        try (Socket socket = new Socket("localhost", 9999)) {
            System.out.println("Connected to server...\n");

            // 讀取服務器的響應並解密
            SocketStream socketStreamDecorator = new Base64SocketStreamDecorator(
                                                    new SM4CipherSocketStreamDecorator(
                                                      new SimpleSocketStream(socket)));
            // 要發送的消息
            String message = "我來自客戶端!";
            System.out.println("客戶端發送數據: " + message);

            // 使用加密流將消息發送到服務器
            socketStreamDecorator.writeData(message);

            // 讀取客戶端請求並解密
            String readData = socketStreamDecorator.readData();
            System.out.println("客戶端讀取數據: " + readData);
            socketStreamDecorator.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

測試結果分析

先啓動 Socket 服務,再發起 Socket 請求。

客戶端輸出結果

Connected to server...

客戶端發送數據: 我來自客戶端!

--base64 編碼後的數據:5oiR5p2l6Ieq5a6i5oi356uv77yB

--SM4 加密數據:anTSZv18wcQOxQtA82w9ap2j8DjagsX9QqRVjzFCjWU=

--SM4 解密前數據:5azwyKP7MB6jSyG3eDlJmfOYx5y+woud2WgE2Jjf5lA=

--base64 解密前的數據:5oiR5p2l6Ieq5pyN5Yqh56uv77yB

客戶端讀取數據: 我來自服務端!

服務端輸出結果

Server started...

Client connected...

--SM4 解密前數據:anTSZv18wcQOxQtA82w9ap2j8DjagsX9QqRVjzFCjWU=

--base64 解密前的數據:5oiR5p2l6Ieq5a6i5oi356uv77yB

服務端讀取數據: 我來自客戶端!

服務端發送數據: 我來自服務端!

--base64 編碼後的數據:5oiR5p2l6Ieq5pyN5Yqh56uv77yB

--SM4 加密數據:5azwyKP7MB6jSyG3eDlJmfOYx5y+woud2WgE2Jjf5lA=

這行代碼 new 的先後順序決定代碼執行的先後順序:new Base64SocketStreamDecorator(new SM4CipherSocketStreamDecorator(new SimpleSocketStream(socket))); ,所以文中的案例測試執行順序如下:

數據流方向

代碼層面的調用順序

調整組合順序測試

如果哪天有需求要改爲先執行 SM4 加密,再執行 Base64 編碼,則代碼只需要修改爲new SM4CipherSocketStreamDecorator(new Base64SocketStreamDecorator(new SimpleSocketStream(socket)));

測試結果變爲:

客戶端輸出結果

客戶端發送數據: 我來自客戶端!

--SM4 加密數據:YoNCZhEm1shIjPWO5vyhsFRWq+XUdHPggFiE325GCKc=

--base64 編碼後的數據:WW9OQ1poRW0xc2hJalBXTzV2eWhzRlJXcStYVWRIUGdnRmlFMzI1R0NLYz0=

--base64 解密前的數據:OUJTQ0ZLUVFIa2dxSGs0WTd2enB1RlJXcStYVWRIUGdnRmlFMzI1R0NLYz0=

--SM4 解密前數據:9BSCFKQQHkgqHk4Y7vzpuFRWq+XUdHPggFiE325GCKc=

客戶端讀取數據: 我來自服務端!

服務端輸出結果變爲

--base64 解密前的數據:WW9OQ1poRW0xc2hJalBXTzV2eWhzRlJXcStYVWRIUGdnRmlFMzI1R0NLYz0=

--SM4 解密前數據:YoNCZhEm1shIjPWO5vyhsFRWq+XUdHPggFiE325GCKc=

服務端讀取數據: 我來自客戶端!

服務端發送數據: 我來自服務端!

--SM4 加密數據:9BSCFKQQHkgqHk4Y7vzpuFRWq+XUdHPggFiE325GCKc=

--base64 編碼後的數據:OUJTQ0ZLUVFIa2dxSGs0WTd2enB1RlJXcStYVWRIUGdnRmlFMzI1R0NLYz0=

裝飾模式的應用

Java 標準庫中的裝飾模式之一:IO 流體系

示例代碼

BufferedReader bufferedReader = new BufferedReader(
                                          new InputStreamReader(
                                            new FileInputStream(
                                              new File("test.txt"))));

逐步增強功能:IO 流的增強組合有順序要求

總結

裝飾模式是一種靈活的結構型模式,允許我們在不修改現有類的情況下,動態地添加功能。它尤其適合於功能可以按需組合疊加、擴展的場景,但需要權衡複雜性與性能開銷。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Rw4iTuor_3wi9AVUp7aUTA