Go语言SOLID实践系列二之开闭原则
@ 归零 | 星期六,十二月 25 日,2021 年 | 3 分钟阅读 | 更新于 星期六,十二月 25 日,2021 年

引用wikipedia上对开闭原则的解释:

在面向对象编程领域中,开闭原则 (The Open/Closed Principle, OCP) 规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。该特性在产品化的环境中是特别有价值的,在这种环境中,改变源代码需要代码审查,单元测试以及诸如此类的用以确保产品使用品质的过程。遵循这种原则的代码在扩展时并不发生改变,因此无需上述的过程。

在本文中,我们将通过实践的方式深入探索这一原则。

当我们不遵守开闭原则时

OCP的要求,我们可以在上面看到,Bob大叔在他的博客中提供了。我更喜欢这种定义开闭原则的方式,因为这样展示出它全部的亮点。 乍一看,我们可能会认为这是一个荒谬的要求。是的,说真的,我们应该如何在不修改的情况下扩展某些东西?意思是,有没有可能在不改变它的情况下改变一些事情? 通过检查下面的代码示例,我们可以看到某些结构不遵守此原则和可能带来的后果å。

import (
	"net/http"

	"github.com/ahmetb/go-linq"
	"github.com/gin-gonic/gin"
)

type PermissionChecker struct {
	//
	// some fields
	//
}

func (c *PermissionChecker) HasPermission(ctx *gin.Context, name string) bool {
	var permissions []string
	switch ctx.GetString("authType") {
	case "jwt":
		permissions = c.extractPermissionsFromJwt(ctx.Request.Header)
	case "basic":
		permissions = c.getPermissionsForBasicAuth(ctx.Request.Header)
	case "applicationKey":
		permissions = c.getPermissionsForApplicationKey(ctx.Query("applicationKey"))
	}
	
	var result []string
	linq.From(permissions).
		Where(func(permission interface{}) bool {
			return permission.(string) == name
		}).ToSlice(&result)

	return len(result) > 0
}

func (c *PermissionChecker) getPermissionsForApplicationKey(key string) []string {
	var result []string
	//
	// extract JWT from the request header
	//
	return result
}

func (c *PermissionChecker) getPermissionsForBasicAuth(h http.Header) []string {
	var result []string
	//
	// extract JWT from the request header
	//
	return result
}

func (c *PermissionChecker) extractPermissionsFromJwt(h http.Header) []string {
	var result []string
	//
	// extract JWT from the request header
	//
	return result
}

这个例子展示了一个结构体,即PermissionChecker。它应该检查是否有访问某些资源所需的权限,这取决于Gin包支持的网络应用程序的Context。

这里我们有一个主要的方法HasPermission,它检查特定名称的权限是否与Context中的数据相关。

从Context中检索权限可能会有所不同,这取决于用户是用JWT令牌、基本授权还是应用密钥进行授权。在这个结构中,我们提供了所有不同的权限片的提取。

如果我们遵守单一责任原则,PermissionChecker负责决定权限是否在Context内,它与授权过程没有任何关系。

一定是授权过程被定义在其他地方,在其他的结构中,甚至是不同的模块中。所以,如果我们想在其他地方扩展授权流程,我们也需要在这里调整逻辑。 假设我们想扩展授权逻辑并添加一些新的流程,比如将用户数据保留在会话中或使用摘要授权。在这种情况下,我们也需要对PermissionChecker进行调整。

这样的实现带来了问题爆发:

  • PermissionChecker保持最初在其他地方处理的逻辑。
  • 任何对授权逻辑的调整,可能是不同的模块,都需要在PermissionChecker中进行调整。
  • 要增加一种提取权限的新方法,我们总是需要修改PermissionChecker。
  • PermissionChecker内部的逻辑不可避免地随着每一个新的授权流程而增长。
  • PermissionChecker的单元测试包含了太多关于不同权限提取的技术细节。

所以,现在再次,我们有一些代码要重构。

如何遵守开闭原则

开放/封闭原则说软件结构应该对扩展开放,对修改封闭。

上面的声明为我们的新代码提供了一些可能的方向,它应该遵守OCP。这段代码应该提供一些允许从外部推送的扩展。

在面向对象编程中,我们通过对同一接口使用不同的实现来支持这种扩展。换句话说,我们使用多态性。

type PermissionProvider interface {
	Type() string
	GetPermissions(ctx *gin.Context) []string
}

type PermissionChecker struct {
	providers []PermissionProvider
	//
	// some fields
	//
}

func (c *PermissionChecker) HasPermission(ctx *gin.Context, name string) bool {
	var permissions []string
	for _, provider := range c.providers {
		if ctx.GetString("authType") != provider.Type() {
			continue
		}
		
		permissions = provider.GetPermissions(ctx)
		break
	}

	var result []string
	linq.From(permissions).
		Where(func(permission interface{}) bool {
			return permission.(string) == name
		}).ToSlice(&result)

	return len(result) > 0
}

