外观模式
简介
外观模式(Facade Pattern)是一种通过为多个复杂的子系统提供一个一致的接口,使这些子系统更容易被访问的设计模式。外观模式对外有一个统一的接口,外部应用程序不用关心内部子系统的具体细节,从而大幅降低应用程序的复杂度,增强应用程序的可维护性。外观模式可以为复杂系统、程序库或框架提供一个简单的接口。
角色组成
- 外观(Facade):可以提供一种访问特定子系统功能的便捷方式,了解如何重定向客户端请求,知晓如何操作所有的活动部件。
- 子系统(Subsystem):有数十个不同的对象组成,如果要用这些对象完成有意义的工作,那么开发者必须深入了解子系统的实现细节,比如按照正确的顺序初始化对象和为其提供正确格式的数据。子系统不会意识到外观的存在,它们在系统内运作,并且相互之间可以直接进行交互
常见使用场景
- 封装复杂的第三方库: 当项目使用了多个功能强大但接口复杂的第三方库时,可以创建一个外观类来封装这些库,提供一套更符合项目需求的、简化的接口。例如,封装数据库操作、消息队列交互、或者调用多个外部 API 的逻辑。
- 简化微服务间的通信: 在微服务架构中,一个服务可能需要调用另一个服务的多个接口才能完成一个业务操作。可以在调用方服务中创建一个外观类,将对被调用服务多个接口的调用封装起来,对外提供一个单一的、业务导向的接口。
- 构建复杂的初始化过程: 一个模块或子系统的初始化可能涉及多个步骤,创建多个对象,设置它们之间的依赖关系等。可以使用外观模式提供一个简单的
Init()
方法,隐藏内部复杂的初始化流程。 - 提供跨层或跨模块的统一访问入口: 在一个分层的 Go 应用中,例如经典的三层架构(Web 层、Service 层、Data 层),Service 层可以作为 Data 层(可能包含多个 DAO 或 Repository)的外观,为 Web 层提供业务功能的统一入口,而不需要 Web 层直接与多个 Data 层对象交互。
- 简化配置加载和管理: 当系统的配置分散在多个文件、环境变量或配置中心时,可以创建一个配置外观类,提供简单的
GetConfig()
方法来获取各种配置项,隐藏底层复杂的配置读取和解析逻辑。 - 日志和监控的统一接口: 如果项目使用了不同的日志库或监控工具,可以创建一个日志或监控的外观类,提供统一的
Log()
或Metric()
方法,内部根据配置或需要调用不同的底层实现。 - 事务处理的封装: 在处理数据库事务时,可能涉及事务的开始、提交、回滚等多个步骤。可以创建一个事务外观,提供
ExecuteInTransaction()
方法,接受一个包含业务逻辑的函数作为参数,并在内部处理事务的整个生命周期。
优点
- 简化复杂系统: 外观模式最主要的优点是简化了客户端与复杂子系统之间的交互。客户端无需了解子系统内部的多个类和它们之间的复杂关系,只需要通过外观类就能调用所需的功能。
- 降低耦合度: 它将客户端与子系统解耦 。客户端依赖于外观类,而不是直接依赖于子系统中的具体类。这样,当子系统内部发生变化时(例如,增删改类),对客户端的影响可以降到最低。
- 提高系统的可维护性: 由于降低了耦合度,当需要修改或重构子系统时,可以更容易地进行,因为对外部系统的影响较小。
- 提高系统的可移植性: 当需要将子系统迁移到新的环境中时,由于外观模式提供了一个稳定的接口,可以更容易地进行移植。
- 更好的分层: 外观模式有助于对系统进行分层,将表示层与业务逻辑层分离,
缺点
- 不符合开闭原则(部分情况): 如果子系统需要扩展新的功能,并且这些新功能也需要通过外观暴露给客户端,那么可能需要修改外观类,这违反了开闭原则(对扩展开放,对修改关闭)。但是,可以通过在外观类中引入新的方法或者结合其他设计模式来缓解这个问题。
- 可能成为瓶颈: 如果外观类包含了过多的功能,或者承担了过多的职责,它可能会变得臃肿,难以维护,甚至成为系统的一个瓶颈。
- 隐藏了底层细节,可能限制灵活性: 虽然隐藏复杂性是优点,但在某些需要直接访问子系统深层功能的场景下,外观模式可能会显得过于 restrictive,不够灵活。
示例代码
Simple
Go
package facade
import "fmt"
type Facade struct {
subsystemA SubsystemA
subsystemB SubsystemB
}
func NewFacade() *Facade {
return &Facade{
subsystemA: SubsystemA{},
subsystemB: SubsystemB{},
}
}
func (f *Facade) MethodA() {
f.subsystemB.MethodThree()
f.subsystemA.MethodOne()
f.subsystemB.MethodFour()
}
func (f *Facade) MethodB() {
f.subsystemB.MethodFour()
f.subsystemA.MethodTwo()
}
type SubsystemA struct{}
type SubsystemB struct{}
func NewSubsystemA() *SubsystemA {
return &SubsystemA{}
}
func NewSubsystemB() *SubsystemB {
return &SubsystemB{}
}
func (s *SubsystemA) MethodOne() {
fmt.Println("SubsystemA.MethodOne")
}
func (s *SubsystemA) MethodTwo() {
fmt.Println("SubsystemA.MethodOne")
}
func (s *SubsystemB) MethodThree() {
fmt.Println("SubsystemB.MethodThree")
}
func (s *SubsystemB) MethodFour() {
fmt.Println("SubsystemB.MethodFour")
}
客户端
package facade
import "testing"
func TestSimple(t *testing.T) {
fa := NewFacade()
fa.MethodA()
fa.MethodB()
}
执行输出
SubsystemB.MethodThree
SubsystemA.MethodOne
SubsystemB.MethodFour
SubsystemB.MethodFour
SubsystemA.MethodOne
Wallet
Go
package facade
import "fmt"
type WalletFacade struct {
Account *Account
Wallet *Wallet
VerificationCode *VerificationCode
Notification *Notification
Ledger *Ledger
}
func NewWalletFacade(accountID string, code int) *WalletFacade {
return &WalletFacade{
Account: NewAccount(accountID),
VerificationCode: NewVerificationCode(code),
Wallet: NewWallet(),
Notification: NewNotification(),
Ledger: NewLedger(),
}
}
func (w *WalletFacade) AddMoneyToWallet(accountID string, securityCode int, amount int) error {
fmt.Println("Starting add money process")
err := w.Account.CheckAccount(accountID)
if err != nil {
return err
}
err = w.VerificationCode.CheckCode(securityCode)
if err != nil {
return err
}
w.Wallet.AddBalance(amount)
w.Notification.SendWalletCreditNotification()
w.Ledger.MakeEntry(accountID, "credit", amount)
return nil
}
func (w *WalletFacade) DeductMoneyFromWallet(accountID string, securityCode int, amount int) error {
fmt.Println("Starting debit money process")
err := w.Account.CheckAccount(accountID)
if err != nil {
return err
}
err = w.VerificationCode.CheckCode(securityCode)
if err != nil {
return err
}
err = w.Wallet.DebitBalance(amount)
if err != nil {
return err
}
w.Notification.SendWalletDebitNotification()
w.Ledger.MakeEntry(accountID, "debit", amount)
return nil
}
// Subsystem
type Account struct {
name string
}
func NewAccount(name string) *Account {
return &Account{
name: name,
}
}
func (a *Account) CheckAccount(accountName string) error {
if a.name != accountName {
return fmt.Errorf("invalid account name")
}
fmt.Println("Account verified")
return nil
}
// Subsystem
type VerificationCode struct {
code int
}
func NewVerificationCode(code int) *VerificationCode {
return &VerificationCode{
code: code,
}
}
func (v *VerificationCode) CheckCode(incomingCode int) error {
if v.code != incomingCode {
return fmt.Errorf("invalid code")
}
fmt.Println("Verification code is correct")
return nil
}
// subsystem
type Wallet struct {
balance int
}
func NewWallet() *Wallet {
return &Wallet{}
}
func (w *Wallet) AddBalance(amount int) {
w.balance += amount
fmt.Println("Wallet balance updated")
}
func (w *Wallet) DebitBalance(amount int) error {
if w.balance < amount {
return fmt.Errorf("insufficient funds")
}
w.balance -= amount
fmt.Println("Wallet balance updated")
return nil
}
// subsystem
type Ledger struct{}
func NewLedger() *Ledger {
return &Ledger{}
}
func (l *Ledger) MakeEntry(accountID, txnType string, amount int) {
fmt.Printf("Ledger entry made for accountID: %s with txnType: %s and amount: %d\n", accountID, txnType, amount)
}
// subsystem
type Notification struct{}
func NewNotification() *Notification {
return &Notification{}
}
func (n *Notification) SendWalletCreditNotification() {
fmt.Println("Wallet credit notification sent")
}
func (n *Notification) SendWalletDebitNotification() {
fmt.Println("Wallet debit notification sent")
}
client
package facade
import (
"testing"
)
func TestWallet(t *testing.T) {
wf := NewWalletFacade("Chroma", 1688)
err := wf.AddMoneyToWallet("Chroma", 1688, 100) // 信用卡中添加16元
if err != nil {
t.Error(err)
}
err = wf.DeductMoneyFromWallet("Chroma", 1688, 50)
if err != nil {
t.Error(err)
}
}