Go设计模式(2)-面向对象分析与设计

2023-12-03 15:18

本文主要是介绍Go设计模式(2)-面向对象分析与设计,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前些日子写完了第一篇设计模式Go设计模式(1)-语法。本篇文章主要讲解遇到项目之后如何做面向对象分析与设计。这篇文章的很多思路都来自于王争的设计模式之美,不过我会按照自己的经验和构思来进行讲解。

很多同学都看过设计模式相关的书籍,但是经常很难用到实际工作中,其中一个原因可能是没有想过如何将具体的业务转化为面向对象的流程。一旦我们熟知这个流程,就能从面向过程思维进入到面向对象思维,然后就可以用学到的设计思想对业务进行设计与优化。

业务

作为一个努力向上的程序员,一天leader告诉你:“为了保证接口调用的安全性,我们希望设计实现一个接口调用鉴权功能,只有经过认证之后的系统才能调用我们的接口,没有认证过的系统调用我们的接口会被拒绝。我希望由你来负责这个任务的开发,争取尽快上线。”

面向对象分析(OOA)

面向对象分析的产出是详细的需求描述。

第一轮基础分析

第一轮分析主要是分析一下具体需求是什么,可以采取什么技术方案解决。

通过和Leader细聊后,发现是自己组维护的后台系统需要提供几个API接口供其他组调用,这些接口返回的数据比较敏感,必须做安全校验。

突然碰到这个问题可能有点懵,可以思考一下有什么方案,如果没有思路,可以和同事讨论一下,也可以看看团队以前是怎么做的,或者网上搜索一下常规方案是怎样的。

公司内部常用的API鉴权方案是X5协议,查看网上信息,一般用签名方案。

X5协议示例:
appid和appkey
  • appidappkey在使用前向管理员申请
  • appkey仅在计算时使用,如有泄露可随时更换。
接口传输数据$sendData格式定义:
<?php
array('header'=>array('appid'=>$appid,'url'=>$url,'method'=>$method,'sign'=>$sign,),'body'=>$body,
);
?>
sign字段算法

appid,对应接口请求报文json串,appkey先后拼接,对其执行32位MD5,之后转为大写

$sign=strtoupper(md5($appid.$reqBody.$appkey))
拼接请求报文

将已有参数按如下格式拼接,之后进行base64编码,即可得到最终的requestbody。

$encRequest=base64_encode(json_encode($reqStream));

根据目前的情况,选择X5协议实现鉴权。主要是因为这是公司内部协议,每个组都比较熟悉,今后对接、联调、扩展都相对容易一些。

第二轮分析优化

这一轮需要对技术方案细化,并出流程图。

一个比较简单的方案是我们过一下整个流程,然后对流程中的各个细节进行提问。

  1. 调用方申请appid和appkey
  2. 调用方使用appid和appkey,计算出sign,并对请求参数进行base64编码
  3. 调用方向服务方发送请求
  4. 服务方将数据解码,重新计算sign,判断sign是否相等

仔细分析流程,我们需要考虑如下几个问题:

  1. appid和appkey如何申请和存放
    • 随着调用方增多,我们需要查看有哪些appid、被分配给哪些组
    • appid和appkey方便修改,最好调用方和服务方能同时生效
    • 是否需要针对不同API设置不同appid和appkey
  2. 很多系统或者接口都有鉴权需求
    • 如何设计的更加通用,对业务侵入性最低
    • 是否需要提供SDK,供其它系统使用
  3. 是否需要考虑重放攻击

这么一想,如果按照最全的方式来做,工作量还是挺大的。鉴于实际情况,我们先完成简单版,完全版可以等业务量起来时优化。所以对于上面的问题,我们做如下选择

  1. appid和appkey存放到服务端配置文件中,调用方自己维护申请到的appid和appkey,按照系统维度提供appid和appkey。后期可使用配置中心、ETCD或者Redis来管理appid和appkey。
  2. 本次只需要要供本系统使用,但设计的需要通用,对服务侵入性要低。后期调用方增多后,可提供SDK。
  3. 因为都是内网调用,相对安全,先不考虑重放攻击。
