大型GO代码库的前3个设计模式
#go #distributedsystems #designpatterns

介绍

设计模式通常被认为是每个优秀软件工程师都应该拥有的基本知识。也许是真的。但是,并非所有理论知识对于日常工作都是有用的。

我对设计模式在我的日常工作中的适用性不确定,在大型基于微服务的代码库中,为数百万用户提供服务。所以我决定调查。

我的目的不是描述所有古典软件设计模式。我专注于前3个设计模式,我经常注意到它的用法。我将解释这些模式中的每一种都解决了哪些特定的现实问题。

设计模式的简短历史

让我们刷新我们对软件工程设计模式如何产生的了解。 1994年,四位作者 - 埃里希·加玛(Erich Gamma),约翰·弗里西德(John Vlissides),拉尔夫·约翰逊(Ralph Johnson)和理查德·赫尔姆(Richard Helm)出版了一本书“Design Patterns: Elements of Reusable Object-Oriented Software”。这本书赢得了四人的昵称 - 后来缩写为Gof Book。

GOF书包含23种分为三组的模式:创造,结构和行为模式。这本书的示例是用C ++和Smalltalk编写的。可以预料的是,所描述的模式受这些特定语言和当时的软件工程需求的影响。但是,模式的概念被证明是持久的。

今天,我们仍然将这23种分为三类的模式称为经典软件设计模式。所有这些模式的示例实现都用所有流行语言编写,包括GO中的实现。最新的是我特别感兴趣的,因为我主要与我自己一起工作。

为什么使用设计模式?

因此,设计模式存在,但是为什么要使用它们?它们帮助工程师避免在解决众所周知或重复反复出现的问题时重新发明轮子。此外,设计模式有助于有效地在软件工程师之间传达想法。

模式的知识也可以帮助工程师在习惯陌生的代码库中。如果代码库的大小如何,那么如果可以用熟悉和通用的东西映射它的想法,那么它将变得不那么压倒性。设计模式为此类映射提供了框架。

策略:清晰度和可读性的计划

策略:旨在实现特定目的的计划
剑桥词典

在软件工程中,策略是一种行为设计模式,允许对象具有一组可互换的行为,并且能够根据当前的需求在运行时选择其中一个。

策略模式的典型用例是端点请求处理程序。处理程序的行为必须根据请求参数有所不同,但是复杂性并不能为每种情况分开终点。

让我们以一种称为权威性报告的微服务为例。它具有单个端点/get-Report ,生成了很好的报告。可以请求不同类型的报告,例如授权角色,授权政策或作业(用户及其授权之间的连接)。

Image description

此示例的代码在两个软件包之间划分 - handler 报告。 Handler函数 getReport 呼叫 build 由报告软件包公开,并将其传递给请求的报告类型。报告类型可以是角色,政策或作业。

package "handler"

func (h *Handler) GetReport(ctx context.Context, body *reportproto.GetReportRequest) (*reportproto.GetReportResponse, error) {
    // Generate report
    result, err := report.Build(ctx, body.ReportType)
    if err != nil {
        return nil, error
    }

    return &reportproto.ReportResponse{report: result}, nil
}

和此处报告软件包。当调用 build 函数时,它将根据提供的类型选择报告策略。

package "report"

func Build(ctx context.Context, reportType string) (Report, error) {
    // We choose strategy to build report
    var reportBuilder reporter
    switch reportType {
    case constants.ReportTypeUsers:
        reportBuilder = allUsersReporter{}
    case constants.ReportTypeRoles:
        reportBuilder = roleReporter{}
    case constants.ReportTypePolicies:
        reportBuilder = policyReporter{}
    default:
        return nil, fmt.Errorf("invalid_report_type: Invalid report type provided")
    }

    report, err := reportBuilder.build(ctx, w)

    return report, err
}

策略模式是一种简单而干净的方法,可以创建不同的行为而不会使代码与许多IF/其他条件超载,从而保持清晰度和可读性。

立面:与第三方服务提供商进行沟通的方式。

false(虚假外观):虚假的外观,使某人或某物看起来比实际上更愉快或更好的外观
剑桥词典

在软件工程中,通过使用立面,我们可以隐藏实际实体实现的内在复杂性,后面是简化的外部接口,该界面仅暴露了外部系统所需的方法。这提供了一个简单且用户友好的接口,可以与。

进行通信。

在基于微服务的体系结构中,我看到这种模式被用于与第三方服务进行交流。例如,用于管理文件或温室以协助招聘的文档。每个立面都是桥接内部公司服务和外部第三方API的自主微服务。这提供了构建通用集成的灵活性,可以将可以缩放和向下缩放,像任何内部服务一样受到监控。 P>

