go抽象封装是一种简化认知的手段

news/2024/10/9 11:26:35/文章来源:https://www.cnblogs.com/cheyunhua/p/18453859

通过 Kubernetes 看 Go 接口设计之道

原创 蔡蔡蔡菜 蔡蔡蔡云原生Go
 

解耦依赖底层

在 Kubernetes 中能看到非常多通过接口对具体实现的封装。

Kubelet 实现了非常多复杂的功能,我们可以看到它实现了各种各样的接口,上层代码在使用的时候并不会直接实现 Kubelet 这个具体的结构体,是为了让上层和下层解耦,这样下层 Kubelet 发生改变的时候,接口只要保持稳定,就不需要改动上层的逻辑。

比如 Bootstrap 就是 Kubelet 实现的其中一个接口

type Bootstrap interface {
 GetConfiguration() kubeletconfiginternal.KubeletConfiguration
 BirthCry()
 StartGarbageCollection()
 ListenAndServe(kubeCfg *kubeletconfiginternal.KubeletConfiguration, tlsOptions *server.TLSOptions, auth server.AuthInterface, tp trace.TracerProvider)
 ListenAndServeReadOnly(address net.IP, port uint, tp trace.TracerProvider)
 ListenAndServePodResources()
 Run(<-chan kubetypes.PodUpdate)
 RunOnce(<-chan kubetypes.PodUpdate) ([]RunPodResult, error)
}

在启动 Kubelet 时用的并不是 Kubelet 这个结构体,而是转换成了 Bootstrap 接口。

func createAndInitKubelet() (k kubelet.Bootstrap, err error) {

 k, err = kubelet.NewMainKubelet(...)

 k.BirthCry()

 k.StartGarbageCollection()

 return k, nil
}

也通过接口定义的分离,让外部启动的时候不会看到 Kubelet 过多的方法细节。

内部去调用时会看到入参过多的细节,将入参隐式转成结构,让内部只看到需要的方法。

这在日常业务研发中是使用的最多的,但是使用的时候也没办法一蹴而就,我们在第一次接到产品需求的时候,会将逻辑封装成 service 的结构体。

type UserService struct{}

func (*UserService) Login(ctx context.Context. req LoginRequest)(User, error){}

最开始我们会直接依赖具体的实现,并且可能也只支持一种登录,比如手机号验证码登录。

产品迭代后需要接入多种登录方式,比如微信扫码登录。这个时候我们可以将原有的 Service 抽象成接口。

type UserService interface {
 Login(ctx context.Context. req LoginRequest)(User, error)
}

然后根据传入参数请求的不同来选择不同的登录逻辑,外面则只需要用接口进行调用即可。

type wxLogin struct{}

func (*wxLogin) Login(ctx context.Context. req LoginRequest)(User, error){}

type phoneLogin struct{}

func (*phoneLogin) Login(ctx context.Context. req LoginRequest)(User, error){}

这样对于调用方来说也不需要复杂的逻辑来选择看使用的是哪一种登录,直接通过 NewUserService 获取  UserService 这个接口,然后调用 Login 方法即可。

func NewUserService() (UserService) {
 // 通过单例只初始化一个
 initOnce.Do(func(){
  
 })
 return userService
}

等到后面 User 模块需要单独拆分服务的时候,只需要提供一个 RPC 的 SDK 实现进行替换,这样调用方就能够无感知的.

func NewUserService() (UserService) {
 initOnce.Do(func(){
  // 通过 RPC 来初始化 userService
 })
 return userService
}

隐藏细节

我们看 Kubelet 实现的另一个接口 SyncHandler.

type SyncHandler interface {
 HandlePodAdditions(pods []*v1.Pod)
 HandlePodUpdates(pods []*v1.Pod)
 HandlePodRemoves(pods []*v1.Pod)
 HandlePodReconcile(pods []*v1.Pod)
 HandlePodSyncs(pods []*v1.Pod)
 HandlePodCleanups(ctx context.Context) error
}

这里我们可以看到 Kubelet 本身有比较多的方法:

  • syncLoop 同步状态的循环
  • Run 用来启动监听循环
  • HandlePodAdditions 处理Pod增加的逻辑
type Kubelet struct{}

func (kl *Kubelet) HandlePodAdditions(pods []*Pod) {
 for _, pod := range pods {
  fmt.Printf("create pods : %s\n", pod.Status)
 }
}

func (kl *Kubelet) Run(updates <-chan Pod) {
 fmt.Println(" run kubelet")
 go kl.syncLoop(updates, kl)
}

