Design Pattern 筆記 - Adapter Pattern

Post Directory

前言

上次文章直接丟到 Golang FB 社團果然瀏覽數飆高(FB 資訊圈朋友太少之前自己 PO 文也都是自嗨 (つд⊂))

不過我自己閉門造車果然也出現很多問題,剛貼過去社團就有人發現 interface 完全沒作用,最後也發現註解跟說明實在太少了

看來筆記的整理技術要在多下點功夫了,那接下來來一起看看新的 Pattern 吧

Adapter Pattern

試想一下,當你想要直流電的電器用品(規格A),在交流電 110V(規格B)的供電下運作,通常都需要接上交流配接器(AC adapter),才能使我們的小家電也能在 110V 或 220V 下運作

Adapter Pattern 也被稱為 Wrapper Pattern,替我們將 A 包裝成 B 可以使用的形狀

然後又可以分成下面兩種:

下面範例為:

輸入一段文字(Hello),程式會呼叫兩個 func 來印出兩行文字

(Hello)
*Hello*

class Banner(規格B),分別有 ShowWithParen()ShowWithAster() 兩 func

Print interface(規格A) 也定義了兩個 func PrintWeek()PrintStrong(),想要直接接上 class Banner 的兩種 func

一樣的本書使用的是 java 當範例,所以我一起附上了

Object Adapter

Class Diagram

Adapter Pattern(Object)

name descreption
Print Print class(規格A)
PrintBanner extends Print(Adapter)
Banner Banner class(規格B)

範例一

java:

public class Banner {
  private String string
  public Banner(Strign string) {
    this.string = string;
  }
  public void showWithParen() {
    System.out.println("(" + string + ")")
  }
  public void showWithAster() {
    System.out.println("*" + string + "*")
  }
}

go:

type Banner struct {
	bannerString string
}

func (b *Banner) Banner(s string) {
	b.bannerString = s
}

func (b *Banner) ShowWithParen() {
	fmt.Printf("(%v)\n", b.bannerString)
}

func (b *Banner) ShowWithAster() {
	fmt.Printf("*%v*\n", b.bannerString)
}

Print

java:

public abstract class Print {
  public abstract void printWeek();
  public abstract void printStrong();
}

go:

這邊的 Print class 是定義好需要的方法後再讓 PrintBanner 繼承,因為 golang 沒繼承,所以這邊就沒做了

PrintBanner

java:

public class PrintBanner extends Print {
  private Banner banner;
  public PrintBanner(String string) {
    this.banner = new Banner(string)
  }
  public void printWeek() {
    banner.showWithParen()
  }
  public void printStrong() {
    banner.showWithAster()
  }
}

go:

// PrintBanner 繼承 Print
// 因為 golang 中並沒有繼承,所以這邊使用 Embedding struct 來實作
type PrintBanner struct {
	banner.Banner
}

func (b *PrintBanner) PrintBanner(s string) {
	b.Banner.Banner(s)
}

func (b *PrintBanner) PrintWeek() {
	b.Banner.ShowWithParen()
}

func (b *PrintBanner) PrintStrong() {
	b.Banner.ShowWithAster()
}

Main

java:

public class Main {
  public static void main(String[] args) {
    Print p = new PrintBanner("Hello")
    p.PrintWeek()
    p.PrintStrong()
  }
}

go:

func main() {
	pb := &model.PrintBanner{}
	pb.PrintBanner("Hello")
	// 透過 PrintBanner(adapter) 可以直接使用 Banner(規格 B)的方法
	pb.PrintWeek()
	pb.PrintStrong()
}

Class Adapter

Class Diagram

Adapter Pattern(Class)

name descreption
Print Print interface(規格A)
PrintBanner Print instance, extends Banner(Adapter)
Banner Banner class(規格B)

golang 沒有繼承,所以這邊就不實作了,直接看看 java code 來感受一下兩者的差別吧

範例二

java:

public class Banner {
  private String string
  public Banner(Strign string) {
    this.string = string;
  }
  public void showWithParen() {
    System.out.println("(" + string + ")")
  }
  public void showWithAster() {
    System.out.println("*" + string + "*")
  }
}

