go简约之道:组合

Go 语言之父 Rob Pike 曾说过:如果 C++ 和 Java 是关于类型层次结构和类型分类的语言,那么 Go 则是关于组合的语言。如果把 Go 应用程序比作是一台机器的话,那么组合关注的就是如何将散落在各个“个体”关联并组装到一起。可以说,组合是 Go 语言的重要设计哲学之一,想要掌握go语言必须了解其思想。

思考点:组合优于继承,但不代表继承永不适用

灵魂拷问:什么是组合

背过八股文的同学可能会脱口而出:组合又叫复合,用来表示个体与组成部分之间的关联关系,例如学生与心脏之间存在复合关系。甚至还能说出组合与聚合的区别:聚合的成员可独立,而组合的成员必须依赖于整体才有意义。

但,此组合非彼组合!

在go里的组合虽然也代表关联关系,但并不是参与组合的个体并不依赖整体,他们是独立正交的,个人认为与uml中的聚合一致

一切皆组合

在计算机技术中,正交性用于表示某种不相依赖性或是解耦性。如果两个或更多事物中的一个发生变化,不会影响其他事物,那么这些事物就是正交的。比如,在设计良好的系统中,数据库代码与用户界面是正交的:你可以改动界面,而不影响数据库;更换数据库,而不用改动界面。

Go 语言就为广大 Gopher 提供了诸多正交的语法元素供后续组合使用,包括:

  • Go 语言无类型体系(Type Hierarchy),没有父子类的概念,类型定义是正交独立的;
  • 方法和类型是正交的,每种类型都可以拥有自己的方法集合,方法本质上只是一个将 receiver 参数作为第一个参数的函数而已;
  • 接口与它的实现者之间无“显式关联”,也就说接口与 Go 语言其他部分也是正交的。

我们先来了解组合的方式,构建 Go 应用程序的静态骨架结构有两种主要的组合方式:

垂直组合

垂直组合是将多个类型(如上图中的 T1、I1 等)通过“类型嵌入(Type Embedding)”的方式实现新类型(如 NT1)的定义。

type T1 struct {
}

func (T1) M1() {
}

type T2 struct {
}

func (T2) M2() {
}

type NT1 struct {
	T1
	T2
}
// 使用T1的能力
func (n NT1) M()  {
	n.M1()
}

传统面向对象编程语言(比如:Java)大多是通过继承的方式建构出自己的类型体系的,但 Go 语言并没有类型体系的概念。Go 语言通过类型的组合而不是继承让单一类型承载更多的功能。

又因为不是继承,那么通过垂直组合定义的新类型与被嵌入的类型之间就没有所谓“父子关系”的概念了,也没有向上、向下转型(Type Casting),被嵌入的类型也不知道将其嵌入的外部类型的存在。调用方法时,方法的匹配取决于方法名字,而不是类型。这样的垂直组合更多应用在新类型的定义方面。通过这种垂直组合,我们可以达到方法实现的复用、接口定义重用等目的。

给我们的启发

尽量定义“小接口”“小结构体”

  • 接口越小,抽象程度越高
  • 小接口易于实现和测试,小结构体易于测试
  • 小接口和结构体表示的“契约”职责单一,易于复用组合

如何实现

  • 首先,别管大小,先抽象出接口或结构体。初期,我们先不要介意这个接口集合中方法的数量,因为对问题域的理解是循序渐进的,在第一版代码中直接定义出小接口可能并不现实。