流程图

重点回顾

有时候需求比较抽象、模糊,需要自己去挖掘做取舍,产生清晰、可落地的需求定义。需求分析是一个迭代过程,可以现有一个大体的方案,然后一步一步细化。在这个过程中,可以先列出大体的流程,然后多提问题,多回答这些问题,最后一定要有产出

说来也有意思,前些日子安排一个同学做了相似的工作,但紧紧是完成任务而已,而没有去思考通用性、扩展性这些问题。其实也比较容易理解,仿照前人做的东西写要容易的多,但这样就很难提升自己的能力和系统架构了。也希望大家今后做项目的时候能够多思考。

面向对象设计

面向对象设计的产出是类。

划分职责进而识别出有哪些类

实现这一步,可以根据需求描述,把其中涉及的功能点,一个一个罗列出来,然后再去看哪些功能点职责相近,操作同样的属性,可否应该归为同一个类。

功能点列表:

  1. 利用appid和appkey,对请求参数进行加密生成sign
  2. 将请求数据进行base64编码
  3. 使用base64对请求数据进行解码,获取到appid
  4. 从存储中取出appid对应的appkey
  5. 重新计算sign,判断两个sign是否匹配

1、5和sign有关,2、3和编码、解析有关,4和存储有关。所以可以粗略得到三个核心类。AuthToken、ApiRequest、CredentialStorage。AuthToken负责实现1、5这两个操作;ApiRequest负责2、3两个操作;CredentialStorage负责4这个操作。

当然,这是一个初步的类的划分,其他一些不重要的、边边角角的类,我们可能暂时没法一下子想全,但这也没关系,面向对象分析、设计、编程本来就是一个循环迭代、不断优化的过程。

定义类及其属性和方法

对于方法的识别,识别出需求描述中的动词,作为候选的方法,再进一步过滤筛选。对于属性的识别,把功能点中涉及的名词,作为候选属性,然后同样进行过滤筛选。

现在我们来看下,每个类都有哪些属性和方法。我们还是从功能点列表中挖掘。

AuthToken

AuthToken类相关的功能点有两个:

  1. 利用appid和appkey,对请求参数进行加密生成sign
  2. 判断两个sign是否匹配

动词有:生成、匹配

名词有:sign(appid和appkey从业务上来说不属于AuthToken,可以当做参数传入)

type Header struct {AppId stringSign  string
}
type Data struct {Header HeaderBody   string
}type AuthToken struct {sign string
}func CreateAuthToken() *AuthToken {return &AuthToken{}
}func (authToken *AuthToken) Create(appId string, appKey string, body string) string {h := md5.New()h.Write([]byte(appId + body + appKey))authToken.sign = strings.ToUpper(fmt.Sprintf("%x", h.Sum(nil)))return authToken.sign
}func (authToken *AuthToken) Match(token *AuthToken) bool {if authToken.sign == token.sign {return true}return false
}
ApiRequest

ApiRequest类相关功能点有两个:

  1. 进行base64编码,生成请求数据
  2. 进行base64解码,获取appid和sign

动词有:编码、解码

type ApiRequest struct {appId stringsign  stringdata  *Data
}func CreateApiRequest() *ApiRequest {return &ApiRequest{}
}
func (apiRequest *ApiRequest) Encode(data string) string {return base64.StdEncoding.EncodeToString([]byte(data))
}
func (apiRequest *ApiRequest) Decode(data string) (appId string, sign string, err error) {bytes, err := base64.StdEncoding.DecodeString(data)if err != nil {return}apiRequest.data = &Data{}if err := json.Unmarshal(bytes, apiRequest.data); err != nil {return "", "", err}apiRequest.appId = apiRequest.data.Header.AppIdapiRequest.sign = apiRequest.data.Header.Signreturn apiRequest.appId, apiRequest.sign, nil
}func (apiRequest *ApiRequest) GetAppid() string {return apiRequest.appId
}func (apiRequest *ApiRequest) GetSign() string {return apiRequest.sign
}
CredentialStorage

CredentialStorage相关功能点只有一个:

  1. 从存储中取出appid对应的appkey

