Dúvida sobre enums em java

Estou estudando sobre a aplicação de enums e constantes no Java como meio de eliminar os famosos “valores mágicos” do código e atribuir maior sentido à lógica empregada. Numa classe “User” por exemplo, com um atributo “acessLevel” do tipo String, compreendo que utilizar um tipo específico com valores pré-determinados (como acontece nas enums) é vantajoso pois limita a atribuição de valores ao atributo, ao invés de qualquer String.

Minha dúvida é a respeito da quantidade de arquivos que irão surgir ao criar uma enum toda vez que achar necessário limitar os valores do atributo. Esta é realmente uma boa prática de programação? Há formas melhores de evitar os “valores mágicos”?

Como exemplo deixo a classe “User” que comentei, juntamente com a enum AcessLevel. Reparem que dois dos atributos do usuário são enums específicas, ao invés de tipos mais abrangentes como String.

package home.room.models;

// @author;
 
public class User {
    private int id;
    private String name;
    private String password;
    private AcessLevel accessLevel;
    private ActivityStatus activityStatus;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public AcessLevel getAccessLevel() {
        return accessLevel;
    }
    
    public void setAccessLevel(String accessName) {
        if(accessName.equals(AcessLevel.ADMIN_ACESS.getName())) {
            this.accessLevel = AcessLevel.ADMIN_ACESS;
        }else {
            this.accessLevel = AcessLevel.COMMON_ACCESS;
        }
    }

    public ActivityStatus getActivityStatus() {
        return activityStatus;
    }

    public void setActivityStatus(String activityStatus) {
        if(activityStatus.equals(ActivityStatus.ACTIVE_USER.toString())) {
            this.activityStatus = ActivityStatus.ACTIVE_USER;
        }else {
            this.activityStatus = ActivityStatus.INACTIVE_USER;
        }
    }
}

Enum AcessLevel:

package home.room.models;

// @author Rodrigo Alberto;

public enum AcessLevel {
    ADMIN_ACESS("Administrativo"),
    COMMON_ACCESS("Comum");
    
    private final String name;

    private AcessLevel(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

Como várias coisas na programação, não tem uma resposta única pra esse tipo de caso, depende de cada situação que você está resolvendo. O principal ponto é tentar deixar seu código o mais flexível possível, sem exagerar na complexidade.

No seu exemplo, o que deve estar te incomodando é não ter como prever quantos tipos cada enum pode ter. Isso ocorre com frequência em várias situações de programação, não é um problema exclusivo de enums, ocorrendo com quaisquer tipos de dados. Para uma implementação inicial, não vejo problema em ter 2 opções em cada enum, e adicionar mais alguns conforme a necessidade.

O que pode requerer uma refatoração no futuro - ou mesmo a remoção dos enums - é caso a quantidade opções aumentar consideravelmente. Nesse caso, pode ser melhor ter algum tipo de controle à parte, em que valores pode ser adicionados ou removidos como em uma lista. Por exemplo, em vez de cada usuário ter 1 acesslevel, ele teria uma lista com todos os acesslevel, que pode ser conferida através de um método específico, ou até mesmo uma classe à parte para gerenciar as permissões de cada usuário.

Algo que não entendi no seu código é o método setAcessLevel recebe uma string. Ele pode receber um enum diretamente, da mesma forma que outros atributos, e a comparação é desnecessária:

public void setAccessLevel(AcessLevel level) {
  this.accessLevel = level;
}

Uma das vantagens de enums é justamente essa, não a descarte dessa forma.

Abraço.

@TerraSkilll, O atributo acessLevel já é proveniente de uma classe à parte que gerencia as permissões dos usuários: a classe AcessLevel. Minha preocupação é em criar muitas classes enum à parte e, consequentemente, ter muitos arquivos distintos no pacote, mesmo que com poucas linhas de código.

Sobre o método setAcessLevel() que comentou, estou utilizando um banco de dados sql nesta aplicação. Nele a tabela usuário tem a coluna acessLevel do tipo varchar, por isso ao “setar” um nível de acesso proveniente do banco, modifiquei o método para salvar o valor no objeto com o tipo específico, diferente do tipo da coluna no banco.

Mas dessa forma o seu enum não tem utilidade nenhuma, já que é permitido fazer:

myUser.setAccessLevel("Abacaxi");

A tipagem forte do enum foi para o ralo, já que você permite passar String como parâmetro.

Na hora de persistir é que você trata o valor a ser enviado para o driver do banco e não no método setAccessLevel.

@staroski É que no caso o setAccessLevel(String tal) só é utilizado no método de consulta do banco, que retorna uma String, o que me faz acreditar que tenho um certo controle do que será “setado”. O método getAccessLevel continua com a tipagem do enum, que é o que uso para as comparações.

Não sei bem como tratar desta forma que comentou. Sei que o banco só pode me retornar o valor do tipo da coluna (varchar) e que, se quiser usar a enum, vou ter que converter esse valor para seu tipo em algum momento.

Isso eu já tinha entendido. Quando falei em gerenciar permissões, seria ter uma lista ou uma classe a mais para conter todos os níveis que o usuário pode ter, em vez de um único atributo, com um único nível de acesso. Resumidamente, algo como:

public class User {
  private List<AcessLevel> accessLevels = new ArrayList();

  public void addAcessLevel(AcessLevel level) {
    accessLevels.add(level);
  }

  public void checkAcessLevel(AcessLevel level){
    // aqui você verifica se o level necessário está presente na lista 
  }

Fora o trabalho de manter uma estrutura complexa, não vejo nenhum problema nisso, a não ser que você já esteja planejando adicionar uma dúzia ou mais de tipos diferentes de AcessLevel, cada um com regras bem específicas, que você vai ter de ficar verificando com ifs. Nesse caso sim, uma mudança de abordagem pode ser bem vinda.

Sim, no momento que você carrega e grava os dados do banco (via sql ou orm), e cria a instância do enum para passar pro cliente. Supondo que você tem um DAO ou similar, seria algo como:

public class UserDAO{
  public User getUser(int id) {
     // essa parte não é importante pro exemplo, é só pra ilustrar
    String sql = 'select * from user where id = :id';
    Statement st = Conexao.executar(sql, id);
    ResultSet rs = st.get(0);
    
    // aqui você está pegando os dados pra um User, e faz o if para criar o enum correto
    User user = new User();
    // outros atributos
    String valorDoEnumNoBanco = rs.get('colunaEnumDoBanco');

    if ('Admistrativo'.equalsIgnoreCase(valorDoEnumNoBanco)) {
      user.setAccessLevel(AcessLevel.ADMIN_ACESS);
    } else ('Comum'.equalsIgnoreCase(valorDoEnumNoBanco)) {
      user.setAccessLevel(AcessLevel.COMMON_ACCESS);
    } else {
      // algo deu errado, o valor não está no banco. Usar o padrão ou dar exceção?
    }

    return user;
  }
  
  // grava um User
  public void gravarUser(User user) {
    String sql = 'update user set colunaEnumDoBanco = :valorEnum  where id = :id';
    Statement st = Conexao.prepare(sql);
    st.params('valorEnum', user.getAcessLevel().getName());
    st.params('id', user.getId());
    st.execute();
  }
}

PS: o código é de exemplo e estou meio enferrujado de Java, mas acredito que dê pra entender a ideia.

Abraço.