create objects dynamically

Asked

Viewed 1,162 times

4

I’m having a problem in college in software engineering which is the following: I have the following scheme when making the payment of a shopping cart I must know if it is BOLETO or CARTÃO_DE_CREDITO. If it is billet I give a random number representing the barcode, if it is a credit card I receive the number of the card and the number of installments that the customer wants and calculate the value of each installment (in this case the number of the card is allegorical).

I thought of something like this:

Java payment.

public interface Pagamento {

}

Paymentboleto.java

import java.util.Random;

public class PagamentoBoleto implements Pagamento{

    private final int codigoDeBarra;

    public PagamentoBoleto(final double valorCompra){
        this.codigoDeBarra = gerarCodigoDeBarra(valorCompra);
    }

    private int gerarCodigoDeBarra(final double valorCompra){
        long milisigundosAgora = System.currentTimeMillis();
        long semente = (long) (valorCompra + milisigundosAgora);

        Random random = new Random(semente);

        int min = 1000;
        int max = 9999;

        return random.nextInt((max - min) + 1) + min;
    }
}

Paymentcredit card.java

public class PagamentoCartaoCredito implements Pagamento{

    private final String numeroCartaoCredito;
    private final int quantidadeParcelas;

    public PagamentoCartaoCredito(final String numeroCartaoCredito, final int quantidadeParcelas){
        this.numeroCartaoCredito = numeroCartaoCredito;
        this.quantidadeParcelas = quantidadeParcelas;
    }

    public double getValorParcela(double valorCompra) {
        return valorCompra / quantidadeParcelas;
    }
}

Tipopagamentoenumfactory.java

public enum TipoPagamento {


 CARTAO(1) {
        public Pagamento getTipoPagamento() {
            return new PagamentoCartaoCredito();
        }
    },
    BOLETO(1) {
        public Pagamento getTipoPagamento() {
            return new PagamentoBoleto();
        }
    };

    public int op;

    TipoPagamento(int valor) {
        op = valor;
    }
    public abstract Pagamento getTipoPagamento();
}

Problem .... as the forms of payments have different builders do not know how to instance them in a very dynamic and flexible way.

3 answers

5

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:

  1. Upon closing the purchase, the User selects the type of payment
  2. The System attempts to make the payment and checks the result
    1. If successful, the system records the payment as done
    2. 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.
    3. 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:

  1. Other implementation PagamentoPaypal.
  2. New endpoint (new method in class Resource)
  3. 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:

  1. Implement new MetodoPagamentoPaypal
  2. Implement endpoint in a new class Resource
  3. 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

    +1 Very good, just one detail, the class PagamentoResource is with two methods pagarComCartao, I think his intention was that the second method was pagarComBoleto.

  • Thank you, @Douglas.

3

It seems to me unnecessary an Enum to do this, you can simply create two methods that will be "Factories" of Pagamento:

Obs.:It is better to use Bigdecimal than double for monetary values.

public class CriadorDePagamentos {

    public static Pagamento criarPagamento(final BigDecimal valorCompra) {
        return new PagamentoBoleto(valorCompra);
    }

    public static Pagamento criarPagamento(final BigDecimal valorCompra, final String numeroCartaoCredito, final int quantidadeParcelas) {
        return new PagamentoCartaoCredito(valorCompra, numeroCartaoCredito, quantidadeParcelas);
    }
}

And it could be used like this:

Pagamento pg = CriadorDePagamentos.criarPagamento(valor);

or so:

Pagamento pg = CriadorDePagamentos.criarPagamento(valor, numCartao, qtdParcelas);

Note that, the client code does not even need to know the types of payments available, being highly decoupled.

But this only makes sense to the client code if it finds what it needs within the interface it’s receiving, which in this case is Pagamento; within this interface we could have, for example, a method getValorCompra() that could be overwritten without so much trouble by PagamentoBoleto how much for PagamentoCartaoCredito, because these classes have a valorCompra.

