(C#)裝飾者模式(Decorator Pattern)

裝飾者模式是一個很精美且優雅的模式

本篇範例 文字內容->AES加密->Zip檔附加密碼->輸出儲存

情境
有個需求要做

  • 文字內容->壓縮zip(附上密碼)->輸出儲存
    又改成…

  • 文字內容->AES加密->輸出儲存
    需求又改成….

  • 文字內容->AES加密->Zip檔附加密碼->輸出儲存

可發現需求一直在對於文字內容操作順序做變化,但他們核心離不開對於文字內容的操作

這種情境很適合來使用 [裝飾者模式]

裝飾者模式 有兩個主要角色 被裝飾物件(Decorated) & 裝飾物件(Decorator)

  1. 被裝飾物件(Decorated)就像蛋糕的一樣,
  2. 裝飾物件(Decorator)就是上的水果,奶油,巧克力…等等裝飾物品

一般先有蛋糕被裝飾物件(Decorated),後再將裝飾物品加上去裝飾物件(Decorator)

被裝飾物件(Decorated)如下圖 蛋糕的原型

圖片來源

  • 將物件有效的往上附加職責,不動到內部的程式碼, 在原來職責上附加額外的職責
  • 裝飾者模式運作就像 俄羅斯娃娃一樣 一層包一層

https://dotblogsfile.blob.core.windows.net/user/%E4%B9%9D%E6%A1%83/4bc56f03-a1e1-4504-bea3-7cb02a8aaa21/1522826815_28596.jpg

圖片來源


第一步 先找尋他們共同裝飾東西,因為是讀寫檔案 所以我們可以對於Byte下手

先做出 介面簽章當作裝飾動作的統一介面

1
2
3
4
5
6
public interface IProcess
{
byte[] Read(string path);

void Write(string writePath, byte[] buffer);
}

在創建一個 ProcessBase 給日後裝飾物品(Decorator)來繼承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class ProcessBase : IProcess
{
/// <summary>
/// 儲存被裝飾的物件
/// </summary>
protected IProcess _process;

public abstract byte[] Read(string path);

public abstract void Write(string writePath, byte[] buffer);

public virtual void SetDecorated(IProcess process)
{
_process = process;
}
}

有兩點特別說明

  1. protected IProcess _process; 儲存被裝飾的物件由 SetDecorated 方法來設置被裝飾的物件
  2. 就像俄羅斯娃娃只包裹一個娃娃,不管被包裹娃娃之前包含哪些娃娃

第二步 創建被裝飾物品(Decorated)

因為是檔案我們直接使用

  • File.ReadAllBytes 讀 檔案
  • File.WriteAllBytes 寫 檔案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// <summary>
/// 讀取檔案
/// </summary>
public class FileProcess : IProcess
{
public byte[] Read(string path)
{
return File.ReadAllBytes(path);
}

public void Write(string writePath, byte[] buffer)
{
File.WriteAllBytes(writePath, buffer);
}
}

第三步 創建裝飾物品(Decorator)

這次主要裝飾物品有兩個

  1. 加壓解壓ZIP檔
  2. 加解密

加密裝飾器繼承ProcessBase並按照加解密重寫 Writeread 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/// <summary>
/// Aes 加密裝飾器
/// </summary>
public class AESCrypProcess : ProcessBase
{
private AesCryptoServiceProvider aes;

public string AESKey { get; set; } = "1776D8E110124E75";
public string AESIV { get; set; } = "B890E7F6BA01C273";

public AESCrypProcess()
{
aes = new AesCryptoServiceProvider();
aes.Key = Encoding.UTF8.GetBytes(AESKey);
aes.IV = Encoding.UTF8.GetBytes(AESIV);
}

public override byte[] Read(string path)
{
byte[] encryptBytes = _process.Read(path);
return DecryptData(encryptBytes);
}

/// <summary>
/// 進行解密
/// </summary>
/// <param name="encryptBytes"></param>
/// <returns></returns>
private byte[] DecryptData(byte[] encryptBytes)
{
byte[] outputBytes = null;
using (MemoryStream memoryStream = new MemoryStream(encryptBytes))
{
using (CryptoStream decryptStream = new CryptoStream(memoryStream, aes.CreateDecryptor(), CryptoStreamMode.Read))
{
MemoryStream outputStream = new MemoryStream();
decryptStream.CopyTo(outputStream);
outputBytes = outputStream.ToArray();
}
}
return outputBytes;
}

/// <summary>
/// 裝飾者呼叫方法
/// </summary>
/// <param name="path"></param>
/// <param name="data"></param>
public override void Write(string path, byte[] data)
{
byte[] outputBytes = EncryptData(data);
_process.Write(path, outputBytes);
}

private byte[] EncryptData(byte[] data)
{
byte[] outputBytes = null;
using (MemoryStream memoryStream = new MemoryStream())
{
using (CryptoStream encryptStream = new CryptoStream(memoryStream, aes.CreateEncryptor(), CryptoStreamMode.Write))
{
MemoryStream inputStream = new MemoryStream(data);
inputStream.CopyTo(encryptStream);
encryptStream.FlushFinalBlock();
outputBytes = memoryStream.ToArray();
}
}

return outputBytes;
}
}

這次讀寫zip使用 SharpZipLib 開源第三方插件

ZIP裝飾器繼承ProcessBase並按照加解密重寫 Writeread 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

/// <summary>
/// 讀取Zip使用
/// </summary>
public class ZipProcess : ProcessBase
{
public string PassWord { get; set; }
public string FileName { get; set; }

public override byte[] Read(string path)
{
byte[] buffer = _process.Read(path);
return ZipReader(path, buffer);
}

public override void Write(string writePath, byte[] data)
{
byte[] buffer = ZipWriter(data);
_process.Write(writePath, buffer);
}

private byte[] ZipWriter(byte[] buffer)
{
using (MemoryStream outputMemStream = new MemoryStream())
using (ZipOutputStream zipStream = new ZipOutputStream(outputMemStream))
using (MemoryStream memStreamIn = new MemoryStream(buffer))
{
zipStream.SetLevel(9);

ZipEntry newEntry = new ZipEntry(FileName);
newEntry.DateTime = DateTime.Now;
zipStream.Password = PassWord;
zipStream.PutNextEntry(newEntry);

StreamUtils.Copy(memStreamIn, zipStream, new byte[4096]);//將zip流搬到memoryStream中
zipStream.CloseEntry();

zipStream.IsStreamOwner = false;
zipStream.Close();

return outputMemStream.ToArray();
}
}

/// <summary>
/// 讀取zip檔
/// </summary>
/// <param name="buffer">zip檔案byte</param>
/// <returns></returns>
private byte[] ZipReader(string filePath, byte[] buffer)
{
byte[] zipBuffer = default(byte[]);
using (MemoryStream memoryStream = new MemoryStream(buffer))
{
memoryStream.Seek(0, SeekOrigin.Begin);
var zip = new ZipFile(memoryStream);
zip.Password = PassWord;
using (MemoryStream streamWriter = new MemoryStream())
{
byte[] bufferReader = new byte[4096];
var file = zip.GetEntry(FileName); //設置要去得的檔名
//如果有檔案
if (file != null)
{
var zipStream = zip.GetInputStream(file);
StreamUtils.Copy(zipStream, streamWriter, bufferReader);
zipBuffer = streamWriter.ToArray();
}
}
}
return zipBuffer;
}
}

上面就把我們要用的裝飾物品 (備料) 準備完成


第四步 創建使用(開始擺盤)

創建一個 DecorateFactory 來當生產 裝飾產品的工廠

建構子傳入一個 被裝飾的物件(FileProcess) 之後可依照喜好一直疊加 裝飾物品(ZipProcess,AESCrypProcess…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DecorateFactory
{
IProcess _original;

public DecorateFactory(IProcess original)
{
_original = original;
}

public DecorateFactory SetProcess(ProcessBase process)
{
process.SetDecorated(_original);
_original = process;
return this;
}

public IProcess GetProcess()
{
return _original;
}
}

裝飾者模式 裝飾的順序是很重要的

為了方便讀者閱讀 我使用小畫家畫出 讀寫順序

如下圖

使用就可很清晰來用

DecorateFactory來創建裝飾流程
factroy.GetProcess(); 方法取得完成後的產品

在簡單呼叫方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
string filePath = @"C:\Users\daniel.shih\Desktop\test.zip";
string content = $"你好 123456 12@()!@ {Environment.NewLine} fsfd嘻嘻哈哈!!";

//設置初始化的被裝飾者
DecorateFactory factroy = new DecorateFactory(new FileProcess());

//設置裝飾的順序
factroy.SetProcess(new AESCrypProcess())
.SetProcess(new ZipProcess() { FileName = "1.txt",PassWord ="1234567"});

IProcess process = factroy.GetProcess();

byte[] data_buffer = Encoding.UTF8.GetBytes(content);
process.Write(filePath, data_buffer);

byte[] buffer = process.Read(filePath);
Console.WriteLine(Encoding.UTF8.GetString(buffer));

日後不管需求是改成

  • 文字內容->壓縮zip(附上密碼)->輸出儲存
  • 文字內容->AES加密->輸出儲存
  • 文字內容->AES加密->Zip檔附加密碼->輸出儲存

還是…..

我們都不怕因為我們把各種操作封裝多態

各個模組間都是獨立的很好映證 高內聚低耦合 的設計原則

小結:

裝飾者模式是一個很精美且優雅的模式 希望這篇文章可讓讀者對於此模式有更加了解

GitHub範例連結

此文作者:Daniel Shih(石頭)
此文地址https://isdaniel.github.io/decorator-pattern/
版權聲明:本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 TW 許可協議。轉載請註明出處!


如果本文對您幫助很大,可街口支付斗內鼓勵石頭^^