Goal
I understand that the purpose of the question would be how to design an API to facilitate extension and maintenance.
Principles and reservations
A good approach would be to adopt SOLID principles. However, it has already been considered in the reply by @Douglas, such principles should be applied when they make sense, avoiding over-Engineering and early optimization.
For example, in a short-term project with one or two developers it’s easy to get over it.
On the other hand, if there is medium- or long-term investment perspective, if there are other project people or even other projects that will consume your API without knowing all the details of how it works, then it makes perfect sense and makes itif it is necessary to invest more time to design something robust.
Fred Brooks, no The Mythical Man-Month, estimates that developing software for reuse requires three times more effort.
Building a robust API
Let’s go to an example that is in the middle between academic and "market":
Build an API that supports default payments via billet and card and also enables new payment methods without modifying existing classes.
This API is integrated with an online store, which has the following stream:
- Upon closing the purchase, the User selects the type of payment
- The System attempts to make the payment and checks the result
- If successful, the system records the payment as done
- If it is necessary to wait for confirmation (as in the case of boleto), the system schedules another attempt to make the payment for the other day.
- If unsuccessful, the system sends a message to the user
Interfaces and basic classes
public enum Status {
ERRO, SUCESSO, AGUARDANDO_CONFIRMACAO
}
public class Resultado {
Status status;
String motivo;
}
public interface MetodoPagamento {
Resultado efetuar(Compra compra);
}
Pagamento
, the main class
The store system could then implement a payment service like this:
@Named
public class PagamentoService {
@Inject
PagamentoDao pagamentoDao;
@Inject
AgendamentoService agendamentoService;
@Inject
ErroPagamentoService erroPagamentoService;
// código que pode ser mais ou menos complexo dependendo
// de quantas integrações forem necessárias
Status efetuar(String usuario, MetodoPagamento metodo, Compra compra) {
Resultado r = metodo.efetuar(compra);
if (Status.ERRO == r.status) {
log.error("Erro...");
erroPagamentoService.notificarErroPagamento(usuario, compra, r.motivo);
} else if (Status.AGUARDANDO_CONFIRMACAO == r.status) {
log.info("Tenta novamente amanhã...");
agendamentoService.agendarVerificacaoPagamento(
usuario, metodo, compra);
} else {
log.info("Sucesso...");
pagamentoDao.inserir(usuario, compra);
}
return r;
}
}
Paying with billets
public class PagamentoBoleto implements MetodoPagamento {
public Resultado efetuar(Compra compra) {
int codigoDeBarras = CodigoDeBarras.gerar(compra);
boolean pagamentoDetectado = apiBanco.codigoDeBarrasFoiPago(codigoDeBarras);
if (pagamentoDetectado) {
return new Resultado(Status.SUCESSO, "");
}
return new Resultado(Status.AGUARDANDO_CONFIRMACAO, "Pagamento não detectado junto ao banco");
}
}
Paying by card
public class PagamentoCartao implements MetodoPagamento {
private DadosCartao cartao;
public PagamentoCartao(DadosCartao cartao) {
this.cartao = cartao;
}
public Resultado efetuar(Compra compra) {
int numeroTransacao = CartaoCredito.gerarNumeroTransacao(compra);
boolean conseguiuPagar = apiBanco.pagarComCartao(cartao, compra);
if (conseguiuPagar) {
return new Resultado(Status.SUCESSO, "Transação " + numeroTransacao + " efetuada com sucesso");
}
return new Resultado(Status.ERRO, "Banco rejeitou cartão " + cartao);
}
}
Controlling all this
Somewhere in the code (in the case of a web system, possibly a controller or endpoint), there will be a code that lists the types of payment and instance the respective payment method.
Example:
@POST("/pagar")
@Named
class PagamentoResource {
@Inject PagamentoService pagamentoService;
@POST("cartao")
public Response pagarComCartao(FormPagamentoCartao form) {
DadosCartao cartao = createDadosCartao(form);
MetodoPagamento metodo = new PagamentoCartao(cartao);
Copra compra = recuperarCompraDaSessao();
return pagar(metodo, compra);
}
@POST("boleto")
public Response pagarComBoleto(FormPagamentoBoleto form) {
DadosCartao cartao = createDadosCartao(form);
MetodoPagamento metodo = new PagamentoCartao(cartao);
Copra compra = recuperarCompraDaSessao();
return pagar(metodo, compra);
}
private Response pagar(MetodoPagamento metodo, Compra compra) {
Copra compra = recuperarCompraDaSessao();
String usuario = recuperarUsuarioLogado();
Resultado r = pagamentoService.efetuar(usuario, metodo, compra);
if (Status.SUCESSO == r.status) {
return Response.ok();
}
return createErrorResponse(r.motivo);
}
}
Obviously, the user interface should also reflect the available options. For example, in the case of the card, a form with the card details is displayed and in the case of the ticket only an image or PDF, but both are not part of the payment itself, so not included in the examples.
Auto-Discover or do not auto-Discover, that is the question
There is something that confuses developers a little when talking about SOLID principles, such as not changing existing classes when adding new code, leading those who know a little more of the language to soon think of using reflection to discover classes at runtime.
Although some libraries or frameworks reach this level, the most common interpretation is not that none existing class must be modified, but rather a minimum quantity class, preferably a single point in the system that controls the functionality in question.
A good test of whether the developer is properly applying standards and principles correctly in a code base is to count how many points in the system are affected by a point change. The less, the better.
Now, have you ever worked on a system in which, to add something even trivial you need to tinker with multiple classes from all layers of the application? And on top of that, you never have a guarantee that you haven’t missed a point? This is common when, instead of abstracting concepts in well-designed interfaces and classes, without realizing the programmer repeats the same logic or different aspects of the same logic at different points in the system (although the code is different, which is even worse).
Adding a new payment method
Without touching the code, I will just list what would be necessary to modify to add a payment, for example, via Paypal:
- Other implementation
PagamentoPaypal
.
- New endpoint (new method in class Resource)
- UI relating to the payment method
Automating to the extreme
Suppose we are developing a pluggable ERP and want to allow new payment methods without modifying the core.
Then one could think about using reflection to list the classes in the classpath, Osgi or some other technology.
In the ERP part, we would have to modify:
- Create a class to locate plugins. The plugin must follow an established format and provide the necessary parts for the interface, validation and payment.
- UI must lookup for all payment methods and list them.
- UI must be able to insert payment forms provided by plugins.
- The ERP should allow the plugin to add a new endpoint that validates and calls the payment. There are many ways to do this, but for this example suppose the plugin can provide a new class Resource with any endpoint.
Still in this case, a developer wanting to plug in a new method should:
- Implement new
MetodoPagamentoPaypal
- Implement endpoint in a new class Resource
- Provide UI with respective form to type of payment
Conclusions
- "Closed for modification" does not mean that no class of the system needs to be modified, but as little as possible or reasonable.
- Both proposed ways to add a new payment require exactly three additions or modifications: payment method, endpoint, UI.
- The "pluggable" proposal, while not modifying existing classes to add new payment methods, adds complexity and requires virtually the same implementation effort.
- Therefore, it is not superior in itself, but the choice of one or the other depends on who consumes the API, that is, whether someone is inside or outside the project.
+1 Very good, just one detail, the class
PagamentoResource
is with two methodspagarComCartao
, I think his intention was that the second method waspagarComBoleto
.– Douglas
Thank you, @Douglas.
– utluiz