// AfterSaleEmailBody
// @Description: 客户售后订单邮件
type AfterSaleEmailBody struct {
	OrderId                       *string          `json:"orderId"`
	Url                           *string          `json:"Url"`                // 订单状态页
	PaymentMethod                 *string          `json:"paymentMethod"`                 // 付款方式,当存在多笔支付单时,采用逗号隔开
	MessageWithCurrency           *string          `json:"messageWithCurrency"`           // 带货币符号的格式体
	MessageWithoutCurrency        *string          `json:"messageWithoutCurrency"`        // 不带货币符号的格式体
	CurrencySymbol                *string          `json:"currencySymbol"`                // 货币符号
	CurrencyCode                  *string          `json:"currencyCode"`                  // 货币代码
	CurrencyName                  *string          `json:"currencyName"`                  // 货币名称
	CurrencyNumber                *string          `json:"currencyNumber"`                // 货币数字代码
	StoreEmail                    *string          `json:"storeEmail"`                    // 客服邮箱
    
	AfterSaleOrderId             *string          `json:"afterSaleOrderId"`
	RefundAmount                  *decimal.Decimal `json:"refundAmount"`     // 退款金额
	RefundTime                    *int64           `json:"refundTime"`       // 退款时间
	RefundMethod                  *string          `json:"refundMethod"`     // 退款方式
	RefundFailReason              *string          `json:"refundFailReason"` // 退款失败原因
}
  • 第二,将大接口(结构)拆分为小接口(结构)。使用一段时间后,我们就来分析哪些场合使用了接口/结构的哪些方法/字段,是否可以将这些公共部分提取出来
// BaseEmailBody
// @Description: 通用邮件内容
type BaseEmailBody struct {
	OrderId                       *string `json:"orderId"`
	Url                           *string `json:"Url"`                // 订单状态页
	PaymentMethod                 *string `json:"paymentMethod"`                 // 付款方式,当存在多笔支付单时,采用逗号隔开
	MessageWithCurrency           *string `json:"messageWithCurrency"`           // 带货币符号的格式体
	MessageWithoutCurrency        *string `json:"messageWithoutCurrency"`        // 不带货币符号的格式体
	CurrencySymbol                *string `json:"currencySymbol"`                // 货币符号
	CurrencyCode                  *string `json:"currencyCode"`                  // 货币代码
	CurrencyName                  *string `json:"currencyName"`                  // 货币名称
	CurrencyNumber                *string `json:"currencyNumber"`                // 货币数字代码
	StoreEmail                    *string `json:"storeEmail"`                    // 客服邮箱
	CustomerName                  *string `json:"customerName"`
}


// AfterSaleEmailBody
// @Description: 客户售后订单邮件
type AfterSaleEmailBody struct {
	BaseEmailBody
	RefundAmount      *decimal.Decimal `json:"refundAmount"`     // 退款金额
	RefundTime        *int64           `json:"refundTime"`       // 退款时间
	RefundMethod      *string          `json:"refundMethod"`     // 退款方式(原路退、标记退)
	RefundFailReason  *string          `json:"refundFailReason"` // 退款失败原因
}
  • 最后,我们要注意接口/结构体的单一职责

思考点:拆不是目的,复用才是

水平组合

当我们通过垂直组合将一个个类型建立完毕后,又如何将他们连接起来呢?我们通过一个例子来看一下:消息发送功能

见代码

需求:不同渠道的发送消息不同实现

利用接口进行抽象

package com

type Test11 struct {
}

// 实现通用方法
func (t Test11) sendMessage(data []byte) {

}


type IMessage interface {
	sendMessage(data []byte)
}

// Email
// @Description: 发送邮件
type Email struct {
	Test11
}

// 实现接口方法
func (Email) SendMessage(data []byte) {

}

type Facebook struct {
	Test11
}

// 实现接口方法
func (Facebook) SendMessage(data []byte) {

}

func TestSendMessage1(t *testing.T) {
	m := map[string]com.IMessage{
		"email":    com.Email{},
		"facebook": com.Facebook{},
	}
	s := []byte("email")
	m["email"].SendMessage(s)
}

 需求:不同类型处理个性字段,共性字段统一处理

package com

type EmailBody struct {
	OrderId string
	StoreId  string
}

// 退款邮件内容体
type RefundEmailBody struct {
	EmailBody
	AfterSaleId string
}

func (r *RefundEmailBody) set() {
}

// 订单确认邮件内容体
type OrderConfirmEmailBody struct {
	Url string
}

// 抽象接口
type IMessageBody interface {
    // 公共行为
	Public()
    // 私有行为
	Private()
}

// 实例化具体类,目的是封装其行为
func NewMessageBody[T IMessageBody]() T {
	var t T
	v := reflect.ValueOf(&t).Elem()
	v.Set(reflect.New(v.Type().Elem()))
	t.Public()
	t.Private()
	return t
}

func (e *EmailBody) Public() {
	e.OrderId = "test"
}

