No.1 解释什么是地道的Go代码, 为什么你要遵循它。

2017-07-13  本文已影响0人  尼古拉斯河马

ACCEPT INTERFACES RETURN CONCRETE TYPES(接收接口返回的具体类型)

OUTLINES WHY YOU SHOULD WRITE YOUR LIBRARIES AND PACKAGES TO ACCEPT INTERFACES AND RETURN CONCRETE TYPES
要点:为什么你应该编写库和包用以接收接口和返回具体的类型。

Note this post was edited after taking on board feedback from reddit discussion
注意这个文章经过参与讨论的反馈后被编辑

I have heard mentioned several times this idea of accepting interfaces and returning concrete types. Here I will try to outline why I think this is good practice. It is important to note that this is intended as a principal to keep in mind when writing Go code, rather than an absolute rule.
我听到过很多次这种接收接口和返回具体类型的想法。在这里,我将尝试说明为什么本人认为这是一个优秀的写法。重要的注释:当在编写Go代码的时候,要记住这一点,然而这不是绝对的规则。

When writing libraries and packages our goal is for them to be consumed by someone. Either by our own code, but also, hopefully, by others too. We want to make this as simple and frictionless as possible. Accepting interfaces and returning structs can be a powerful way to achieve this. It allows the consumers of our packages to reduce the coupling between their code and yours. It helps clearly define the contract between API and the consumer, it makes it easier when consumers of your code are writing tests for the code that depends on your package. Lets look at some examples to help illustrate.
当我们在编写库和包的时候我们的目标是让他们被某人使用。或者是我们自己的代码,但也希望其他人也能怎么做。我们想让他尽可能的简单和无摩擦。接收接口和返回结构体可以实现这一点的强有力的方法。他允许我们的包的使用者减少他们的代码和你的代码之间的耦合。它有助于清晰的定义API和使用者之间的约定,当您的代码的使用者为依赖于您的包的代码编写测试时,它会更容易。让我们看一些例子来帮助说明。

Accepting Interfaces(接收接口)

Accepting a concrete type can limit the uses or our API and also cause difficulty for consumers of our code when it comes to testing. For example, if the public API of our library or package were to accept the concrete type os.File instead of the io.Writer interface, it would force consumers to use the same types in order to use our API. Now if we were instead to accept an interface, it would ensure that the requirements of our API are met, while not forcing a concrete type on the consumer.
接收具体类型可以限制用途或者API,并且在涉及到测试时也会对我们的代码的使用者造成困难。例如,如果我们的库和包的公有API是接收具体类型
os.Fileer而不是io.Writer接口,他将强制使用者使用相同的类型以使用我们的API。现在,如果我们改为接收一个接口,那么他将确保满足我们API的要求,而不会强制使用者使用具体类型。

Below is a contrived, simple example, but it is something you can often hit in real world code.
下面是一个精心设计的简单实例,但他是你经常可以在现实世界中使用的代码。

Using a concrete type(使用具体类型)

package myapi

import "os"

type MyWriter struct {}

func (mw *MyWriter) UpdateSomething(f *os.File) error {
    //code using the file to write ...
    return nil
}

func New() *MyWriter {
    return &MyWriterApi{}
}

So we mentioned how this pattern effect the consumers of our API. Lets look at how the above API would be consumed and tested:
所以我们提到这种模式如何影响我们API的使用者。 让我们看看上面的API将如何被使用和测试:

 package myconsumer 

  import (
      "github.com/someone/myapi"
  )

  func UseMyApi(doer *myapi.MyWriter, f *os.File)error{
      //do awesome business logic
      return doer.UpdateSomething(f)
  }

In our application code, at first, this seems ok as long as we only need to use files with this API. The difficulty shows itself best when we implement a test.
在我们的应用程序代码中,首先,只要我们只需要使用带有该API的文件就可以了。当我们执行一个测试的时候,这个难点表现的最好。

  package myconsumer_test 

  import (
      "github.com/someone/myconsumer"
      "os"
  )

  type mockDoer struct{}
  func (md mockDoer)UpdateSomething(f *os.File)error{
      return nil
  }

  func TestUseMyApi(t *testing.T){

      //we now need to get a concrete implementation of  *os.File somehow to use with our test.
      f,err := os.Open("/some/path/to/fixture/file.txt")
      if err != nil{
          t.Fatalf("failed to open file for test %s",err.Error())
      }
      defer f.Close()
      if err := myconsumer.UseMyApi(mockDoer{},f); err != nil{
          t.Errorf("did not expect error calling UseMyApi but got %s ", err.Error())
      }
       
  }

