做善事,做坏事:用Go编写邪恶代码,第2部分

Go程序员的坏提示

图片

在出版物的第一部分,我解释了如何成为“恶意” Go程序员。 邪恶有多种形式,但是在编程中,它在于理解和维护代码的故意困难。 邪恶的程序会忽略语言的基本手段,而倾向于使用能带来短期利益以换取长期问题的技术。 简要提醒一下,Go的邪恶“做法”包括:

  • 命名和组织不佳的包裹
  • 接口组织不正确
  • 将指针传递给函数中的变量以填充其值
  • 使用恐慌代替错误
  • 使用初始化函数和空导入来配置依赖项
  • 使用初始化功能下载配置文件
  • 使用框架而不是库

邪恶大球


如果我们将所有邪恶的作法放在一起会怎样? 我们将拥有一个框架,该框架将使用许多配置文件,使用指针填写结构字段,定义用于描述已发布类型的接口,在出现问题时依靠“魔术”代码和恐慌。

而我做到了。 如果访问https://github.com/evil-go ,您将看到Fall ,这是一个DI框架,可让您实施所需的任何“邪恶”实践。 我用一个遵循相同原理的小型Outboy网络框架焊接了Fall。

您可能会问他们有多邪恶? 让我们看看。 我建议使用提供http端点的简单Go程序(使用最佳实践编写)。 然后使用Fall和Outboy重写它。

最佳实务


我们的程序位于一个名为greet的程序包中,该程序包使用所有基本功能来实现我们的端点。 既然这是一个示例,我们将使用内存中的DAO,其中包含三个将返回的值的字段。 我们还将提供一种方法,根据输入的不同,该方法将替换对数据库的调用并返回所需的问候语。

package greet type Dao struct { DefaultMessage string BobMessage string JuliaMessage string } func (sdi Dao) GreetingForName(name string) (string, error) { switch name { case "Bob": return sdi.BobMessage, nil case "Julia": return sdi.JuliaMessage, nil default: return sdi.DefaultMessage, nil } } 

接下来是业务逻辑。 为了实现它,我们定义了一个用于存储输出数据的结构,一个用来描述在数据搜索级别上正在寻找什么业务逻辑的GreetingFinder接口以及一个用于将业务逻辑本身与GreetingFinder字段一起存储的结构。 实际的逻辑很简单-它仅调用GreetingFinder并处理可能发生的任何错误。

 type Response struct { Message string } type GreetingFinder interface { GreetingForName(name string) (string, error) } type Service struct { GreetingFinder GreetingFinder } func (ssi Service) Greeting(name string)(Response, error) { msg, err := ssi.GreetingFinder.GreetingForName(name) if err != nil { return Response{}, err } return Response{Message: msg}, nil } 

然后是Web层,为此,我们定义了Greeter接口,该接口提供了我们需要的所有业务逻辑,以及包含使用Greeter配置的http处理程序的结构。 然后,我们创建一个方法来实现http.Handler接口,该接口拆分http请求,调用greeter(焊接),处理错误并返回结果。

 type Greeter interface { Greeting(name string) (Response, error) } type Controller struct { Greeter Greeter } func (mc Controller) ServeHTTP(rw http.ResponseWriter, req *http.Request) { result, err := mc.Greeter.Greeting( req.URL.Query().Get("name")) if err != nil { rw.WriteHeader(http.StatusInternalServerError) rw.Write([]byte(err.Error())) return } rw.Write([]byte(result.Message)) } 

问候包到此结束。 接下来,我们将看到一个“好的” Go开发人员将如何编写main来使用该程序包。 在主程序包中,我们定义了一个名为Config的结构,其中包含我们需要运行的属性。 然后main函数执行3件事。


 package main type Config struct { DefaultMessage string BobMessage string JuliaMessage string Path string } func main() { c, err := loadProperties() if err != nil { fmt.Println(err) os.Exit(1) } dao := greet.Dao{ DefaultMessage: c.DefaultMessage, BobMessage: c.BobMessage, JuliaMessage: c.JuliaMessage, } svc := greet.Service{GreetingFinder: dao} controller := greet.Controller{Greeter: svc} err = server.Start(server.Endpoint{c.Path, http.MethodGet, controller}) if err != nil { fmt.Println(err) os.Exit(1) } } 

