Como desemaranhar uma classe de dados onde as anotações de diferentes frameworks se misturam?

5

O problema do mix de framework

Estou usando dois frameworks - SDK SCIM 2 do PingIdentity e Spring LDAP - para desserializar um recurso SCIM (ou seja, JSON) para um objeto Java em seguida, escreva-o em um diretório LDAP e vice-versa. O problema é que ambas as estruturas querem anotar os campos da minha classe de dados Java e isso está ficando muito confuso. Existe um bom design para dividir esses requisitos estrangeiros em minha classe - vindo dos frameworks - para que eles não se misturem ou até mesmo entrem em conflito em uma classe? Imagino que este caso de "JSON < - > objeto < - > armazenamento" seja bastante comum ...

Exemplo de código

Vamos tornar meu exemplo mais concreto com algum código. Primeiro, aqui estão os requisitos da minha classe, digamos AppleSauce , impostos pelos frameworks:

  1. A classe AppleSauce deve estender com.unboundid.scim2.common.BaseScimResource .
  2. A classe AppleSauce deve ser anotada com @com.unboundid.scim2.common.annotations.Schema .
  3. Os campos de AppleSauce que devem aparecer na serialização JSON devem ser anotados com @com.unboundid.scim2.common.annotations.Attribute .
  4. A classe AppleSauce deve ser anotada com @org.springframework.ldap.odm.annotations.Entry (não é realmente um problema, mas é mencionada por completo).
  5. Os campos de AppleSauce que devem ser gravados nos atributos do LDAP devem ser anotados com @org.springframework.ldap.odm.annotations.Attribute .
  6. AppleSauce deve conter um campo do tipo javax.naming.Name anotado com @org.springframework.ldap.odm.annotations.Id .
  7. O SDK do SCIM 2 e o LDAP do Spring desejam que AppleSauce seja um JavaBean mutável com getters e setters para todos os campos relevantes para eles. Eu quero que AppleSauce seja um objeto de valor imutável.

É assim que o AppleSauce se parece com apenas um campo interessante email (minha classe real tem cerca de 45 campos):

// imports omitted

@com.unboundid.scim2.common.annotations.Schema(id = "MY_SCHEMA_URN", name = "AppleSauce", description = "delicious stuff to go with cake")
@org.springframework.ldap.odm.annotations.Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" }, base = "ou=sauces,dc=example,dc=org")
public class AppleSauce extends com.unboundid.scim2.common.BaseScimResource {
  @org.springframework.ldap.odm.annotations.Id javax.naming.Name dn;

  @com.unboundid.scim2.common.annotations.Attribute(description = "email address", multiValueClass = String.class, isRequired = true)
  @org.springframework.ldap.odm.annotations.Attribute(name = "mail", syntax = "1.3.6.1.4.1.1466.115.121.1.26{256}")
  private List<String> email;

  // getters and setter omitted
}

Uma possível solução

Meu pensamento inicial é extrair uma interface contendo todos os getters (isso também tem a vantagem de fornecer uma visão somente leitura da classe) e duas classes implementando-a, cada uma contendo os mesmos campos com anotações para os respectivos frameworks. Algo parecido com isto:

public interface AppleSauce {
  List<String> getEmail();
}

@com.unboundid.scim2.common.annotations.Schema(id = "MY_SCHEMA_URN", name = "AppleSauce", description = "delicious stuff to go with cake")
public class ScimAppleSauce extends com.unboundid.scim2.common.BaseScimResource implements AppleSauce {

  @com.unboundid.scim2.common.annotations.Attribute(description = "email address", multiValueClass = String.class, isRequired = true)
  private List<String> email;

  @Override
  List<String> getEmail() {
    return this.email;
  }
}

@org.springframework.ldap.odm.annotations.Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" }, base = "ou=sauces,dc=example,dc=org")
public class LdapAppleSauce implements AppleSauce {
  @org.springframework.ldap.odm.annotations.Id javax.naming.Name dn;