The choice of the methods of the interface is a difficult subject that can lead to different models of architecture, each one can have its pros and cons, being also necessary to know the client codes of the interface to make sensible choices, and being interesting to consider the Principle of Segregation of Interfaces. Choices can have impacts such as proliferation of instaceof, Cast, may lead to creating operations on the interface that may be invalid depending on its implementation (such as the method Collection#add(E and)), or even lead to change in the return of Factories-methods for more concrete types, etc. It is not a simple subject to be addressed in this answer, but it is important.

  • 1

    About the code not need to know the type of payment, this becomes useful when you have some method in the interface. Finally, the modeling of the interface I believe should also be criticized

  • 1

    @Jeffersonquesado It’s true, but it seems to me that only one getValorCompra() would make sense on the interface, maybe a set as well (I would have to carefully analyze whether this would make sense, but I think not, because he even marked as final the instance variables, he must want immutability). Maybe @Gabriel didn’t put anything on the interface to focus the example on what matters to him, or because he doesn’t know yet what he’ll put on it. I will edit my reply later to talk about that question.

  • 1

    In addition to not putting the interface methods, @Gabriellemos also did not put the code that makes use of this interface. I believe that this would also help to better understand the issue and provide a solution. The current solution is good and pragmatic, but note that it violates the open-closed principle (a new type of payment requires modification of the Creators class). It’s just a point, maybe it doesn’t matter in a small application (why it’s important to also be pragmatic).

  • @Piovezan, without using reflection I do not see how not to have an open class for editing in architecture, because when creating a new class that implements the interface it will be necessary to connect it to the architecture in some way, ie some open class-to-edit will have to engage the new-class to provide instances of it and/or access to it. In this case, it is the Creators class that is open, and the idea is that through it the client classes are not open-for-editing. My aim is to avoid that several customer classes need to be edited when creating new Payment Types.

  • @Douglas I believe it is possible yes. It creates a class and adds itself to the architecture by means of new code, and not by changing code already written. But to visualize a solution.

  • @Piovezan the client codes of the interface really are missing, because through this we would know better what should be in the interface, and if it would be necessary to use Segregation of Interfaces to meet different client codes with different needs without acoplá-them to the Concrete Payment Classes. But from what I understand, the focus of the question is not the interface, but the instantiation of classes that share the same interface but have different constructors.

  • @Piovezan is very interesting this possibility that you are presenting: "A class is created and added to the architecture by means of new code, and not by changing code already written." but I still haven’t figured out how it would be done. You can give the link to an example?

  • @Douglas I believe the examples of the answers of that question demonstrate this. At least I believe this is the intention of the principle.

  • @Piovezan, these answers actually confirm what I said. For example, the last paragraph of Math’s answer, he’s describing exactly what I did here. On the other hand, the utluiz response is only applicable when we already have the instances to be processed in a uniform way, it is not about the creation of the instances that implement the interfaces. Without reflection, you can create a new class by implementing/extending something and putting it into the package, but your program will treat it as if it doesn’t even exist; so you will need to edit some "open" code to "add it" to the program.

  • @Douglas I believe that the Factory standard is slightly different from what you used in your reply, but I have little experience with the standard to say. In the case of the utluiz response, I understand that it proposes to create instances implementing the interfaces IntegracaoBanco and DadosPagamento thus avoiding tampering with existing code.

Show 5 more comments

0

The problem seems to me very simple to have a Factory.

I’m having a problem in college in software engineering which is next: I have the following scheme when making the payment of a shopping cart should know if it is BOLETO or CARTÃO_DE_CREDITO. Case be billet i Gero a random number representing the code of bar, if it is credit card I get the card number and the number of installments the customer wants and calculate the value of each instalment (in this case the card number is allegorical).

See my suggestion:

public interface Pagamento {

}

class Boleto implements Pagamento {
    Boleto(valorCompra) { /*construtor*/ }
}

class Cartao implements Pagamento {
    Cartao (valorCompra, numeroCartao, numeroParcelas) { /*construtor*/ }
}

And using:

Pagamento pagamento = new Boleto(valorCompra);
Pagamento pagamento = new Cartao(valorCompra, numeroCartao, numeroParcelas);

And because you want something more flexible (dynamic), you can extend these behaviors well using only Decorator.

And a hint: Use BigDecimal instead of Double.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.