Continuando nossa série de padrões de projeto, o segundo que iremos revisitar é o Prototype!

Este padrão visa ter um “planta” para gerar instâncias. Isso é muito útil em situações onde existem vários objetos com propriedades diferentes, onde seja custoso criar novos recursos e quando queremos especificar a instanciação da classe em tempo de execução.

Basicamente, para implementar um Prototype, temos que adicionar à classe original desejada um método que clona uma instância baseada na original. O próximo passo é criar uma espécie de “registro” que mantenha um cache dos objetos prototipados; seguido por uma classe de fábrica que clona o original e retorna o resultado.

O processo de clonagem é um dos aspecto mais importantes do Prototype, então, para entender melhor as nuances escondidas na clonagem de um objeto, vamos falar sobre Deep Cloning e Shallow Cloning.

Deep Cloning versus Shallow Cloning

Deep Cloning significa que, ao clonar, cada cópia terá sua própria referência. Se alterarmos um atributo da cópia, o original permanecerá intacto.

O mesmo não acontece com o Shallow Cloning: tanto a cópia quanto o original compartilham a mesma referência. Isso significa que as alterações feitas nos atributos de cópia também serão refletidas nos atributos do objeto original.

Implementando o Padrão Prototype em Java

Existem algumas “pegadinhas” ao implementar o Prototype em Java: A escolha óbvia, a interface Cloneable do Java, é comumente conhecida por ser defeituosa. Simplesmente não existe um mecanismo construído em Java para criar “deep clones” nativamente, o que significa que implementar Cloneable e chamar super.clone() sempre criará uma cópia superficial. Pior ainda, a interface Cloneable é uma classe marker que apenas marca a classe que a implementa como adequada para clonagem, e não possui um método clone() próprio (este método existe na classe Object).

Esses fatores argumentam por ser uma escolha melhor usar cópia via construtores para criar clones ao invés do Cloneable do Java.

Com isso em mente, vamos tentar usar o Prototype para resolver um problema. O conceito é o seguinte: Queremos acompanhar algumas Criptomoedas que foram compradas em 1º de janeiro de 2022. Os valores nesse período preenchem a instância original quando o aplicativo é iniciado. O usuário envia sua escolha de criptomoeda e o aplicativo chama um endpoint para obter informações em tempo real sobre essa criptomoeda específica, calcula a diferença entre o antes e o depois e envia de volta em um relatório.

Esse é o diagrama de arquitetura do aplicativo:

A classe de controller da aplicação é a seguinte:

@RestController
@AllArgsConstructor
public class CryprocurrencyController {
  private final CryptocurrencyService cryptocurrencyService;

  @GetMapping(value = "/crypto/{cryptocurrencyType}")
  public CryptocurrencyReport cryptocurrencyController(@PathVariable CryptocurrencyTypeEnum cryptocurrencyType) {
    return cryptocurrencyService.getCryptocurrencyReport(cryptocurrencyType);
  }
}

Então, o controller invoca a service:

@Service
@AllArgsConstructor
public class CryptocurrencyService {

  private final CryptocurrencyClient cryptocurrencyClient;
  private final CryptocurrencyLoader cryptocurrencyLoader;

  public CryptocurrencyReport getCryptocurrencyReport(CryptocurrencyTypeEnum cryptocurrencyTypeEnum){

    //Get original Cryptocurrency instance
    Cryptocurrency cryptocurrencyAtPurchase = cryptocurrencyLoader.getCryptocurrencyLoaded(cryptocurrencyTypeEnum);

    //Get Cryptocurrency cloned with updated values
    Cryptocurrency cryptocurrencyNow = getUpdatedCryptocurrencyValues(cryptocurrencyTypeEnum);

    //Returns the report calculating variation between purshased value and current value
    return CryptocurrencyReport.builder()
        .cryptocurrencyCurrentValues(cryptocurrencyNow)
        .cryptocurrencyPurchaseValues(cryptocurrencyAtPurchase)
        .rankVariation(calculateVariation(cryptocurrencyAtPurchase.getRank(), cryptocurrencyNow.getRank()))
        .priceUSDVariation(calculateVariation(cryptocurrencyAtPurchase.getPriceUSD(), cryptocurrencyNow.getPriceUSD()))
        .volume24Variation(calculateVariation(cryptocurrencyAtPurchase.getVolume24(), cryptocurrencyNow.getVolume24()))
        .marketCapUSDVariation(calculateVariation(cryptocurrencyAtPurchase.getMarketCapUSD(), cryptocurrencyNow.getMarketCapUSD()))
        .build();
  }

