Mẫu Adapter
Thuộc phần Lập trình hướng đối tượng trong hành trình GO của Coddy. Bài học 92 trên 107.
Mẫu Adapter cho phép các giao diện không tương thích hoạt động cùng nhau bằng cách bọc một kiểu hiện có bằng giao diện mới. Trong khi Command đóng gói các hành động, Adapter chuyển đổi một giao diện sang giao diện khác mà phía sử dụng mong đợi.
Hãy tưởng tượng bạn có một máy in cũ dùng chữ ký phương thức khác với chữ ký mà ứng dụng của bạn mong đợi:
// Interface đích mà ứng dụng sử dụng
type Printer interface {
Print(text string) string
}
// Máy in cũ có interface không tương thích
type OldPrinter struct{}
func (o OldPrinter) PrintDocument(doc string, copies int) string {
return fmt.Sprintf("Old printer: %s (x%d)", doc, copies)
}Bộ chuyển đổi bao bọc kiểu cũ và triển khai giao diện được mong đợi:
type OldPrinterAdapter struct {
oldPrinter OldPrinter
}
func (a OldPrinterAdapter) Print(text string) string {
return a.oldPrinter.PrintDocument(text, 1)
}Giờ đây, máy in cũ hoạt động liền mạch với code mong đợi giao diện Printer:
func PrintMessage(p Printer, msg string) string {
return p.Print(msg)
}
adapter := OldPrinterAdapter{oldPrinter: OldPrinter{}}
result := PrintMessage(adapter, "Hello") // "Old printer: Hello (x1)"Mẫu Adapter rất hữu ích khi tích hợp thư viện bên thứ ba, làm việc với code cũ hoặc khi bạn cần dùng các lớp hiện có không đáp ứng yêu cầu về giao diện của mình. Mẫu này đóng vai trò như một cầu nối mà không cần sửa đổi code gốc.
Thử thách
DễHãy xây dựng một hệ thống phát media bằng mẫu Adapter! Bạn có một trình phát âm thanh hiện đại cần một giao diện thống nhất, nhưng cần tích hợp thiết bị âm thanh cũ sử dụng chữ ký phương thức hoàn toàn khác. Adapter sẽ kết nối hai bên, giúp phần cứng cũ hoạt động trơn tru với hệ thống mới.
Bạn sẽ sắp xếp code trong ba tệp:
player.go: Định nghĩa giao diện đích và một thiết bị âm thanh cũ.Tạo giao diện
AudioPlayermà hệ thống hiện đại cần, với phương thứcPlay(track string) stringnhận tên bản nhạc và trả về thông báo phát nhạc.Đồng thời, định nghĩa một thiết bị cũ không tương thích với giao diện này:
- Cấu trúc
VinylPlayercó trườngSpeedRPM(int). Cấu trúc này có phương thứcDropNeedle(record string, side string) stringtrả vềPlaying [record] ([side] side) at [speed]rpm
Giao diện của VinylPlayer không tương thích với AudioPlayer. Nó cần hai tham số và sử dụng tên phương thức hoàn toàn khác.
- Cấu trúc
adapter.go: Tạo adapter giúp thiết bị cũ tương thích.Tạo cấu trúc
VinylAdapterbọc mộtVinylPlayervà triển khai giao diệnAudioPlayer. Khi gọiPlay, adapter sẽ gọi phương thứcDropNeedlecủa trình phát đĩa than, dùngAlàm giá trị mặc định cho tham số mặt đĩa.Đồng thời, tạo một trình phát hiện đại để so sánh:
- Cấu trúc
DigitalPlayer: phương thứcPlaytrả vềStreaming: [track]
- Cấu trúc
main.go: Minh họa cách cả hai trình phát hoạt động thông qua cùng một giao diện.Tạo hàm
PlayMusic(player AudioPlayer, track string) stringsử dụng giao diện để phát nhạc. Hàm này không cần biết hay quan tâm liệu nó đang dùng trình phát kỹ thuật số hay trình phát đĩa than đã được chuyển đổi bằng adapter.Đọc loại trình phát (
digitalhoặcvinyl), và nếu là vinyl thì đọc thêm tốc độ RPM. Sau đó đọc tên bản nhạc. Tạo trình phát phù hợp, dùng adapter cho vinyl, phát bản nhạc thông qua hàmPlayMusicrồi in kết quả.
Đầu vào sẽ có dạng như sau:
- Dòng 1: Loại trình phát (
digitalhoặcvinyl) - Dòng 2: Tốc độ RPM (chỉ khi là vinyl)
- Dòng cuối: Tên bản nhạc cần phát
Ví dụ, với đầu vào:
digital
Bohemian RhapsodyKết quả đầu ra là:
Streaming: Bohemian RhapsodyVới đầu vào:
vinyl
33
Abbey RoadKết quả đầu ra là:
Playing Abbey Road (A side) at 33rpmVới đầu vào:
vinyl
45
Blue MondayKết quả đầu ra là:
Playing Blue Monday (A side) at 45rpmLưu ý rằng hàm PlayMusic hoạt động giống hệt với cả hai trình phát. Hàm không biết rằng trình phát đĩa than thực chất được chuyển đổi từ một giao diện hoàn toàn khác. Adapter âm thầm chuyển các lời gọi giao diện hiện đại sang định dạng cũ!
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
// PlayMusic dùng giao diện AudioPlayer để phát nhạc
// Hàm (function) này hoạt động với mọi cách triển khai AudioPlayer
// TODO: Triển khai hàm này để gọi player.Play(track) và trả về kết quả
func PlayMusic(player AudioPlayer, track string) string {
// TODO: Triển khai hàm này
return ""
}
func main() {
reader := bufio.NewReader(os.Stdin)
// Đọc loại trình phát
playerType, _ := reader.ReadString('\n')
playerType = strings.TrimSpace(playerType)
var player AudioPlayer
var track string
if playerType == "vinyl" {
// Đọc tốc độ RPM của đĩa than
rpmStr, _ := reader.ReadString('\n')
rpmStr = strings.TrimSpace(rpmStr)
rpm, _ := strconv.Atoi(rpmStr)
// Đọc tên bản nhạc
track, _ = reader.ReadString('\n')
track = strings.TrimSpace(track)
// TODO: Tạo VinylPlayer với RPM đã cho
// TODO: Bọc nó trong VinylAdapter
// TODO: Gán cho biến (variable) player
_ = rpm // Xóa dòng này khi bạn dùng rpm
} else {
// Đọc tên bản nhạc
track, _ = reader.ReadString('\n')
track = strings.TrimSpace(track)
// TODO: Tạo DigitalPlayer
// TODO: Gán cho biến (variable) player
}
// TODO: Gọi PlayMusic và in ra kết quả
_ = player // Xóa dòng này khi bạn dùng player
_ = track // Xóa dòng này khi bạn dùng track
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Nền tảng OOP trong Go
Tệp bên ngoàiWorkspace và module trong GoPackage và importTên được export và không được exportGiới thiệu về OOP trong GoStruct như lớpĐịnh nghĩa phương thức cho structReceiver kiểu con trỏ và kiểu giá trịKhởi tạo structHàm khởi tạoÔn tập: máy tính đơn giản4Interface
Giới thiệu về interfaceTriển khai ngầm địnhInterface như một hợp đồngInterface rỗng (any)Khẳng định kiểuswitch kiểuKết hợp interfaceInterface Stringer và ErrorÔn tập: Máy tính hình học7Tính đóng gói
Trường được xuất và không được xuấtĐóng gói ở cấp packagePhương thức getter và setterẨn thông tin trong GoÔn tập: hồ sơ học sinh10Generics (Go 1.18 trở lên)
Giới thiệu về GenericsTham số kiểuRàng buộc kiểuStruct tổng quátGiải pháp thay thế cho phương thức tổng quátÔn tập: tập hợp tổng quát13Các mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếMẫu SingletonMẫu FactoryMẫu Abstract FactoryMẫu ObserverMẫu Strategy2Tìm hiểu chuyên sâu về kiểu dữ liệu và struct
Kiểu cơ bản và kiểu tổng hợpĐịnh nghĩa kiểu dữ liệu tùy chỉnhThẻ structStruct ẩn danhStruct lồng nhauGiá trị zero và giá trị mặc địnhÔn tập: sổ liên lạc5Ưu tiên kết hợp hơn kế thừa
Vì sao Go không có kế thừaNhập môn nhúng structTự động nâng phương thứcNhúng nhiều structNhúng và kết tậpChe khuất phương thức được nhúngÔn tập: hệ phân cấp nhân viên8Xử lý lỗi và lập trình hướng đối tượng
Giao diện errorCác kiểu lỗi tùy chỉnhBọc lỗi với fmt.ErrorfLỗi sentinelerrors.Is() và errors.As()Panic, defer và recoverÔn tập: trình phân tích tệp11Thư viện chuẩn và OOP
io.Reader và io.Writersort.Interfaceinterface fmt.Stringerencoding/json với structinterface http.HandlerÔn tập: mô hình REST API14Mẫu thiết kế phần 2
Mẫu CommandMẫu AdapterMẫu DecoratorMẫu Template MethodMẫu StateMẫu CompositeMiddleware dưới dạng Decorator3Con trỏ và bộ nhớ
Cơ bản về con trỏ trong GoCon trỏ đến structTruyền theo giá trị và tham chiếuHàm new()Thu gom rác trong GoÔn tập: xây dựng danh sách liên kết6Đa hình trong Go
Đa hình qua interfaceĐịnh kiểu vịt trong GoQuy tắc thỏa mãn interfaceCác tập hợp đa hìnhTiêm phụ thuộcÔn tập: bộ xử lý thanh toán9Lập trình đồng thời và lập trình hướng đối tượng
Cơ bản về goroutineChannel và giao tiếpChannel có bộ đệm và không có bộ đệmCâu lệnh selectsync.Mutex và sync.RWMutexsync.WaitGroupThiết kế struct an toàn luồngÔn tập: worker poolTự luyện tập: Trình biên dịch Go online