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 string
    Sign  string
}
type Data struct {
    Header Header
    Body   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 string
    sign  string
    data  *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.AppId
    apiRequest.sign = apiRequest.data.Header.Sign
    return 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对应的appkey
    appKey := apiAuthencator.credentialStorage.GetAppkeyByAppid(appId)
    //3.重新计算sign
    authToken := 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 = sign
   sendDataMarshal, _ := 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. 思考/读书笔记
相关文章
|
2月前
|
设计模式 数据库连接 PHP
PHP中的设计模式:提升代码的可维护性与扩展性在软件开发过程中,设计模式是开发者们经常用到的工具之一。它们提供了经过验证的解决方案,可以帮助我们解决常见的软件设计问题。本文将介绍PHP中常用的设计模式,以及如何利用这些模式来提高代码的可维护性和扩展性。我们将从基础的设计模式入手,逐步深入到更复杂的应用场景。通过实际案例分析,读者可以更好地理解如何在PHP开发中应用这些设计模式,从而写出更加高效、灵活和易于维护的代码。
本文探讨了PHP中常用的设计模式及其在实际项目中的应用。内容涵盖设计模式的基本概念、分类和具体使用场景,重点介绍了单例模式、工厂模式和观察者模式等常见模式。通过具体的代码示例,展示了如何在PHP项目中有效利用设计模式来提升代码的可维护性和扩展性。文章还讨论了设计模式的选择原则和注意事项,帮助开发者在不同情境下做出最佳决策。
|
3月前
|
设计模式 数据库连接 PHP
PHP编程中的面向对象与设计模式
在PHP编程世界中,掌握面向对象编程(OOP)和设计模式是提升代码质量和开发效率的关键。本文将深入浅出地介绍如何在PHP中应用OOP原则和设计模式,以及这些实践如何影响项目架构和维护性。通过实际案例,我们将探索如何利用这些概念来构建更健壮、可扩展的应用程序。
|
22天前
|
安全 Go 开发者
代码之美:Go语言并发编程的优雅实现与案例分析
【10月更文挑战第28天】Go语言自2009年发布以来,凭借简洁的语法、高效的性能和原生的并发支持,赢得了众多开发者的青睐。本文通过两个案例,分别展示了如何使用goroutine和channel实现并发下载网页和构建并发Web服务器,深入探讨了Go语言并发编程的优雅实现。
33 2
|
1月前
|
算法 Java 编译器
你为什么不应该过度关注go语言的逃逸分析
【10月更文挑战第21天】逃逸分析是 Go 语言编译器的一项功能,用于确定变量的内存分配位置。变量在栈上分配时,函数返回后内存自动回收;在堆上分配时,则需垃圾回收管理。编译器会根据变量的使用情况自动进行逃逸分析。然而,过度关注逃逸分析可能导致开发效率降低、代码复杂度增加,并且对性能的影响相对较小。编译器优化通常比人工干预更准确,因此开发者应更多关注业务逻辑和整体性能优化。
|
2月前
|
设计模式 Java Spring
spring源码设计模式分析-代理设计模式(二)
spring源码设计模式分析-代理设计模式(二)
|
1月前
|
算法 安全 Go
Python与Go语言中的哈希算法实现及对比分析
Python与Go语言中的哈希算法实现及对比分析
41 0
|
1月前
|
机器学习/深度学习 自然语言处理 Go
Python与Go在AIGC领域的应用:比较与分析
Python与Go在AIGC领域的应用:比较与分析
46 0
|
3月前
|
编译器 Go
Go中init()执行顺序分析
文章分析了Go语言中`init()`函数的执行顺序和时机,指出全局变量初始化后先于`init()`函数执行,而`init()`函数在`main()`函数之前执行,且包的`init()`函数按包的导入顺序进行初始化。
29 1
|
3月前
|
设计模式 存储 缓存
Guava 源码中7种设计模式的实现分析
V 哥在学习 Guava 源码中总结的7个设计模式的实现分析,欢迎关注威哥爱编程,做自己的技术,让别人去卷吧。
|
3月前
|
程序员 Go
Go 语言:面向对象还是非面向对象?揭开编程语言的本质
Go 语言:面向对象还是非面向对象?揭开编程语言的本质
下一篇
无影云桌面