java:

public abstract interface Print {
  public abstract void printWeek();
  public abstract void printStrong();
}

PrintBanner

java:

public class PrintBanner extends Banner implements Print {
  public PrintBanner(String string) {
    super(string)
  }
  public void printWeek() {
    showWithParen()
  }
  public void printStrong() {
    showWithAster()
  }
}

Main

java:

public class Main {
  public static void main(String[] args) {
    Print p = new PrintBanner("Hello")
    p.PrintWeek()
    p.PrintStrong()
  }
}

Adapter 中登場的角色

- Target(對象)

定義著所需要的方法(即最上面所提到的電器用品需要的直流電電源),在範例中為 Print interface 和 Print struct 

- Client(請求者)

即負責使用 Target 實現的功能(直流電的電器,如筆電),在範例中為 Main

- Adaptee(被 adapter)

為既有的方法(原本就有的交流電),在範例中為 Banner struct
而如果 Adaptee 的方法與 Target 相同(即原本的插孔就是直流電了),這樣就不需要 Adapter 角色了

- Adapter

滿足 Target 的需求(讓交流電插座可以被直流電電器所使用),在範例中為 PrintBanner 

So Why?

使用時機

也許你會這樣想,為何不直接呼叫需要的方法就好,而是透過 adapter 替我們去呼叫呢?

這邊書中解釋為

- Adapter Pattern 會對現有的 class(Adaptee) 配接(adapter)成新的 class 和方法,而現有的 class 也許已經做過充分的 test 且 bug 很少,所以當 Bug 出現時,直椄找 Adapter 就好

- 有些時候 Adaptee 只有大部分符合 Target 需求,但還是有寫需要修修改改的地方,如果直接從 Adaptee 修改,也許原本的 Adaptee 測試也要一起更動到,而使用 Adapter Pattern 就可以不需要更動到 Adaptee 的 code 了

- 新舊版本兼容問題,假設今天我們只想維護新版,這時我們可以讓新版本擔任 Adaptee 角色,叫舊版本擔任 Target,讓它使用新版本的 class 來實現舊版本的 class 中的方法 Adapter Pattern(sample)

練習題

“github.com/magiconair/properties” 可以用來管理 key-value 集合(書中是用 java.util.Properties,所以我也上網找了個 golang 版本的來)

year=2019
month=1
day=13

請使用 Adapter Pattern 將 input.txt 中的 key-value 集合保存至文件中的 FileProperties class,FileProperties 為 FileIO(Target 角色) 的 implementation,然後經過一些修改後存成 newfile.txt

FileIO:

type FileIO interface {
	ReadFile(fileName string)
	WriteToFile(fileName string)
	SetVale(key, value string)
	GetValue(key string)
}

main:

func main() {
	var fp model.FileIO = &model.FileProperties{}
	fp.ReadFile("./input.txt")
	fp.SetVale("year", "2019")
	fp.SetVale("month", "1")
	fp.SetVale("day", "13")
	fp.WriteToFile("newfile.txt")
}

input.txt

year=1999

newfile.txt

# written by FileProperties
# Sun, 13 Jan 2019 17:03:09 CST1999
day=13
year=2019
month=1

解答

https://github.com/VagrantPi/golang-design-pattern/tree/master/2.adapter/practice

基本上 Target 可以在完全不知道 “github.com/magiconair/properties” 的方法的情況下使用它,因為已經被我們包裝成 FileIO 了

結語

文中的 Source Code 都放在 github 了,自己 clone 下來玩玩吧

還好上一篇沒說要每週一篇文章 (つд⊂),書越看就發現要學的東西還很多

Adapter Pattern 我覺得用法比較常見,之前把 aws-sdk-go 的上傳 s3 功能包裝簡單的方法 ,讓同事不用清楚知道 aws-sdk-go 的情況下簡單操作(這概念也算是吧? XD)

感謝 FB Golang 社團一些大大的建議,沒想到有人認真看找到了些問題 QQ,之後應該也會找時間把我 hackmd 上的技術筆記慢慢的變成 blog 的文章貼過來吧,不過想想又好懶

系列文章

Tweet