func (kl *Kubelet) syncLoop(updates <-chan Pod, handler SyncHandler) {
 for {
  select {
  case pod := <-updates:
   handler.HandlePodAdditions([]*Pod{&pod})
  }
 }
}

由于 syncLoop 其实并不需要知道 kubelet 上其他的方法,所以通过 SyncHandler 接口的定义,让 kubelet 实现该接口后,外面作为参数传入给 syncLoop  ,它就会将类型转换为 SyncHandler 。

经过转换后 kubelet 上其他的方法在入参里面就看不到了,编码时就可以更加专注在 syncLoop 本身逻辑的编写。

但是这样做同样会带来一些问题,第一次研发的需求肯定是能满足我们的抽象,但是随着需求的增加和迭代,我们在内部需要使用 kubelet 其他未封装成接口的方法时,我们就需要额外传入 kubelet 或者是增加接口的封装,这都会增加我们的编码工作,也破坏了我们最开始的封装,所以这个方法要辩证的去使用,如果本身逻辑并没有复杂到需要隐藏的程度,那就没有必要额外用接口来入参。

分层隐藏设计是我们设计的最终目的,在代码设计的过程中让一个局部关注到它需要关注的东西即可,但是在兼顾隐藏的同时也要考虑需求变动后,代码变动的复杂度。

有些相对稳定的逻辑代码则可以通过该方法进行抽象,也能让局部的方法变的更灵活,能够帮助我们解耦业务逻辑和函数的具体实现,类似于模板方法。

促销的业务实现

我们看一个商城促销的例子,有多种促销策略:满减、打折和赠品,我们希望经过各种促销策略后算出我们购物车需要支付的总金额。

我们先定义购物车和促销接口。

type Cart struct {
    Total float64
    Items []string
}

type Promotion interface {
  // 在购物车上应用促销策略,返回的是新的购物车实体
    Apply(cart Cart) Cart
}

然后不同促销策略都实现 Promotion 接口

// 满减促销
type DiscountByAmount struct {
    Threshold float64
    Discount  float64
}

func (d DiscountByAmount) Apply(cart Cart) Cart {
    if cart.Total >= d.Threshold {
        cart.Total -= d.Discount
    }
    return cart
}

// 打折促销
type DiscountByPercentage struct {
    Percentage float64
}

func (d DiscountByPercentage) Apply(cart Cart) Cart {
    cart.Total *= (1 - d.Percentage)
    return cart
}

// 赠品促销
type FreeGift struct {
    Gift string
}

func (g FreeGift) Apply(cart Cart) Cart {
    cart.Items = append(cart.Items, g.Gift)
    return cart
}

使用所有策略则通过 GetEffectPromotion 获得所有仍然在进行的活动,然后依次应用到购物车上

// ApplyPromotion 应用在活动期限的促销策略
func ApplyPromotion(cart Cart) Cart {
 promotions := GetEffectPromotion()

 for _, promotion := range promotions {
  cart = promotion.Apply(cart)
 }

 return cart
}

func GetEffectPromotion() []Promotion {
 promotions := make([]Promotion, 0)

 // 满减促销
 discountByAmount := DiscountByAmount{Threshold: 100, Discount: 20}
 promotions = append(promotions, discountByAmount)

 // 打折促销
 discountByPercentage := DiscountByPercentage{Percentage: 0.1}
 promotions = append(promotions, discountByPercentage)

 // 赠品促销
 freeGift := FreeGift{Gift: "Free Mug"}
 promotions = append(promotions, freeGift)

 // 如果有其他策略可以在这里初始化

 return promotions
}

如果不通过接口入参的方式实现,我们也可以用函数进行实现。

package main

// 满减促销
type DiscountByAmount struct {
    Threshold float64
    Discount  float64
}

// 应用满减促销
func ApplyDiscountByAmount(cart Cart, promo DiscountByAmount) Cart {
    if cart.Total >= promo.Threshold {
        cart.Total -= promo.Discount
    }
    return cart
}

// 打折促销
type DiscountByPercentage struct {
    Percentage float64
}

// 应用打折促销
func ApplyDiscountByPercentage(cart Cart, promo DiscountByPercentage) Cart {
    cart.Total *= (1 - promo.Percentage)
    return cart
}

// 赠品促销
type FreeGift struct {
    Gift string
}

// 应用赠品促销
func ApplyFreeGift(cart Cart, promo FreeGift) Cart {
    cart.Items = append(cart.Items, promo.Gift)
    return cart
}