因为后期获取数据位置会变,所以最好设计为接口,基于接口而非具体的实现编程。

type CredentialStorage interface {GetAppkeyByAppid(appId string) string
}type CredentialStorageConfig struct {
}func (config *CredentialStorageConfig) GetAppkeyByAppid(appId string) string {if appId == "test" {return "test"}return "test"
}

定义类与类之间的交互关系

类之间的交互关系可以简化为四种:泛化、实现、组合、依赖

泛化:可以理解为继承关系

实现:一般指接口实现类之间的关系

组合:包括聚合、组合、关联等,一般指类中包含其它类

依赖:只要两个类有任何关系,就认为是依赖关系

所以CredentialStorage和CredentialStorageConfig是实现关系。

将类组装起来并提供执行入口

接下来我们需要将类组装起来,让整个代码跑起来。

我们封装所有的实现细节,设计了一个最顶层的ApiAuthencator类,暴露一组给外部调用者使用的API接口,作为触发执行鉴权逻辑的入口。

type ApiAuthencator struct {credentialStorage CredentialStorage
}func CreateApiAuthenCator(cs CredentialStorage) *ApiAuthencator {return &ApiAuthencator{credentialStorage: cs}
}func (apiAuthencator *ApiAuthencator) Auth(data string) (bool, error) {//1.解析数据apiRequest := CreateApiRequest()appId, sign, err := apiRequest.Decode(data)//fmt.Println(appId, sign, apiRequest.data)if err != nil {return false, fmt.Errorf("Decode failed")}//2.获取appId对应的appkeyappKey := apiAuthencator.credentialStorage.GetAppkeyByAppid(appId)//3.重新计算signauthToken := CreateAuthToken()newSign := authToken.Create(appId, appKey, apiRequest.data.Body)if sign == newSign {return true, nil}return false, nil
}

面向对象编程

面向对象设计完成之后,我们已经定义清晰了类、属性、方法、类之间的交互,并且将所有的类组装起来,提供了统一的执行入口。接下来,面向对象编程的工作,就是将这些设计思路翻译成代码实现。

在面向对象分析的时候,会完成部分编码工作。面向对象编程过程中需要将代码进行完善。不过因为这个业务相对简单,所以现在只写一下main函数,看一下执行效果。

func main() {//客户端appId := "test"appKey := "test"sendData := &Data{Header: Header{AppId: appId,},Body: "for test",}authToken := CreateAuthToken()sign := authToken.Create(appId, appKey, sendData.Body)sendData.Header.Sign = signsendDataMarshal, _ := json.Marshal(sendData)sendDataString := CreateApiRequest().Encode(string(sendDataMarshal))//fmt.Println(sign, sendData, string(sendDataMarshal), string(sendDataString))//服务端apiAuthenCator := CreateApiAuthenCator(new(CredentialStorageConfig))auth, err := apiAuthenCator.Auth(sendDataString)if err != nil {fmt.Println(err.Error())return}if auth == false {fmt.Println("auth failed")return}fmt.Println("auth success")return
}

优点

虽然使用面向对象编写流程比面向过程负责的多,但按照套路走,即便是没有太多设计经验的初级工程师,都可以按部就班地参照着这个流程来做分析、设计和实现。

面向对象最重要的一点是:能够把扩展点提前准备好,今后有变更时只需要更改少量代码。

面向对象的设计没有最好,只有更好,它是需要根据发展不断迭代重构的过程。

对于鉴权需求,最可能变更的是获取appkey方式变化,但因为使用了接口,后期变更只需要编写新的获取appkey的类,然后更改CreateApiAuthenCator(new(CredentialStorageConfig))代码即可。

面向对象要使用的好

  1. 需要对业务熟悉,能够预料到哪些是未来会变化的点
  2. 要埋下合适的扩展点,这需要了解一些原则和设计模式

总结

多年的工作经验告诉我,编码过程中一定要善于使用面向对象思想,否则系统会越来越臃肿,越来越难以维护。这篇文章阐述了使用面向对象方法的套路,按照这个套路走,不断提升自己,让自己成为更优秀的人。

