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中的优雅关闭机制使得在应用程序接收到终止信号时,能够进行平滑的资源清理,通过使用context来管理goroutine的生命周期,结合signal... 目录1. 什么是信号处理?2. 如何优雅地关闭 Go 应用?3. 代码实现3.1 基本的信号捕获和优雅关闭3.2

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

怎么让1台电脑共享给7人同时流畅设计

在当今的创意设计与数字内容生产领域,图形工作站以其强大的计算能力、专业的图形处理能力和稳定的系统性能,成为了众多设计师、动画师、视频编辑师等创意工作者的必备工具。 设计团队面临资源有限,比如只有一台高性能电脑时,如何高效地让七人同时流畅地进行设计工作,便成为了一个亟待解决的问题。 一、硬件升级与配置 1.高性能处理器(CPU):选择多核、高线程的处理器,例如Intel的至强系列或AMD的Ry

基于51单片机的自动转向修复系统的设计与实现

文章目录 前言资料获取设计介绍功能介绍设计清单具体实现截图参考文献设计获取 前言 💗博主介绍:✌全网粉丝10W+,CSDN特邀作者、博客专家、CSDN新星计划导师,一名热衷于单片机技术探索与分享的博主、专注于 精通51/STM32/MSP430/AVR等单片机设计 主要对象是咱们电子相关专业的大学生,希望您们都共创辉煌!✌💗 👇🏻 精彩专栏 推荐订阅👇🏻 单片机

Go Playground 在线编程环境

For all examples in this and the next chapter, we will use Go Playground. Go Playground represents a web service that can run programs written in Go. It can be opened in a web browser using the follow

go基础知识归纳总结

无缓冲的 channel 和有缓冲的 channel 的区别? 在 Go 语言中,channel 是用来在 goroutines 之间传递数据的主要机制。它们有两种类型:无缓冲的 channel 和有缓冲的 channel。 无缓冲的 channel 行为:无缓冲的 channel 是一种同步的通信方式,发送和接收必须同时发生。如果一个 goroutine 试图通过无缓冲 channel

如何确定 Go 语言中 HTTP 连接池的最佳参数?

确定 Go 语言中 HTTP 连接池的最佳参数可以通过以下几种方式: 一、分析应用场景和需求 并发请求量: 确定应用程序在特定时间段内可能同时发起的 HTTP 请求数量。如果并发请求量很高,需要设置较大的连接池参数以满足需求。例如,对于一个高并发的 Web 服务,可能同时有数百个请求在处理,此时需要较大的连接池大小。可以通过压力测试工具模拟高并发场景,观察系统在不同并发请求下的性能表现,从而

SprinBoot+Vue网络商城海鲜市场的设计与实现

目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 application.yml3.5 SpringbootApplication3.5 Vue 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍:CSDN认证博客专家,CSDN平台Java领域优质创作者,全网30w+

【Go】go连接clickhouse使用TCP协议

离开你是傻是对是错 是看破是软弱 这结果是爱是恨或者是什么 如果是种解脱 怎么会还有眷恋在我心窝 那么爱你为什么                      🎵 黄品源/莫文蔚《那么爱你为什么》 package mainimport ("context""fmt""log""time""github.com/ClickHouse/clickhouse-go/v2")func main(