  private Cryptocurrency getUpdatedCryptocurrencyValues(CryptocurrencyTypeEnum cryptocurrencyTypeEnum){
    //Requests current Crypto's data 
    final CryptocurrencyDTO cryptocurrencyDTO =
        cryptocurrencyClient.getCryptocurrency(cryptocurrencyTypeEnum.getId()).iterator().next();
    //Loads original cripto values
    Cryptocurrency cryptocurrencyLoaded = cryptocurrencyLoader.getCryptocurrencyLoaded(cryptocurrencyTypeEnum);
    //Maps the current values to the original Criptocurrency object
    return updateLoadedPrototypeWithCurrentValues(cryptocurrencyDTO, cryptocurrencyLoaded);
  }

  private BigDecimal calculateVariation(BigDecimal valueBefore, BigDecimal valueNow){
    return valueNow.subtract(valueBefore);
  }

  private Integer calculateVariation(Integer valueBefore, Integer valueNow){
    return valueNow - valueBefore;
  }
}

Essa é a classe que realiza o mapeamento do objeto recebido para o clone:

@NoArgsConstructor
public class CryptocurrencyDTOMapper {

  public static Cryptocurrency updateLoadedPrototypeWithCurrentValues(CryptocurrencyDTO cryptocurrencyDTO, Cryptocurrency cryptocurrencyLoaded){
    cryptocurrencyLoaded.setRank(cryptocurrencyDTO.getRank());
    cryptocurrencyLoaded.setPriceUSD(cryptocurrencyDTO.getPriceUSD());
    cryptocurrencyLoaded.setVolume24(cryptocurrencyDTO.getVolume24());
    cryptocurrencyLoaded.setMarketCapUSD(cryptocurrencyDTO.getMarketCapUSD());
    return cryptocurrencyLoaded;
  }
}

A classe que carrega a instância de Cryptocurrency:

@Component
public class CryptocurrencyLoader {

  HashMap<String, Cryptocurrency> cryptocurrencyTable = new HashMap<>();
  //Returns crypto object based on the enum requested by the user
  public Cryptocurrency getCryptocurrencyLoaded(CryptocurrencyTypeEnum crypto){
    Cryptocurrency loadedCryptocurrency = cryptocurrencyTable.get(crypto.getId());
    //Here the cloning happens!
    return new Cryptocurrency(loadedCryptocurrency);
  }

  //Loads the crypto information on application startup
  @PostConstruct
  private void loadCryptoInfo1stJanuary2022(){
    Cryptocurrency bitcoin = Cryptocurrency.builder()
        .id(CryptocurrencyTypeEnum.BTC.getId())
        .name("BITCOIN")
        .symbol(CryptocurrencyTypeEnum.BTC)
        .rank(1)
        .volume24(new BigDecimal("24582667004"))
        .priceUSD(new BigDecimal("47686.81"))
        .marketCapUSD(new BigDecimal("902104193385"))
        .build();

    Cryptocurrency ethereum = Cryptocurrency.builder()
        .id(CryptocurrencyTypeEnum.ETH.getId())
        .name("ETHEREUM")
        .symbol(CryptocurrencyTypeEnum.ETH)
        .rank(1)
        .volume24(new BigDecimal("9776191466"))
        .priceUSD(new BigDecimal("3769.70"))
        .marketCapUSD(new BigDecimal("448537615143"))
        .build();

    cryptocurrencyTable.put(CryptocurrencyTypeEnum.BTC.getId(), bitcoin);
    cryptocurrencyTable.put(CryptocurrencyTypeEnum.ETH.getId(), ethereum);
  }
}

Finalmente, a classe de Cryptocurrency que realiza a clonagem através do construtor com um parâmetro:

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Cryptocurrency {

  private String id;

  private CryptocurrencyTypeEnum symbol;

  private String name;

  private Integer rank;

  @JsonProperty("price_usd")
  private BigDecimal priceUSD;

  @JsonProperty("volume_24")
  private BigDecimal volume24;

  @JsonProperty("market_cap_usd")
  private BigDecimal marketCapUSD;

  /*
  * Since the clone method from Cloneable class is broken, we use a copy constructor.
  * */
  public Cryptocurrency(Cryptocurrency cryptocurrency) {
    this(cryptocurrency.getId(),
        cryptocurrency.getSymbol(),
        cryptocurrency.getName(),
        cryptocurrency.getRank(),
        cryptocurrency.getPriceUSD(),
        cryptocurrency.getVolume24(),
        cryptocurrency.getMarketCapUSD());
  }
}

Você pode checar a implementação completa do projeto clicando aqui.

Referências:

Discussões interessantes sobre o Cloneable do Java:

Java: Clone and Cloneable > Programming.Guide

Java Cloning Problems > Vojtech Ruzicka’s Programming Blog

Copy constructors > Java Practices