func ApplyPromotion(cart Cart) Cart {
   // 满减促销
    discountByAmount := GetDiscountByAmountRules()
    cart = ApplyDiscountByAmount(cart, discountByAmount)

    // 打折促销
    discountByPercentage := GetDiscountByPercentage()
    cart = ApplyDiscountByPercentage(cart, discountByPercentage)
   
    // 赠品促销
    freeGift := GetFreeGift()
    cart = ApplyFreeGift(cart, freeGift)
    
    return cart
}

如果后期增加新的促销策略,则需要增加对应的函数或逻辑块。

而抽象出接口之后,可以将具体的促销逻辑和应用到购物车的逻辑拆开。这样做的好处是 ApplyPromotion 使用逻辑可以相对稳定,编写单元测试也更加简单,如果增加策略,我们只需要单独测试新增的促销策略逻辑是否正确和获取有效的促销策略是否正确即可,而不需要再测试整个 ApplyPromotion 逻辑。

转换类型

go 中 http 的 Handle 接口的定义后,将HandlerFunc 函数作为一个类型,再实现 ServeHTTP 接口

type Handler interface {
 ServeHTTP(ResponseWriter, *Request)
}

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
 f(w, r)
}

我们有了方法后直接使用 HandlerFunc 进行类型转换,则就实现了 Handle 接口,我们看具体使用的例子

func greeting(w http.ResponseWriter, r *http.Request) {
    // 处理HTTP请求的逻辑
}

// 然后使用的时候只需要这样进行转换即可
http.Handle("/greeting", http.HandlerFunc(greeting))

http.HandlerFunc 的真正含义是将函数greeting显式转换为 HandlerFunc类型。

转成这种接口类型有什么实际的意义?

通过这种转换,我们可以轻易的实现处理方法灵活替换,根据不同的 Path 选择不同的处理方法,同时注册到 HTTP server 上。

并且也不需要通过再次写一个 struct 来实现对应的 Handler 接口, 如果不支持这样的写法,我们需要怎么写?可以看下面这个例子

func greeting(w http.ResponseWriter, r *http.Request) {
    // 处理HTTP请求的逻辑
}

// 手动实现http.Handler接口
type GreetingHandler struct{}

func (h GreetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    greeting(w, r)
}

// 然后在main函数中这样使用:
http.Handle("/greeting", GreetingHandler{})

可以看到我们如果要实现 Handler 这个接口的话,要自己手动去实现 ServeHTTP 。

那这么封装有什么实际的作用,能给我们的编码带来什么样的遍历呢?

由于都实现了 Handler 接口,所以能够方便的实现链式调用,形成复杂的调用链,下面是日志中间件的写法。

// 日志中间件
func loggingMiddleware(next http.Handler) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  start := time.Now()
  next.ServeHTTP(w, r)
  log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
 })
}

// 实际的处理函数
func greetingHandler(w http.ResponseWriter, r *http.Request) {}

func main() {
 // 使用loggingMiddleware包装greetingHandler
 handler := loggingMiddleware(http.HandlerFunc(greetingHandler))

 http.Handle("/greeting", handler)
}

go 中 io 的处理同样也是用了类似方式来实现链式调用。

k8s 中将 context 封装进了 Reader 中。

// readerCtx is the interface that wraps io.Reader with a context
type readerCtx struct {
 ctx context.Context
 io.Reader
}

然后在每次 Read  之前都会先判断 context 是否有 err ,有的话则直接返回错误。

func (r *readerCtx) Read(p []byte) (n int, err error) {
 if err := r.ctx.Err(); err != nil {
  return 0, err
 }
 return r.Reader.Read(p)
}

然后使用的时候就通过 new 来将 context 和 Reader 封装在一起,返回的类型仍然是 io.Reader

// newReaderCtx gets a context-aware io.Reader
func newReaderCtx(ctx context.Context, r io.Reader) io.Reader {
 return &readerCtx{
  ctx:    ctx,
  Reader: r,
 }
}

小结

读到这里,我们来对整篇文章做一个小结

  1. 抽象封装是一种简化认知的手段,避免我们掉落进代码的细节内而不知道整体功能需要做什么.
  2. 本文以 Kubernetes 中的 Kubelet 为例,我们看到接口封装的使用使得上层代码不必直接依赖 Kubelet 的具体实现,从而在 Kubelet 改动时无需修改上层逻辑。
  3. 通过接口的方式也可以减少入参的复杂度,让代码更加专注于核心逻辑。
  4. 接口的使用也需谨慎。如果需求或逻辑较为简单,引入过多接口反而会增加开发和维护的复杂度,设计时应权衡隐藏细节和代码变动的复杂性,确保接口抽象既能解耦业务逻辑,也能在需求变动时提供足够的灵活性。

 

 
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/810445.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