完整代码可查看:https://github.com/shidawuhen/asap/blob/master/controller/design/2design.go

资料

  1. 设计模式之美
  2. 接口鉴权

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:https://shidawuhen.github.io/

往期文章回顾:

  1. Go语言
  2. MySQL/Redis
  3. 算法
  4. 架构/网络/项目
  5. 思考/读书笔记

这篇关于Go设计模式(2)-面向对象分析与设计的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/449899

相关文章

go中的时间处理过程

《go中的时间处理过程》:本文主要介绍go中的时间处理过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 获取当前时间2 获取当前时间戳3 获取当前时间的字符串格式4 相互转化4.1 时间戳转时间字符串 (int64 > string)4.2 时间字符串转时间

Go语言中make和new的区别及说明

《Go语言中make和new的区别及说明》:本文主要介绍Go语言中make和new的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 概述2 new 函数2.1 功能2.2 语法2.3 初始化案例3 make 函数3.1 功能3.2 语法3.3 初始化

Java设计模式---迭代器模式(Iterator)解读

《Java设计模式---迭代器模式(Iterator)解读》:本文主要介绍Java设计模式---迭代器模式(Iterator),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录1、迭代器(Iterator)1.1、结构1.2、常用方法1.3、本质1、解耦集合与遍历逻辑2、统一

Go语言中nil判断的注意事项(最新推荐)

《Go语言中nil判断的注意事项(最新推荐)》本文给大家介绍Go语言中nil判断的注意事项,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1.接口变量的特殊行为2.nil的合法类型3.nil值的实用行为4.自定义类型与nil5.反射判断nil6.函数返回的

Go语言数据库编程GORM 的基本使用详解

《Go语言数据库编程GORM的基本使用详解》GORM是Go语言流行的ORM框架,封装database/sql,支持自动迁移、关联、事务等,提供CRUD、条件查询、钩子函数、日志等功能,简化数据库操作... 目录一、安装与初始化1. 安装 GORM 及数据库驱动2. 建立数据库连接二、定义模型结构体三、自动迁

Go语言代码格式化的技巧分享

《Go语言代码格式化的技巧分享》在Go语言的开发过程中,代码格式化是一个看似细微却至关重要的环节,良好的代码格式化不仅能提升代码的可读性,还能促进团队协作,减少因代码风格差异引发的问题,Go在代码格式... 目录一、Go 语言代码格式化的重要性二、Go 语言代码格式化工具:gofmt 与 go fmt(一)

Go学习记录之runtime包深入解析

《Go学习记录之runtime包深入解析》Go语言runtime包管理运行时环境,涵盖goroutine调度、内存分配、垃圾回收、类型信息等核心功能,:本文主要介绍Go学习记录之runtime包的... 目录前言:一、runtime包内容学习1、作用:① Goroutine和并发控制:② 垃圾回收:③ 栈和

Go语言中泄漏缓冲区的问题解决

《Go语言中泄漏缓冲区的问题解决》缓冲区是一种常见的数据结构,常被用于在不同的并发单元之间传递数据,然而,若缓冲区使用不当,就可能引发泄漏缓冲区问题,本文就来介绍一下问题的解决,感兴趣的可以了解一下... 目录引言泄漏缓冲区的基本概念代码示例:泄漏缓冲区的产生项目场景:Web 服务器中的请求缓冲场景描述代码

Go语言如何判断两张图片的相似度

《Go语言如何判断两张图片的相似度》这篇文章主要为大家详细介绍了Go语言如何中实现判断两张图片的相似度的两种方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 在介绍技术细节前,我们先来看看图片对比在哪些场景下可以用得到:图片去重:自动删除重复图片,为存储空间"瘦身"。想象你是一个

Go语言中Recover机制的使用

《Go语言中Recover机制的使用》Go语言的recover机制通过defer函数捕获panic,实现异常恢复与程序稳定性,具有一定的参考价值,感兴趣的可以了解一下... 目录引言Recover 的基本概念基本代码示例简单的 Recover 示例嵌套函数中的 Recover项目场景中的应用Web 服务器中