  @org.springframework.ldap.odm.annotations.Attribute(name = "mail", syntax = "1.3.6.1.4.1.1466.115.121.1.26{256}")
  private List<String> email;

  @Override
  List<String> getEmail() {
    return this.email;
  }
}

A desvantagem dessa abordagem é que ela requer código extra para converter instâncias de ScimAppleSauce para LdapAppleSauce (copiando muitos campos) e vice-versa, porque nenhuma das estruturas funcionará corretamente com o tipo AppleSauce . Eu não estou muito preocupado com o boilerplate getters / setters graças às anotações do Projeto Lombok , mas copiar manualmente 45 campos é uma perspectiva muito chata e propensa a erros .

Dificuldade de bônus

É claro que há mais nessa história ...

  1. Os campos da classe AppleSauce também carregam as anotações de javax.validation , adicionando à confusão nos campos. Colocar essas anotações em getters na interface AppleSauce seria uma boa ideia?
  2. E se eu precisar serializar AppleSauce em um formato JSON diferente, como JSON simples sem SCIM? Pode haver uma terceira implementação de AppleSauce , JsonAppleSauce , mas o número de conversores explode.
  3. Eu não gosto da mutabilidade dos JavaBeans e prefiro muito mais que todos os objetos sejam imutáveis.

Existe um design limpo para resolver este problema "framework mix"?

    
por Kolargol00 27.08.2018 / 14:49
fonte

3 respostas

1

Eu sugeriria manter seu único objeto confuso na camada da API (talvez prefixar com API, ou sufocá-lo com JSON ou JO?), e então ter uma classe de modelo de domínio separada com a qual seu sistema trabalha.

Usando essa abordagem, se você quiser ter uma classe de email para campos de email, por exemplo, isso pode ser feito. E a validação no email pode ser feita dentro da classe Email.

Eu entendo que pode parecer que você está se repetindo, mas você pode facilmente usar um mapeador de beans para ir de um objeto a outro, e acabar com um objeto de domínio validado limpo.

    
por 12.12.2018 / 15:13
fonte
0

Acho que a melhor abordagem pode ser projetar uma linguagem específica de domínio (DSL) para descrever esses recursos serializáveis. O analisador para esta DSL pode gerar alguma representação intermediária. Você pode então implementar alguns geradores de código simples que pegam a representação intermediária e geram classes Java anotadas corretamente:

DSL ---> IR --+-> SCIM2 Code Generator ---> SCIM2 annotated class
              |
              +-> LDAP Code Generator ---> LDAP annotated class

Por exemplo, você pode usar o JavaCC. O JavaCC gerará um analisador em Java, que você pode definir de forma que contenha um método parse() que produza List<Resource> . Resource aqui é um objeto que você define, que pode conter uma lista de campos, seus tipos, etc. Resource só precisa de informações suficientes para ser serializado na classe Java anotada.

Você deve ser capaz de integrar sua geração de código com coisas como o Maven facilmente, e IDEs como o Intellij podem ser configurados para reconhecer fontes geradas. Isso significa que as classes geradas serão reconhecidas uma vez que você tente usá-las no resto do seu código mais tarde.

    
por 29.08.2018 / 16:52
fonte
0

Uma possível solução seria seguir uma Arquitetura limpa abordagem.

Você teria sua classe de entidade AppleSauce livre de anotações de estrutura de qualquer tipo, ou seja, um POJO simples. Em seguida, você teria uma classe AppleSauce diferente para cada estrutura, contendo apenas as anotações necessárias para a estrutura correspondente, juntamente com as classes de mapeamento que as mapeariam para sua classe de entidade.

Se você estiver disposto a conviver com o fato de que o número de turmas pode começar a crescer rapidamente, você obtém os benefícios das aulas de Responsabilidade única e do código desacoplado e coeso.

    
por 12.12.2018 / 17:24
fonte