首页 > 其他分享 >go抽象封装是一种简化认知的手段

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

时间:2024-10-09 11:24:09浏览次数:8  
标签:促销 封装 认知 cart Cart 接口 func go http

通过 Kubernetes 看 Go 接口设计之道

原创 蔡蔡蔡菜 蔡蔡蔡云原生Go    2024年10月01日 08:30 广东

解耦依赖底层

在 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. 接口的使用也需谨慎。如果需求或逻辑较为简单,引入过多接口反而会增加开发和维护的复杂度,设计时应权衡隐藏细节和代码变动的复杂性,确保接口抽象既能解耦业务逻辑,也能在需求变动时提供足够的灵活性。

 

   

标签:促销,封装,认知,cart,Cart,接口,func,go,http
From: https://www.cnblogs.com/cheyunhua/p/18453859

相关文章

  • Go 语言变量数据类型
    Go语言变量数据类型Go语言是一种静态类型语言,这意味着每个变量都有一个明确的数据类型。在Go语言中,主要的数据类型包括整型、浮点型、布尔型和字符串。下面,我将分别介绍这些数据类型,并提供其在程序中的应用场景、原理,以及相应的代码示例。整型(int)介绍整型用于存储整......
  • (分享源码)计算机毕业设计必看必学 上万套实战教程手把手教学JAVA、PHP,node.js,C++、pyth
    摘要信息化社会内需要与之针对性的信息获取途径,但是途径的扩展基本上为人们所努力的方向,由于站在的角度存在偏差,人们经常能够获得不同类型信息,这也是技术最为难以攻克的课题。针对社区防疫管理等问题,对社区防疫管理系统进行研究分析,然后开发设计出基于Django框架的社区防......
  • Google:敏感信息访问权限和 API 政策更新
    目录公布时间公布内容内容有关GooglePlay照片和视频权限政策的详细信息截止时间相关问题公布时间公布日期:2023-10-25公布内容内容为向用户提供更注重隐私保护的体验,我们将推出“照片和视频访问权限”政策,以减少获准针对照片/视频请求广泛权限(READ_......
  • 基于django+vue+Vue防疫物资仓库管理系统【开题报告+程序+论文】-计算机毕设
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景在全球新冠疫情持续蔓延的背景下,防疫物资的有效管理和分配成为了公共卫生应急响应中的关键环节。随着疫情形势的不断变化,各类防疫物资如口......
  • 【开题报告】基于django+vue基于微信小程序点餐系统的设计与实现(论文+源码)计算机毕
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着移动互联网技术的飞速发展,智能手机已成为人们日常生活中不可或缺的一部分,而微信小程序作为微信生态系统中的重要组成部分,凭借其无需下......
  • 第二节:基于.Net 链接Mongodb实战
     三. 实操(详见官网:https://www.mongodb.com/zh-cn/docs/drivers/csharp/current/quick-start/)1.相关程序集 【MongoDB.Driver2.17.1】  相关实体:加上如下特性,id值自动生成。publicclassShipInfo{///<summary>///自动生成id/......
  • Goland安全开发第二节
    Goland安全开发第二节一、基础语法-初识函数、基本类型packagemainimport"fmt"funcmain(){fmt.Println("HelloGo!")}上述是一个简单的go代码Package包是Go语⾔的基本组成单元,通常使⽤单个的⼩写单词命名,⼀个Go程序本质上就是⼀组包的集合。所有Go代码都有⾃......
  • Django学习笔记十四:系统框架总结
    Django系统框架的主要特点Django是一个功能强大的PythonWeb框架,以下是对其系统框架的总结:一、MTV架构模型(Model):负责与数据库交互,处理数据的存储、检索、更新和删除等操作。通过定义数据模型类,Django可以自动生成数据库表结构,或者根据现有的数据库反向生成模型类......
  • 图床搭建与工作流优化全方位集成教程:EasyImages2.0与七牛云的搭建与配置+通过 PicGo
    1.准备搭建–简单图床——EasyImage2.01.1前言无意中网上冲浪的时候,发现了一个叫做简单图床——EasyImage的图床,很好用,搭建也很简单。特别鸣谢简单图床——EasyImage作者大大。1.2特点支持WebP格式转换支持仅登录后上传支持设置图片质量支持文字/图片水印支持设置......
  • Django路由和视图(筛选)
    温习温故知新1.路由分发方式一:includefromdjango.urlsimportpath,includeurlpatterns=[path('app/',include('app.urls'))]方式二:手工分发urlpatterns=[#path('user/login/',login,name='login'),#path('......