func (r *RefundEmailBody) Private() {
	r.AfterSaleId = "after"
}

func (o *OrderConfirmEmailBody) Private() {
	o.Url = "https://"
}

func Concrete[T IMessageBody]() func() IMessageBody {
	return func() IMessageBody {
		return NewMessageBody[T]()
	}
}

func TestSendMessage3(t *testing.T) {
	m := map[string]func() IMessageBody{
		"Refund":  Concrete[*RefundEmailBody](),
		"Confirm": Concrete[*OrderConfirmEmailBody](),
	}
	body := m["Refund"]()
	t.Log(reflect.TypeOf(body))
	bytes, _ := json.Marshal(body)
	t.Log(string(bytes))
}

需求:提供不同的字段处理器,灵活组装使用

package com_test

import (
	"encoding/json"
	"testing"
)

type Body struct {
	OrderId string
	StoreId  string
	Amount   int64
	Url      string
}

func (b *Body) Set(fn BodySetFunc) {
	fn(b)
}

type BodySetFunc func(*Body)

type IBody interface {
	Set(BodySetFunc)
}

// 订单id设置器
type OrderIdSet struct {
	i IBody
}

func (o OrderIdSet) Set(fn BodySetFunc) {
	o.i.Set(func(body *Body) {
		body.OrderId = "test"
		fn(body)
	})
}

// 金额设置器
type AmountBodySet struct {
	i IBody
}

func (a AmountBodySet) Set(fn BodySetFunc) {
	a.i.Set(func(body *Body) {
		body.Amount = int64(100)
		fn(body)
	})
}

func TestBuild(t *testing.T) {
	body := Body{}
	var i IBody = &body
    // 包装订单id设置器
	i = OrderIdSet{i}
    // 再次包装金额设置器
	i = AmountBodySet{i}
    // 通用参数设置
	i.Set(func(body *Body) {

	})
	bytes, _ := json.Marshal(body)
	t.Log(string(bytes))
}

接口:go最强大的魔法

接口作为 Go 语言提供的具有天然正交性的语法元素,鸭子类型使得接口完全解藕,将接口仅仅定义为描述行为的集合,成为衔接各模块的关节

哪些场景适合使用接口?

创建模式

Go 社区流传一个经验法则:“接受接口,返回结构体(Accept interfaces, return structs)”,这其实就是一种把接口作为“关节”的应用模式。

// $GOROOT/src/sync/cond.go
type Cond struct {
    ... ...
    L Locker
}

func NewCond(l Locker) *Cond {
    return &Cond{L: l}
}

// $GOROOT/src/log/log.go
type Logger struct {
    mu     sync.Mutex 
    prefix string     
    flag   int        
    out    io.Writer  
    buf    []byte    
}

func New(out io.Writer, prefix string, flag int) *Logger {
    return &Logger{out: out, prefix: prefix, flag: flag}
}

// $GOROOT/src/log/log.go
type Writer struct {
    err error
    buf []byte
    n   int
    wr  io.Writer
}

func NewWriterSize(w io.Writer, size int) *Writer {
    // Is it already a Writer?
    b, ok := w.(*Writer)
    if ok && len(b.buf) >= size {
        return b
    }
    if size <= 0 {
        size = defaultBufSize
    }
    return &Writer{
        buf: make([]byte, size),
        wr:  w,
    }
}

包装器模式

参考:需求:提供不同的字段处理器,灵活组装使用

适配器模式

// Concrete Concrete[T IMessageBody]
// @Description: 具体化泛型,用于不能装泛型的位置,如map.value
// @return  func(mtx *MessageContext) IMessageBody
func Concrete[T IMessageBody]() func(mtx *MessageContext) IMessageBody {
	return func(mtx *MessageContext) IMessageBody {
		// generalize, boxing
		result, err := ordermessage.CreateMessageBody[T](mtx)
		if err != nil {
			return nil
		}
		return result
	}
}

扩展

Java允许但go不允许的事

继承、循环依赖、异常处理、运行时泛型……

Javaer写go常犯的错误

  • 定义过多结构体或接口
  • 在结构体内写方法
  • 只有struct才配有方法
  • ......

  • 21
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值