Image description

代码示例演示了一个端点处理程序 reademployee ,负责使用 client.getemployeee 从外部HR系统中获得ID员工。然后,使用 conshaling.employeetoproto 将员工数据转换为Protobuf对象形状,然后再返回请求者。

package handler

func (h *Handler) HandleGETReadEmployee(ctx context.Context, body *hrsystemproto.GETReadEmployeeRequest) (*hrsystemproto.GETReadEmployeeResponse, error) {
    if body.EmployeeId == "" {
        return nil, validation.ErrMissingParam("employee_id")
    }

    // client is based on Go net/http package and does request to 3rd party system
    employee, err := client.GetEmployee(ctx, body.EmployeeId)
    if err != nil {
        return nil, fmt.Errorf("failed to read employee")
    }

    marshalled, err := marshaling.EmployeeToProto(ctx, employee)
    if err != nil {
        return nil, fmt.Errorf("failed to marshal employee")
    }
    return &hrsystemproto.GETReadEmployeeResponse{
        Employee: marshalled,
    }, nil
}

使用立面提供了一些好处。工程师的端点强度有限,其中已经解决了与请求身份验证,可选参数或对象转换相关的复杂性。

出现新的第三方集成的需要时,建造它是一项快速简便的任务,因为这样做的全面蓝图已经存在。

粉丝/粉丝/粉丝:利用并发速度更快地获得结果

fan-out:散布在广阔的区域
剑桥词典。

此设计模式属于并发模式组,并且不在GOF书籍之外。但是,该模式在分布式系统中无处不在。因此,我想讨论它。

这种模式的概念是将数据检索任务分为多个块,同时执行它们,然后汇总结果。

应该以某种方式处理每个项目的项目数组是一个简单但现实的例子。想象一下数组很大或项目的处理时间很长(例如,每个项目都会产生/需要HTTP调用)。

Image description

我将查看微服务权威性报告作为使用粉丝/风扇淘汰模式的示例。我们已经在策略设计模式部分中讨论了这一微服务。注意到阻止服务一次实施多种设计模式。

让我们说,该服务收到了生成作业报告的请求。在此服务领域,任务是指公司员工与授予该员工的授权之间的联系。预计任务报告将提供有关所有公司雇员和目前授予的所有授权的信息。

创建此类报告需要大量网络请求,并且在员工的基础上依次按照员工进行依次进行,大约需要30分钟才能完成。

粉丝出口/粉丝模式进行了救援。值得注意的是,在剪切的代码中,我们不会为每个员工带来goroutine,因为它将在所有下游服务的请求中造成超过一千多个goroutines和不健康的尖峰。我们使用信号量将我们的扇形宽度限制在100 goroutines中。

func createReportRows(ctx context.Context, employeeProfiles []profile) (map[string]reportRow, error) {
    numOfEmployees := len(employeeProfiles)
    rowsChan := make(chan reportRow, numOfEmployees)
    concurrencyLimiter := semaphore.NewWeighted(100)

    // Fan-out stage, create rows of assignment report concurrenty
    for _, employee := range employeeProfiles {
        // Acquire a "token" from semaphore, block if none available.
        if err := concurrencyLimiter.Acquire(ctx, 1); err != nil {
            return nil, err
        }

        go func(curProfile profile, rowsChan chan<- reportRow, concurrencyLimiter <-chan struct{}) {
            // Release the "token" back to semaphore
            defer concurrencyLimiter.Release(1)

            rowsChan <- newRow(ctx, curProfile)
        }(employee, rowsChan, concurrencyLimiter)
    }

    // Fan-in stage, all created rows go into one data structure
    // As we know the exact number of rows, we can use a simple for loop
    // and wait for all of them to be created and sent to the channel.
    rows := make(map[string]reportRow, numOfEmployees)
    for i := 0; i < numOfEmployees; i++ {
        assignment := <-rowsChan
        rows[assignment.id] = assignment
    }

    return rows, nil
}

fan-out/fan-in是现代软件工程景观中最有用和无处不在的模式之一,因为一一处理的事情还不够。

讨论的设计模式的回顾

证明的设计模式的使用证明,这些模式不仅仅是理论构造。它们是生产代码库不可或缺的一部分,并通过重新实现了解决反复问题的有效模式来帮助维持高编码标准。

除了经典的设计模式外,并发模式还成为当代软件开发的重要组成部分。我迅速查看了这组图案,并使用广泛使用的风扇/风扇形式。

这就是我对可以应用于软件工程师日常工作的设计模式的看法。我从经验中探索了前三名,但探索并不停止。设计模式已经超过了GOF的原始23,超越了并发模式的添加。有建筑模式,消息传递模式,还有更多需要探索!