OlĂĄ Habr! Apresento a vocĂȘ a tradução do artigo OpçÔes funcionais sobre esterĂłides de MĂĄrk SĂĄgi-KazĂĄr .
As opçÔes funcionais sĂŁo o paradigma da Go para APIs limpas e extensĂveis. Ela Ă© popularizada por Dave Cheney e Rob Pike . Esta postagem Ă© sobre prĂĄticas que existem em torno do modelo desde que foi introduzido pela primeira vez.As opçÔes funcionais surgiram como uma maneira de criar APIs boas e limpas com uma configuração que inclui parĂąmetros opcionais. HĂĄ muitas maneiras Ăłbvias de fazer isso (construtor, estrutura de configuração, setters etc.), mas quando vocĂȘ precisa passar por dezenas de opçÔes, elas sĂŁo mal lidas e nĂŁo fornecem APIs tĂŁo boas quanto as opçÔes funcionais.Introdução - o que sĂŁo opçÔes funcionais?
Geralmente, quando vocĂȘ cria um "objeto", chama o construtor com os argumentos necessĂĄrios:obj := New(arg1, arg2)
(Vamos ignorar o fato de que não hå construtores tradicionais no Go.)As opçÔes funcionais permitem estender a API com parùmetros adicionais, transformando a linha acima no seguinte:
obj := New(arg1, arg2)
obj := New(arg1, arg2, myOption1, myOption2)
As opçÔes funcionais sĂŁo basicamente argumentos de uma função variĂĄvel que assume como parĂąmetros um tipo de configuração composto ou intermediĂĄrio. Devido Ă sua natureza variĂĄvel, Ă© perfeitamente aceitĂĄvel chamar o construtor sem opçÔes, mantendo-o limpo, mesmo se vocĂȘ quiser usar os valores padrĂŁo.Para demonstrar melhor o padrĂŁo, vamos dar uma olhada em um exemplo realista (sem opçÔes funcionais):type Server struct {
addr string
}
func NewServer(addr string) *Server {
return &Server {
addr: addr,
}
}
Depois de adicionar a opção de tempo limite, o código fica assim:type Server struct {
addr string
timeout time.Duration
}
func Timeout(timeout time.Duration) func(*Server) {
return func(s *Server) {
s.timeout = timeout
}
}
func NewServer(addr string, opts ...func(*Server)) *Server {
server := &Server {
addr: addr,
}
for _, opt := range opts {
opt(server)
}
return server
}
A API resultante Ă© fĂĄcil de usar e fĂĄcil de ler:
server := NewServer(":8080")
server := NewServer(":8080", Timeout(10 * time.Second))
server := NewServer(":8080", Timeout(10 * time.Second), TLS(&TLSConfig{}))
Para comparação, eis a aparĂȘncia da inicialização usando o construtor e a estrutura de configuração da configuração:
server := NewServer(":8080")
server := NewServerWithTimeout(":8080", 10 * time.Second)
server := NewServerWithTimeoutAndTLS(":8080", 10 * time.Second, &TLSConfig{})
server := NewServer(":8080", Config{})
server := NewServer(":8080", Config{ Timeout: 10 * time.Second })
server := NewServer(":8080", Config{ Timeout: 10 * time.Second, TLS: &TLSConfig{} })
A vantagem de usar opçÔes funcionais sobre o construtor Ă© provavelmente Ăłbvia: elas sĂŁo mais fĂĄceis de manter e ler / gravar. Os parĂąmetros funcionais tambĂ©m excedem a estrutura de configuração quando os parĂąmetros nĂŁo sĂŁo passados ââpara o construtor. Nas seçÔes a seguir, mostrarei ainda mais exemplos em que a estrutura de configuração pode nĂŁo corresponder Ă s expectativas.Leia o histĂłrico completo das opçÔes funcionais clicando nos links da introdução. (nota do tradutor - um blog com um artigo original de Dave Cheney nem sempre estĂĄ disponĂvel na RĂșssia e pode exigir uma conexĂŁo VPN para visualizĂĄ-la. AlĂ©m disso, as informaçÔes do artigo estĂŁo disponĂveis no vĂdeo de seu relatĂłrio no dotGo 2014 )PrĂĄticas de OpçÔes Funcionais
As opçÔes funcionais em si nada mais sĂŁo do que funçÔes passadas ao construtor. A facilidade de uso de funçÔes simples oferece flexibilidade e possui grande potencial. Portanto, nĂŁo surpreende que, ao longo dos anos, muitas prĂĄticas tenham aparecido em torno do modelo. Abaixo, fornecerei uma lista do que considero as prĂĄticas mais populares e Ășteis.Envie-me um e-mail se achar que algo estĂĄ faltando.Tipo de opçÔes
A primeira coisa que vocĂȘ pode fazer ao aplicar o modelo de opçÔes funcionais Ă© determinar o tipo da função opcional:
type Option func(s *Server)
Embora isso possa nĂŁo parecer uma grande melhoria, o cĂłdigo se torna mais legĂvel usando um nome de tipo em vez de definir uma função:func Timeout(timeout time.Duration) func(*Server) { }
func NewServer(addr string, opts ...func(s *Server)) *Server
func Timeout(timeout time.Duration) Option { }
func NewServer(addr string, opts ...Option) *Server
Outra vantagem de ter um tipo de opção é que Godoc coloca as funçÔes de opção sob o tipo:
Lista de tipos de opção
Normalmente, parĂąmetros funcionais sĂŁo usados ââpara criar uma Ășnica instĂąncia de algo, mas esse nem sempre Ă© o caso. Reutilizar a lista de parĂąmetros padrĂŁo ao criar vĂĄrias instĂąncias tambĂ©m nĂŁo Ă© incomum:defaultOptions := []Option{Timeout(5 * time.Second)}
server1 := NewServer(":8080", append(defaultOptions, MaxConnections(10))...)
server2 := NewServer(":8080", append(defaultOptions, RateLimit(10, time.Minute))...)
server3 := NewServer(":8080", append(defaultOptions, Timeout(10 * time.Second))...)
Esse cĂłdigo nĂŁo Ă© muito legĂvel, especialmente considerando que o objetivo do uso de opçÔes funcionais Ă© criar APIs amigĂĄveis. Felizmente, hĂĄ uma maneira de simplificar isso. SĂł precisamos dividir a opção [] diretamente com a opção:
func Options(opts ...Option) Option {
return func(s *Server) {
for _, opt := range opts {
opt(s)
}
}
}
Depois de substituir a fatia pela função OpçÔes, o código acima se torna:defaultOptions := Options(Timeout(5 * time.Second))
server1 := NewServer(":8080", defaultOptions, MaxConnections(10))
server2 := NewServer(":8080", defaultOptions, RateLimit(10, time.Minute))
server3 := NewServer(":8080", defaultOptions, Timeout(10 * time.Second))
Prefixos das opçÔes With / Set
As opçÔes geralmente sĂŁo tipos complexos, diferentemente de um tempo limite ou do nĂșmero mĂĄximo de conexĂ”es. Por exemplo, no pacote do servidor, vocĂȘ pode definir a interface do Agente de Log como uma opção (e usĂĄ-la se nĂŁo houver um agente de log padrĂŁo):type Logger interface {
Info(msg string)
Error(msg string)
}
Obviamente, o nome do criador de logs nĂŁo pode ser usado como o nome da opção, pois jĂĄ Ă© usado pela interface. VocĂȘ pode chamar a opção LoggerOption, mas nĂŁo Ă© um nome muito amigĂĄvel. Se vocĂȘ olhar para o construtor como uma frase, no nosso caso a palavra com: WithLogger vem Ă mente.func WithLogger(logger Logger) Option {
return func(s *Server) {
s.logger = logger
}
}
NewServer(":8080", WithLogger(logger))
Outro exemplo comum de uma opção de tipo complexo é uma fatia de valores:type Server struct {
whitelistIPs []string
}
func WithWhitelistedIP(ip string) Option {
return func(s *Server) {
s.whitelistIPs = append(s.whitelistIPs, ip)
}
}
NewServer(":8080", WithWhitelistedIP("10.0.0.0/8"), WithWhitelistedIP("172.16.0.0/12"))
Nesse caso, a opção geralmente acrescenta os valores aos existentes, em vez de substituĂ-los. Se vocĂȘ precisar sobrescrever um conjunto de valores existente, poderĂĄ usar a palavra set no nome da opção:func SetWhitelistedIP(ip string) Option {
return func(s *Server) {
s.whitelistIPs = []string{ip}
}
}
NewServer(
":8080",
WithWhitelistedIP("10.0.0.0/8"),
WithWhitelistedIP("172.16.0.0/12"),
SetWhitelistedIP("192.168.0.0/16"),
)
Predefinição de modelo
Casos de uso especiais sĂŁo geralmente universais o suficiente para apoiĂĄ-los na biblioteca. No caso de uma configuração, isso pode significar um conjunto de parĂąmetros agrupados e usados ââcomo uma predefinição para uso. Em nosso exemplo, o servidor pode ter cenĂĄrios de uso aberto e interno que definem tempos limite, limites de velocidade, nĂșmero de conexĂ”es etc. de diferentes maneiras.
func PublicPreset() Option {
return Options(
WithTimeout(10 * time.Second),
MaxConnections(10),
)
}
func InternalPreset() Option {
return Options(
WithTimeout(20 * time.Second),
WithWhitelistedIP("10.0.0.0/8"),
)
}
Embora as predefiniçÔes possam ser Ășteis em alguns casos, elas provavelmente sĂŁo de grande valor nas bibliotecas internas e, nas bibliotecas pĂșblicas, seu valor Ă© menor.Valores de tipo padrĂŁo x valores predefinidos padrĂŁo
Ir sempre tem valores padrĂŁo. Para nĂșmeros, geralmente Ă© zero, para tipos booleanos, Ă© falso e assim por diante. Em uma configuração opcional, Ă© considerado uma boa prĂĄtica confiar nos valores padrĂŁo. Por exemplo, um valor nulo deve significar um tempo limite ilimitado em vez de sua ausĂȘncia (o que geralmente nĂŁo faz sentido).Em alguns casos, o valor do tipo padrĂŁo nĂŁo Ă© um bom valor padrĂŁo. Por exemplo, o valor padrĂŁo para o Agente de Log Ă© nulo, o que pode levar ao pĂąnico (se vocĂȘ nĂŁo proteger as chamadas do agente com verificaçÔes condicionais).Nesses casos, definir o valor no construtor (antes de aplicar os parĂąmetros) Ă© uma boa maneira de determinar o fallback:func NewServer(addr string, opts ...func(*Server)) *Server {
server := &Server {
addr: addr,
logger: noopLogger{},
}
for _, opt := range opts {
opt(server)
}
return server
}
Vi exemplos com predefiniçÔes padrão (usando o modelo descrito na seção anterior). No entanto, não considero isso uma boa pråtica. Isso é muito menos expressivo do que apenas definir valores padrão no construtor:func NewServer(addr string, opts ...func(*Server)) *Server {
server := &Server {
addr: addr,
}
opts = append([]Option{DefaultPreset()}, opts...)
for _, opt := range opts {
opt(server)
}
return server
}
Opção de estrutura de configuração
Ter uma estrutura Config como uma opção funcional provavelmente não é tão comum, mas isso às vezes é encontrado. A ideia é que as opçÔes funcionais se refiram à estrutura de configuração em vez de se referirem ao objeto real que estå sendo criado:type Config struct {
Timeout time.Duration
}
type Option func(c *Config)
type Server struct {
config Config
}
Esse modelo Ă© Ăștil quando vocĂȘ tem muitas opçÔes, e a criação de uma estrutura de configuração parece mais limpa do que listar todas as opçÔes em uma chamada de função:config := Config{
Timeout: 10 * time.Second
}
NewServer(":8080", WithConfig(config))
Outro caso de uso para este modelo Ă© definir valores padrĂŁo:config := Config{
Timeout: 10 * time.Second
}
NewServer(":8080", WithConfig(config), WithTimeout(20 * time.Second))
Modelos avançados
Depois de escrever dezenas de opçÔes funcionais, vocĂȘ pode se perguntar se existe uma maneira melhor de fazer isso. NĂŁo em termos de uso, mas em termos de suporte a longo prazo.Por exemplo, e se pudĂ©ssemos definir tipos e usĂĄ-los como parĂąmetros:type Timeout time.Duration
NewServer(":8080", Timeout(time.Minute))
(Observe que o uso da API permanece o mesmo). Aoalterar o tipo de opção, podemos fazer isso facilmente:
type Option interface {
apply(s *Server)
}
A substituição de uma função opcional como interface abre a porta para vĂĄrias novas maneiras de implementar opçÔes funcionais:VĂĄrios tipos internos podem ser usados ââcomo parĂąmetros sem uma função de invĂłlucro:
type Timeout time.Duration
func (t Timeout) apply(s *Server) {
s.timeout = time.Duration(t)
}
As listas de opçÔes e estruturas de configuração (consulte as seçÔes anteriores) também podem ser redefinidas da seguinte maneira:
type Options []Option
func (o Options) apply(s *Server) {
for _, opt := range o {
o.apply(s)
}
}
type Config struct {
Timeout time.Duration
}
func (c Config) apply(s *Server) {
s.config = c
}
Meu favorito pessoal é a capacidade de reutilizar a opção em vårios construtores:
type ServerOption interface {
applyServer(s *Server)
}
type ClientOption interface {
applyClient(c *Client)
}
type Option interface {
ServerOption
ClientOption
}
func WithLogger(logger Logger) Option {
return withLogger{logger}
}
type withLogger struct {
logger Logger
}
func (o withLogger) applyServer(s *Server) {
s.logger = o.logger
}
func (o withLogger) applyClient(c *Client) {
c.logger = o.logger
}
NewServer(":8080", WithLogger(logger))
NewClient("http://localhost:8080", WithLogger(logger))
SumĂĄrio
ParĂąmetros funcionais Ă© um modelo poderoso para criar APIs limpas e extensĂveis com dezenas de parĂąmetros. Embora isso faça um pouco mais de trabalho do que o suporte a uma estrutura de configuração simples, ele oferece muito mais flexibilidade e APIs muito mais limpas do que as alternativas.