Após retornar do API Days em Londres, estou estudando padrões de design para APIs. O último artigo que li foi “Arguments and Results” de James Noble (1), publicado em 1997 e ainda extremamente relevante. Parte do meu trabalho atual, desenvolver um data warehouse, é otimizar o transporte de grandes volumes de dados. E se eu quiser usar APIs, como posso fazer isso de forma eficiente? Há um consenso de que APIs REST não são adequadas em cenários de alto volume e estudando esse tópico cheguei ao Noble.
O artigo discute padrões para protocolos de objetos e para isso existem dois grupos:
- Como enviar objetos (Argumentos): Arguments object, Selector object and Curried object
- Como receber objetos (Resultadoss): Result object, Future object and Lazy object
Padrões
- Arguments object: Este padrão é utilizado para agilizar a assinatura de um método, consolidando argumentos e objetos comuns e modificando a assinatura do método para aceitar esse objeto consolidado. Traz diversas vantagens, você deixa seu domínio explícito; seus clientes podem utilizar o que têm em mãos (os objetos, em vez de valores separados); e você é capaz de separar a lógica de processamento da sua lógica de negócios
De
void Send-FixedIncome(EntityName, IssueDate, MaturityDate, PrincipalAmount, ...) { }
Para
void Send-FixedIncome(FixedIncome) { }
“adding an eleventh argument to a message with ten arguments is qualitatively quite different to adding a second argument to a unary message”
- Selector object: Quando você possui métodos com argumentos bastante semelhantes, este padrão introduz um seletor que permite escolher um deles. Noble mencionou que você poderia um enum, talvez usar GoF a Flyweight resolveria.
De
void CalculateHomeLoanAmount(HomeLoan, Collateral) { }
void CalculateHomeLoanAmount(HomeLoan) { }
void CalculateAutoLoanAmount(AutoLoan) { }
void CalculatePersonalLoanAmount(PersonalLoan) { }
Para
void CalculateAmount(LoanType, Loan) { } //Loan would be the interface for all loan types
“Protocols (APIs) where many messages perform similar functions are often difficult to learn and to use, especially as the similarity is often not obvious from the protocol’s documentation”
- Curried object: do mundo da programação funcional, o currying quebra uma função com vários argumentos em funções menores, onde cada uma dessas funções recebe alguns (geralmente um) dos argumentos originais; essas funções são chamadas em sequência então. Noble introduz esse padrão para um cenário onde o chamador não precisa fornecer todos os argumentos (por exemplo, constantes), o método pode fornecer em seu nome. Nos termos do GoF, ele atua como um proxy.
De
void SettleDebt(Loan, ParcelsToPay) { } # no. of parcels can be retrieved from the loan
Para
int GetOpenParcels(Loan) { }
void SettleDebt(Loan) { () => Settle(Loan) } # in Settle no. of parcels is retrieved
“These kinds of arguments increase the complexity of a protocol. The protocol will be difficult to learn, as programmers must work out which arguments must be changed, and which must remain constant.”
- Results object: Para cenários complexos, podem ser necessárias várias chamadas de método para gerar o resultado esperado. Seu objeto de resultados deve ser único com tudo nele. Este padrão pode ser visto como o outro lado do padrão Curried Object. Um aspecto que vale a pena mencionar é quando essas diversas chamadas significam vários sistemas. Nesse caso. um objeto de resultado ajuda a reduzir o acoplamento e atua como uma abstração em torno deles.
“Perhaps the computation returns more than on object”
- Future object: Este padrão é empregado quando precisamos executar uma operação demorada e realizar outras tarefas enquanto aguardamos a conclusão da operação. Em essência, nosso objetivo é processar um resultado de forma assíncrona e provavelmente invocar um retorno de chamada após a conclusão.
” Sometimes you need to ask a question, then do something else while waiting for the answer to arrive”
- Lazy object: Às vezes você precisa fornecer um resultado, embora não haja 100% de certeza se o método será chamado. A vantagem disso é que não obtemos dados desnecessariamente.
“Some computations can best be performed immediately but the computation’s result may never be needed”
Em suma, estes conceitos já são bastante difundidos. Qualquer linguagem de programação mainstream tem suporte para métodos assíncronos (Lazy object) e avaliação lenta (Lazy object). Além disso, um bom design OO faz com que o ‘Arguments object’ e o ‘Selector object’ apareçam naturalmente. Foi um bom artigo back-to-basics para me lembrar da importância de um bom design em meus métodos de API.