Go语言 编译时保证安装正确性的生成器模式

ogq8wdun  于 2023-11-14  发布在  Go
关注(0)|答案(1)|浏览(97)

我创建了一个模块来发出HTTP请求,它的使用方式如下:

response, err := Get[ResponseType]("https://example.com").
  Query("foo", "bar").
  Header("Accept", "text/plain").
  Do(context.Background())

字符串
它使用构建器和泛型来简化HTTP请求。
虽然(IMO)构建器模式读起来很好,但我现在试图在编译时强制执行一些检查。例如,需要http.Client来进行实际的HTTP调用。我想让用户注入他们自己的http.Client或指示构建器使用默认的http.Client。像这样:

response, err := Get[ResponseType]("https://example.com").
  Client(myHttpClient).
  Do(context.Background())

// or

response, err := Get[ResponseType]("https://example.com").
  UseDefaultClient().
  Do(context.Background())


如果这两种指定客户端的方法都没有使用,那么我希望构建器在编译时失败。我尝试过使用泛型来实现这一点,但我还没有得到一个完全令人满意的解决方案。这是我到目前为止得到的:

// R is for response type, C is for config state

type Builder[R any, C any] struct {
    httpClient *http.Client
}

type NoClient struct{}

type WithClient struct{}

func Get[R any](requestUrl string) *Builder[R, NoClient] {
    return &Builder[R, NoClient]{ /* ... */ }
}

func (b *Builder[R, C]) Client(httpClient *http.Client) *Builder[R, WithClient] {
    b.httpClient = httpClient
    return (*Builder[R, WithClient])(b)
}

func (b *Builder[R, C]) UseDefaultClient() *Builder[R, WithClient] {
    b.httpClient = &http.Client{}
    return (*Builder[R, WithClient])(b)
}

func (b *Builder[R, C]) Header(key string, value string) *Builder[R, C] {
    /* add header */
    return b
}

func (b *Builder[R, C]) Do(ctx context.Context) (Response[R], error) {
    /* execute request */
}


如果类型是Builder[R, NoClient],我希望在编译时调用方法Do失败,否则成功。但是,我相信这是不可能的。编译器完全可以:

response, err := Get[ResponseType]("https://example.com").
    Do(context.Background())


因为Do方法的接收器指针没有约束(AFAIK也不可能添加一个)。
我发现的一个潜在的解决方案是使用函数而不是方法:

func ExecuteRequest[R any](
    ctx context.Context,
    builder *Builder[R, WithClient],
) (Response[R], error) {
    /* ... */
}


现在这在编译时失败,如所期望的:

response, err := ExecuteRequest(
    context.Background(),
    Get[ResponseType]("https://example.com")
)


我想知道是否有可能在保持Get[R](...).Do()方法的同时解决这个问题,虽然我使用了一个具体的例子(希望)使它更容易理解这个问题,但我认为在许多情况下,为构建器强制执行编译时保证是有用的。

sqyvllje

sqyvllje1#

所以,你基本上是想强迫你的构建器的用户在发出任何请求之前专门设置客户端。然后只限制他们的接口只有这两个方法。
实现目标的一种方法是使用只允许设置客户端的中间构建器,这里有一个例子:

type Response[T any] struct{}

type ClientBuilder[R any] struct {
    httpClient *http.Client
}

func (b *ClientBuilder[R]) WithClient(httpClient *http.Client) *Builder[R] {
    b.httpClient = httpClient
    return (*Builder[R])(b)
}

func (b *ClientBuilder[R]) DefaultClient() *Builder[R] {
    b.httpClient = &http.Client{}
    return (*Builder[R])(b)
}

type Builder[R any] struct {
    httpClient *http.Client
}

func Get[R any](requestUrl string) *ClientBuilder[R] {
    return &ClientBuilder[R]{ /* ... */ }
}

func (b *Builder[R]) Header(key string, value string) *Builder[R] {
    /* add header */
    return b
}

func (b *Builder[R]) Do(ctx context.Context) (Response[R], error) {
    /* execute request */
    return Response[R]{}, nil
}

func main() {
    // This will fail on compile
    response, err := Get[string]("https://example.com").
        Do(context.Background())

    // This wont
    response, err := Get[string]("https://example.com").
        DefaultClient().
        Do(context.Background())

    fmt.Println(response, err)
}

字符串
但是我认为有默认的客户端设置要求有点太冗长了,按照你最初的代码示例,我会完全删除UseDefaultClient,当用户决定不设置自定义的时候,我只在Do方法中使用一个:

func (b *Builder[R, C]) Do(ctx context.Context) (Response[R], error) {
    client := b.httpClient
    if client == nil {
        client = http.DefaultClient
    }
    /* execute request */
}

相关问题