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.
- 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.
- Carga semântica excessiva. O usuário da classe precisa monitorar para que tipo, quais atributos estão disponíveis.
- 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
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()
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?
SpoilerNã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