在上面的例子中,我们可以看到一个尊重OCP的候选者。适配器PermissionChecker并没有隐藏从Context中提取权限的技术细节。

相反,我们引入了一个新的接口,PermissionProvider。这个新结构代表了放置不同权限提取逻辑的地方。

例如,它可以是JwtPermissionProvider,或ApiKeyPermissionProvider,或AuthBasicPermissionProvider。现在,负责授权的模块也可以包含权限的提取器。

这意味着我们可以把关于授权用户的逻辑放在一个地方,而不是把它散布在代码中。

另一方面,我们的主要目标是扩展PermissionChecker,而不需要修改它,现在可以实现了。我们可以用我们想要的不同的PermissionProviders来初始化PermissionChecker。

假设我们需要添加一种从session密钥获取权限的可能性。在这种情况下,我们需要实现一个新的SessionPermissionProvider,它将从Context中提取cookie并使用它从SessionStore中获取权限。

使之有可能在我们需要的时候扩展PermissionChecker,而不再修改其内部逻辑。现在我们看到了什么是对扩展的开放和对修改的封闭。

一些更多的例子

前面的问题我们可以用一个稍微不同的方法来解决。让我们看下面的代码片段。

type PermissionProvider interface {
	Type() string
	GetPermissions(ctx *gin.Context) []string
}

type PermissionChecker struct {
	//
	// some fields
	//
}

func (c *PermissionChecker) HasPermission(ctx *gin.Context, provider PermissionProvider, name string) bool {
	permissions := provider.GetPermissions(ctx)

	var result []string
	linq.From(permissions).
		Where(func(permission interface{}) bool {
			return permission.(string) == name
		}).ToSlice(&result)

	return len(result) > 0
}

通过新的实现,我们从PermissionChecker中删除了PermissionProviders。相反,我们将正确的provider定义为HasPermission方法的一个参数。

我更喜欢第一种方法,但这个方法也可能是一个解决方案,这取决于我们应用程序中的使用情况。

我们可以将 “开/闭原则 “应用于方法,而不仅仅是结构。如下:

func GetCities(sourceType string, source string) ([]City, error) {
	var data []byte
	var err error

	if sourceType == "file" {
		data, err = ioutil.ReadFile(source)
		if err != nil {
			return nil, err
		}
	} else if sourceType == "link" {
		resp, err := http.Get(source)
		if err != nil {
			return nil, err
		}

		data, err = ioutil.ReadAll(resp.Body)
		if err != nil {
			return nil, err
		}
		defer resp.Body.Close()
	}

	var cities []City
	err = yaml.Unmarshal(data, &cities)
	if err != nil {
		return nil, err
	}

	return cities, nil
}

函数GetCities从某个来源读取城市列表。这个来源可能是一个文件或互联网上的一些资源。不过,我们将来可能想从内存、Redis或其他任何来源读取数据。

所以在某种程度上,让读取原始数据的过程更抽象一些会更好。既然如此,我们可以从外部提供一个读取策略作为方法参数。

type DataReader func(source string) ([]byte, error)

func ReadFromFile(fileName string) ([]byte, error) {
	data, err := ioutil.ReadFile(fileName)
	if err != nil {
		return nil, err
	}

	return data, nil
}

func ReadFromLink(link string) ([]byte, error) {
	resp, err := http.Get(link)
	if err != nil {
		return nil, err
	}

	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	return data, nil
}

func GetCities(reader DataReader, source string) ([]City, error) {
	data, err := reader(source)
	if err != nil {
		return nil, err
	}

	var cities []City
	err = yaml.Unmarshal(data, &cities)
	if err != nil {
		return nil, err
	}

	return cities, nil
}

如上代码所示,在Go中,我们可以定义一个新的类型来嵌入函数。这里我们描述了一个新的类型,DataReader,代表了一个从某个源头读取原始数据的函数类型。

新方法ReadFromFile和ReadFromLink是DataReader类型的具体实现。GetCities方法期望DataReader的具体实现作为一个参数,然后在函数体内部执行并获取原始数据。

正如你所看到的,OCP的主要目的是给我们的代码和我们代码的用户提供更多的灵活性。只要有人能够扩展我们的库,而不fork它们,为它们提供pull requests,或以任何方式修改它们,我们的库就具有真正的价值。

总结

开闭原则是第二个SOLID原则,它代表字母O。它指导我们应该总是在不修改代码结构的情况下扩展它们。

在其源头,我们应该使用多态性来满足这一要求。我们的代码应该提供一个简单的接口来增加这种可扩展性。

参考: 1、https://levelup.gitconnected.com/practical-solid-in-golang-open-closed-principle-1dd361565452 2、https://zh.wikipedia.org/wiki/%E5%BC%80%E9%97%AD%E5%8E%9F%E5%88%99

© 2014 - 2022 Lionel's Blog

Powered by Hugo with theme Dream.