As the API implementation can only accept a concret os.File we are now forced to use a real file in our test. So how dow we solve this problem and make our API better for its consumers?
由于API的实现只能接收一个具体
os.File(系统文件),我们现在不得不在测试中使用一个真实的文件。因此,我们如何解决这个问题,让我们API更好为使用者服务?

Accepting an interface(接收一个接口)

Back to API code:(回到API代码)

package myapi

import "io"


type MyWriter struct {}

#Swithing from an io.File to an io.Writer makes things far easer for the consumer.
func (mw *myWriter) UpdateSomething(w io.Writer) error {
    //code using the writer to write ...
    return nil
}

func New() *MyWriter {
    return &MyWriterApi{}
}

Now our implementation takes anything that implements the builtin io.Writer interface. Although we are using a builtin interface here, in code, within specific business domains, this could well be a custom interface expressing the concerns of your domain. So how does this change impact our test code?
现在,我们实现需要实现内置的io.Writer接口的所有东西。虽然我们在这里使用一个内置接口,在代码中,在特定的业务领域,这可能是表达你关注的领域的自定义接口。因此这个变化如何影响我们的测试代码呢?

  package myconsumer_test 

  import (
      "github.com/someone/myconsumer"
      "io"
      "bytes"
  )

  type mockDoer struct{}
  func (md mockDoer)UpdateSomething(w io.Writer)error{
      return nil
  }

  func TestUseMyApi(t *testing.T){

      //we no longer need an actual file, all we need is something that 
      //implements the write method from io.Writer. bytes.Buffer is one such type.
      var b bytes.Buffer
    
      if err := myconsumer.UseMyApi(mockDoer{},&b); err != nil{
          t.Errorf("did not expect error calling UseMyApi but got %s ", err.Error())
      }
       
  }

Another advantage here is that if our UseMyApi function, also writes using the writer, we can assert based on the contents of the bytes.Buffer.
这里另一个优势是,如果我们的UseMyApi函数,也可以使用writer来写,我们可以基于bytes.Buffer的内容来断言。

Reducing our footprint and coupling(减少我们的封装和耦合)

Our libraries may use other libraries for some of its functionality. In our public API we should avoid exposing third party types to the consumers of our API. If our public API exposes a 3rd party type, then our consumers will also need to import and use that third party type. This couples there code to a dependency of your code and means they need to know too much about how the innards of your API works. It is a leaky abstraction. To avoid this we should either define our own types that internally can be translated to the required type or define and accept an interface.
我们的库可以使用其他的库来实现一些功能。在我们共有的API中,我们应该尽量避免第三方类型暴露给API的使用者。如果我们的共有API暴露了第三方类型,那么我们的使用者也需要导入和使用第三方类型,这将代码与代码的依赖关联起来,意味着他们需要了解API内容的工作原理。这是一个漏洞的抽象层,为了避免这种情况,我们应该定义我们自己的类型,内部可以将其转换为所需类型或定义并接受一个接口。

Returning Concrete Types(返回具体类型)

So what are the advantages of returning concrete types? If we want to accept interfaces, why would we not also return interfaces?
所以返回具体类型的优点是什么呢?如果我们想要接收接口,为什么我们不返回接口撒?

Navigating to the implementation code of a returned type, is something that you will often do when using a library even if it is only to get a better understanding of how something works. Returning an interface makes this far more difficult for the consumer as they will first navigate to an interface definition and then need to spend additional time trying to find the implementation code. Consumers of our code, my only be interested in a small subset of the functionality, while returning an interface doesn’t stop them from defining a new interface, nor does returning a concrete type. So given the uneeded indirection a returned interface will cause, it makes more sense and reduces friction to return a concrete type.
导航到返回类型的代码实现,这是你在使用库时经常做的事情,即使它仅仅是为了更好地了解某些工作原理。返回一个接口让使用者变得更加困难,因为它们将首先导航到接口定义,然后需要花费额外的时间尝试查找实现代码。我们代码的使用者,只对一小部分功能感兴趣,而返回接口并不能阻止他们定义新接口,也不会返回具体的类型。因此,考虑到返回的接口将会导致的无方向的间接影响,它会更有意义,减少摩擦以返回一个具体的类型。

原文地址

上一篇下一篇

猜你喜欢

热点阅读