这个例子很简短,但是它展示了Go的编写过程。 有些事情是模棱两可的,但总的来说很清楚发生了什么。 我们粘合专门为协同工作而设置的小型库。 什么都没有隐藏; 任何人都可以使用此代码,了解其各个部分如何连接在一起,并在必要时将它们重做为新的。

黑点


现在,我们将考虑Fall和Outboy的版本。 我们要做的第一件事是将greet包分成几个包,每个包都包含一个应用程序层。 这是DAO包。 它导入了我们的DI框架Fall,并且由于我们“邪恶”并相反地定义了与接口的关系,因此我们将定义一个名为GreetDao的接口。 请注意-我们已删除了所有指向错误的链接; 如果出了什么问题,我们会感到恐慌。 在这一点上,我们已经有不良的包装,不良的界面和不良的错误。 伟大的开始!

我们从一个很好的例子中稍微重命名了我们的结构。 现在,字段具有struct标签; 它们用于使Fall在字段中设置注册值。 我们的包装还具有初始化功能,通过该功能我们可以积累“邪恶力量”。 在包初始化函数中,我们两次调用Fall:

  • 一次注册提供结构标签值的配置文件。
  • 另一个是注册指向结构实例的指针。 Fall将能够为我们填写这些字段,并使DAO可供其他代码使用。

 package dao import ( "github.com/evil-go/fall" ) type GreetDao interface { GreetingForName(name string) string } type greetDaoImpl struct { DefaultMessage string `value:"message.default"` BobMessage string `value:"message.bob"` JuliaMessage string `value:"message.julia"` } func (gdi greetDaoImpl) GreetingForName(name string) string { switch name { case "Bob": return gdi.BobMessage case "Julia": return gdi.JuliaMessage default: return gdi.DefaultMessage } } func init() { fall.RegisterPropertiesFile("dao.properties") fall.Register(&greetDaoImpl{}) } 

让我们看一下服务包。 它导入DAO包,因为它需要访问在那里定义的接口。 服务包还会导入模型包,我们尚未考虑过-我们将在其中存储数据类型。 我们导入了Fall,因为它像所有“好的”框架一样,渗透到了所有地方。 我们还定义了一个服务接口来提供对Web层的访问。 同样,没有错误处理。

现在,我们的服务实现具有带有导线的结构标签。 当结构在Fall中注册时,标记为wire的字段会自动连接其依赖性。 在我们的小示例中,很清楚将为该字段分配什么。 但是在更大的程序中,您只会知道实现了GreetDao接口的某个位置,并且该接口已在Fall中注册。 您无法控制依赖行为。

接下来是我们的服务方法,已对其进行了稍微修改,以从模型包中获取GreetResponse结构,并删除了任何错误处理。 最后,我们在包中有一个init函数,用于在Fall中注册一个服务实例。

 package service import ( "github.com/evil-go/fall" "github.com/evil-go/evil-sample/dao" "github.com/evil-go/evil-sample/model" ) type GreetService interface { Greeting(string) model.GreetResponse } type greetServiceImpl struct { Dao dao.GreetDao `wire:""` } func (ssi greetServiceImpl) Greeting(name string) model.GreetResponse { return model.GreetResponse{Message: ssi.Dao.GreetingForName(name)} } func init() { fall.Register(&greetServiceImpl{}) } 

现在让我们看一下模型包。 尤其是没有什么值得一看的。 可以看出,模型与创建模型的代码是分离的,只是将代码划分为多个层。

 package model type GreetResponse struct { Message string } 

在网络软件包中,我们有一个网络界面。 在这里,我们导入了Fall和Outboy,还导入了Web软件包所依赖的服务软件包。 由于框架只有在幕后集成时才能很好地协同工作,因此Fall具有特殊的代码来确保框架和Outboy能够一起工作。 我们还正在更改结构,使其成为我们的Web应用程序的控制器。 她有两个领域:

  • 第一个通过Fall连接到服务包中GreetService接口的实现。
  • 第二个是我们唯一的Web端点的路径。 从该程序包的init函数中注册的配置文件中为其分配值。

