Classes seladas. Semântica vs desempenho

Provavelmente não eu sozinha depois de ler a documentação sobre aulas seladas, pensei: “Tudo bem. Talvez isso seja útil algum dia. Mais tarde, quando, no meu trabalho, realizei algumas tarefas nas quais consegui usar essa ferramenta com sucesso, pensei: “Nada mal. Você deve pensar frequentemente no aplicativo. ” E, finalmente, me deparei com uma descrição da classe de tarefas no livro Effective Java (Joshua Bloch, terceiro) (sim, no livro sobre Java).

Vamos examinar um aplicativo e avaliá-lo em termos de semântica e desempenho.


Acho que todo mundo que trabalhou com a interface do usuário já conheceu a implementação da interação da interface do usuário com o serviço através de alguns estados , onde um dos atributos era algum tipo de marcador . A mecânica de processar o próximo estado em tais implementações, geralmente depende diretamente do marcador especificado. Por exemplo, essa implementação da classe State :

class State( val type: Type, val data: String?, val error: Throwable? ) { enum class Type { LOADING, ERROR, EMPTY, DATA } } 

Listamos as desvantagens dessa implementação (tente você mesmo)
Comentários do capítulo 23 do livro "Preferir hierarquias de classes a classes marcadas". Proponho me familiarizar com ela.

  1. Consumo de memória para atributos que são inicializados apenas para certos tipos. O fator pode ser significativo em grandes volumes. A situação é exacerbada se objetos padrão forem criados para preencher os atributos.
  2. Carga semântica excessiva. O usuário da classe precisa monitorar para que tipo, quais atributos estão disponíveis.
  3. Suporte complicado nas classes de lógica de negócios. Suponha uma implementação em que um objeto possa executar algumas operações em seus dados. Essa classe parecerá uma colheitadeira , e a adição de um novo tipo ou operação pode se tornar difícil.


O processamento de um novo estado pode ficar assim:

 fun handleState(state: State) { when(state.type) { State.Type.LOADING -> onLoading() State.Type.ERROR -> state.error?.run(::onError) ?: throw AssertionError("Unexpected error state: $state") State.Type.EMPTY -> onEmpty() State.Type.DATA -> state.data?.run(::onData) ?: throw AssertionError("Unexpected data state: $state") } } fun onLoading() {} fun onError(error: Throwable) {} fun onEmpty() {} fun onData(data: String) {} 

Observe que, para estados como ERROR e DATA, o compilador não é capaz de determinar a segurança do uso de atributos; portanto, o usuário precisa escrever um código redundante. Alterações na semântica só podem ser detectadas em tempo de execução.

Classe selada


imagem

Com a refatoração simples, podemos dividir nosso Estado em um grupo de classes:

 sealed class State //    stateless  -     singleton object Loading : State() data class Error(val error: Throwable) : State() //  ,     ,  stateless  -  singleton object Empty : State() data class Data(val data: String) : State() 

No lado do usuário, obtemos o processamento do estado, onde a acessibilidade dos atributos será determinada no nível do idioma e o uso indevido causará erros no estágio de compilação:

 fun handleState(state: State) { when(state) { Loading -> onLoading() is Error -> onError(state.error) Empty -> onEmpty() is Data -> onData(state.data) } } 

Como apenas atributos significativos estão presentes nas cópias, podemos falar sobre como economizar memória e, o que é mais importante, melhorar a semântica. Os usuários de classes seladas não precisam implementar manualmente as regras para trabalhar com atributos, dependendo do marcador de tipo ; a disponibilidade de atributos é garantida pela separação em tipos.

Tudo isso é grátis?


Spoiler
Não, não de graça.

Os desenvolvedores de Java que experimentaram o Kotlin devem ter examinado o código descompilado para ver como são os termos do Kotlin Java. Uma expressão com when será algo como isto:

 public static final void handleState(@NotNull State state) { Intrinsics.checkParameterIsNotNull(state, "state"); if (Intrinsics.areEqual(state, Loading.INSTANCE)) { onLoading(); } else if (state instanceof Error) { onError(((Error)state).getError()); } else if (Intrinsics.areEqual(state, Empty.INSTANCE)) { onEmpty(); } else if (state instanceof Data) { onData(((Data)state).getData()); } } 

Ramos com uma abundância de exemplos de instâncias podem ser alarmantes devido a estereótipos sobre o “sinal de código incorreto” e o “impacto no desempenho”, mas não temos ideia. É necessário comparar de alguma forma a velocidade de execução, por exemplo, usando jmh .

Com base no artigo “Medindo a velocidade do código Java corretamente” , foi preparado um teste de processamento de quatro estados (CARREGANDO, ERRO, VAZIO, DADOS), e aqui estão seus resultados:

 Benchmark Mode Cnt Score Error Units CompareSealedVsTagged.sealed thrpt 500 940739,966 ± 5350,341 ops/s CompareSealedVsTagged.tagged thrpt 500 1281274,381 ± 10675,956 ops/s 

Pode-se observar que a implementação selada trabalha cerca de 25% mais lenta (havia uma suposição de que o atraso não excederia 10-15%).

Se em quatro tipos tivermos um quarto de atraso, com tipos crescentes (o número de instâncias de verificações), o atraso deverá aumentar apenas. Para verificar, aumentaremos o número de tipos para 16 (suponha que tenhamos conseguido obter uma hierarquia tão ampla):

 Benchmark Mode Cnt Score Error Units CompareSealedVsTagged.sealed thrpt 500 149493,062 ± 622,313 ops/s CompareSealedVsTagged.tagged thrpt 500 235024,737 ± 3372,754 ops/s 

Juntamente com uma diminuição na produtividade, o atraso nas vendas seladas aumentou para ~ 35% - um milagre não aconteceu.

Conclusão


Neste artigo, não descobrimos a América, e implementações seladas em filiais, por exemplo, realmente funcionam mais lentamente do que comparações de links.


No entanto, alguns pensamentos precisam ser expressados:

  • na maioria dos casos, queremos trabalhar com boa semântica de código, acelerar o desenvolvimento devido a dicas adicionais das verificações do IDE e do compilador - nesses casos, você pode usar classes seladas
  • se você não pode sacrificar o desempenho em uma tarefa, deve negligenciar a implementação selada e substituí-la, por exemplo, por uma implementação de tagget. Talvez você deva abandonar completamente o kotlin em favor de idiomas de nível inferior

Source: https://habr.com/ru/post/pt430014/


All Articles