5.4 5.5

5.4 求解下列非线性规划:点击查看代码 import numpy as np from scipy.optimize import minimize def objective(x): return -np.sum(np.sqrt(x) * np.arange(1, 101)) def constraint1(x): return x[1] - 10 def constraint2(x): return 20 - (x[1] + 2*x[2]) def c…

升轨和降轨地表形变监测结果合成处理方法

一般情况下,InSAR处理得到的形变结果是视线方向形变(LOS)。在处理时候,输出结果可以选择垂直、坡向方向形变,甚至自定义方向上的形变,但这种结果往往是通过简单的勾股弦方式计算得到,理论精度不是很可靠。要想得到更精确的垂直向形变,甚至水平向(东西向、南北向)形变…

无线电通信卡:9-基于DSP TMS320C6678+FPGA XC7V690T的6U VPX信号处理卡

6U 6槽 VPX , AI加速计算 , 板 ,GPU计算主 图像处理卡 , 无线电通信一、概述 本板卡基于标准6U VPX 架构,为通用高性能信号处理平台,系我公司自主研发。板卡采用一片TI DSP TMS320C6678和一片Xilinx公司Virtex 7系列的FPGA XC7V690T-2FFG1761I作为主处理器,Xilinx 的Aritex …

vue 前端导出 excel

npm install xlsx-js-style import XLSX from xlsx-js-style;//导出数据 exportD(title,data,fileName){ title=["标题1","标题2","标题3","标题4","标题5","标题6"]; data=[["数据1…

[智能网联汽车/数据标准/法规政策] 标准解读:GB/T 44464-2024《汽车数据通用要求》

0 引言随着智能技术的不断发展,智能网联汽车作为新时代移动智能终端的代表,正引领着汽车产业向智能化、网联化深刻转型与升级。智能网联汽车与云端服务器、移动端、车端等设备存在大量的数据交互,包括车辆运行数据、用户个人信息等。缺乏对这些数据实施的有效监管与控制,将…

铁威马新品F8 SSD Plus:假期出行的完美存储“伙伴”

国庆小长假刚刚结束 大家都去哪里玩了呢? 假期出行 如何安全、便捷地存储和管理 大量的照片、视频和其他文件 也是一个不容忽视的问题铁威马秋季系列新品NAS的发售 为我们提供了多种选择 而F8 SSD Plus 性能与便携的完美融合 成为假期出行不可或缺的“好伙伴”F8 SSD Plus 特…

springboot 加mybatis 配置多数据源

案例:比如说,接口接收到的数据,放到多个数据库。 1、先引入dynamic<dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>4.3.1</version></dependency>…

zabbix“专家坐诊”第258期问答

问题一 Q:各位大神 问下这个zabbix通知到企业微信的py脚本可以使吗? A: 这个需要自行测试。 Q:用jconsole连接失败了。 A:那就是没配好, 连接不上,要确保能正常远程连接。问题二 Q:这种是虚拟机内部的jmx配置有问题吗? A:提示被拒绝了,没成功连上,jmx 的要用先测…

图片转绘画效和绘画软件

我收藏的软件:绘画软件: ArtRage Krita 图片转绘画效: Snap Art 4 Impresso Proffffffffffffffffftest red font.

Java反序列化

Java反序列化 什么是序列化和反序列化 如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。序列化:将数据结构或对象转换成二进制字节流的过程 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或…

QT5中引入GMSSL库

近来项目中需要使用加密算法,对上/下位机之间的消息进行加密。客户要求使用国密算法库,不能使用国际上通用的AES、RSA等算法。 国密即国家密码局认定的国产密码算法。主要有SM1,SM2,SM3,SM4。密钥长度和分组长度均为128位。 其中SM1没有开源,其他的均开源。 源码编译 开源…

Day2 备战CCF-CSP练习

201403-3Day2 题目描述 请你写一个命令行分析程序,用以分析给定的命令行里包含哪些选项。 每个命令行由若干个字符串组成,它们之间恰好由一个空格分隔。 这些字符串中的第一个为该命令行工具的名字,由小写字母组成,你的程序不用对它进行处理。 在工具名字之后可能会包含若干…