我们的http处理程序已重命名为GetHello,现在没有错误处理。 我们还有Init方法(带有大写字母),不应与init函数混淆。 初始化是一种魔术方法,在填充所有字段之后,将为Fall中注册的结构调用此方法。 在Init中,我们调用Outboy在使用Fall设置的路径中注册控制器及其端点。 查看代码,您将看到路径和处理程序,但未指定HTTP方法。 在Outboy中,方法名称用于确定处理程序响应的HTTP方法。 由于我们的方法称为GetHello,因此它会响应GET请求。 如果您不知道这些规则,您将无法理解他的要求。 是的,这很恶毒吗?

最后,我们调用init函数以在Fall中注册配置文件和控制器。

 package web import ( "github.com/evil-go/fall" "github.com/evil-go/outboy" "github.com/evil-go/evil-sample/service" "net/http" ) type GreetController struct { Service service.GreetService `wire:""` Path string `value:"controller.path.hello"` } func (mc GreetController) GetHello(rw http.ResponseWriter, req *http.Request) { result := mc.Service.Greeting(req.URL.Query().Get("name")) rw.Write([]byte(result.Message)) } func (mc GreetController) Init() { outboy.Register(mc, map[string]string{ "GetHello": mc.Path, }) } func init() { fall.RegisterPropertiesFile("web.properties") fall.Register(&GreetController{}) } 

它仅是为了显示我们如何运行该程序。 在主程序包中,我们使用空导入来注册Outboy和Web程序包。 并且主要函数调用fall.Start()以启动整个应用程序。

 package main import ( _ "github.com/evil-go/evil-sample/web" "github.com/evil-go/fall" _ "github.com/evil-go/outboy" ) func main() { fall.Start() } 

皮被破坏


这是一个使用我们所有邪恶的Go工具编写的完整程序。 这是一场噩梦。 她神奇地隐藏了程序的各个部分是如何组合在一起的,这使她很难理解她的工作。

但是,您必须承认,使用Fall和Outboy编写代码具有一定的吸引力。 对于一个很小的程序,您甚至可以说这是一种改进。 看看配置有多容易! 我几乎不需要代码就可以连接依赖项! 我使用该方法的名称注册了该方法的处理程序! 而且没有任何错误处理,一切看起来都很干净!

邪恶就是这样运作的。 乍一看,它确实很有吸引力。 但是随着程序的变化和增长,所有这些魔力只会开始干扰,使对正在发生的事情的理解变得复杂。 只有当您完全沉迷于邪恶时,您才会回头并意识到自己被困住了。

对于Java开发人员,这似乎很熟悉。 这些技术可以在许多流行的Java框架中找到。 如前所述,从1996年的1.0.2开始,我从事Java已有20多年的历史。 在许多情况下,Java开发人员是第一个在Internet时代编写大型企业软件时遇到问题的人。 我记得servlet,EJB,Spring和Hibernate刚刚出现的时代。 Java开发人员当时做出的决定是有道理的。 但是这些年来,这些技术显示出了自己的年龄。 诸如Go这样的较新语言旨在消除使用较旧技术时发现的痛点。 但是,随着Java开发人员开始学习Go并用它编写代码,他们应该记住,尝试从Java复制模式会产生不好的结果。

Go专为认真的编程而设计-适用于跨越数百个开发人员和数十个团队的项目。 但是对于Go来说,您需要以最有效的方式使用它。 我们可以选择邪恶或善良。 如果选择邪恶,我们可以鼓励年轻的Go开发人员在了解Go之前先改变他们的风格和技术。 或者我们可以选择好。 作为Go开发人员,我们的工作之一是教育年轻的Gopher(地鼠),以帮助他们理解构成我们最佳实践的原则。

遵循善良道路的唯一弊端是,您必须寻找另一种表达内心邪恶的方式。 也许尝试以30公里/小时的速度在联邦高速公路上行驶?

Source: https://habr.com/ru/post/zh-CN460911/


All Articles