Objeto: criação de um pequeno app com ordenação de tabela por colunas usando polimorfismo para contornar replicação de código.
Principais termos técnicos abordados: polimorfismo, bibliotecas, imports, constantes, information hiding, get/set, private/public, números mágicos
Requisitos para as instruções funcionarem: navegador ou ide (VS Code, Android Studio etc.)
Requisitos para compreensão das instruções: noções básicas de programação e ter realizado receita anterior, com exercícios
Como ler essa receita: instruções, dentro dos passos da receita e que requerem ação sua no computador, estarão escritas em cor azul. Comentários sobre os passos estarão em fonte normal, cor preta. Comandos, código-fonte, termos técnicos ou configuração explícita, estarão com fonte diferenciada.
Observação: eventuais instruções de terminal serão comandos Linux. Adapte caso esteja usando outro sistema operacional
"Poeta é aquele que tira de onde não tem para botar onde não cabe" (Pinto do Monteiro)
Vamos partir exatamente de onde paramos na receita anterior, então abra logo o código resultante dela.
Temos uma replicação horrenda no arquivo ordenador.dart... Vale lembrar...
class Ordenador{
List ordenarCervejasPorNomeCrescente(List cervejas){
List cervejasOrdenadas = List.of(cervejas);
bool trocouAoMenosUm;
do{
trocouAoMenosUm = false;
for (int i=0; i<cervejasOrdenadas.length-1; i++){
var atual = cervejasOrdenadas[i];
var proximo = cervejasOrdenadas[i+1];
if (atual["name"].compareTo(proximo["name"]) > 0){
var aux = cervejasOrdenadas[i];
cervejasOrdenadas[i] = cervejasOrdenadas[i+1];
cervejasOrdenadas[i+1] = aux;
trocouAoMenosUm = true;
}
}
}while(trocouAoMenosUm);
return cervejasOrdenadas;
}
List ordenarCervejasPorNomeDecrescente(List cervejas){
List cervejasOrdenadas = List.of(cervejas);
bool trocouAoMenosUm;
do{
trocouAoMenosUm = false;
for (int i=0; i<cervejasOrdenadas.length-1; i++){
var atual = cervejasOrdenadas[i];
var proximo = cervejasOrdenadas[i+1];
if (atual["name"].compareTo(proximo["name"]) > 0){
var aux = cervejasOrdenadas[i];
cervejasOrdenadas[i] = cervejasOrdenadas[i+1];
cervejasOrdenadas[i+1] = aux;
trocouAoMenosUm = true;
}
}
}while(trocouAoMenosUm);
return cervejasOrdenadas;
}
List ordenarCervejasPorEstiloCrescente(List cervejas){
List cervejasOrdenadas = List.of(cervejas);
bool trocouAoMenosUm;
do{
trocouAoMenosUm = false;
for (int i=0; i<cervejasOrdenadas.length-1; i++){
var atual = cervejasOrdenadas[i];
var proximo = cervejasOrdenadas[i+1];
if (atual["style"].compareTo(proximo["style"]) > 0){
var aux = cervejasOrdenadas[i];
cervejasOrdenadas[i] = cervejasOrdenadas[i+1];
cervejasOrdenadas[i+1] = aux;
trocouAoMenosUm = true;
}
}
}while(trocouAoMenosUm);
return cervejasOrdenadas;
}
List ordenarCervejasPorEstiloDecrescente(List cervejas){
List cervejasOrdenadas = List.of(cervejas);
bool trocouAoMenosUm;
do{
trocouAoMenosUm = false;
for (int i=0; i<cervejasOrdenadas.length-1; i++){
var atual = cervejasOrdenadas[i];
var proximo = cervejasOrdenadas[i+1];
if (atual["style"].compareTo(proximo["style"]) > 0){
var aux = cervejasOrdenadas[i];
cervejasOrdenadas[i] = cervejasOrdenadas[i+1];
cervejasOrdenadas[i+1] = aux;
trocouAoMenosUm = true;
}
}
}while(trocouAoMenosUm);
return cervejasOrdenadas;
}
}
Basicamente, temos 4 métodos implementando o mesmo algoritmo - as implementações só estão descaradamente parecidas porque implementam, efetivamente, o mesmo algoritmo ordenação. A gente deve ter um método apenas, que seja capaz de ordenar uma lista de qualquer coisa, de inteiro, de String, de JSON, de objetos de qualquer classe...
E a gente vai resolver isso como os poetas que improvisam versos, tirando de onde não tem pra botar onde não cabe.
Assim, a primeira coisa que precisamos saber é em que lugar a gente deve "tirar de onde não tem", e esse lugar eu entreguei pelos trechos em negrito do código anterior: a única coisa que a gente não tem como unificar entre esses métodos são as expressões booleanas naqueles ifs.
E, se a gente pensar bem, aqueles ifs estão perguntando sempre a mesma coisa: eu devo trocar o atual pelo próximo?
Não precisa apagar esses 4 métodos ainda. Vamos, primeiro, escrever um que possa subistituir a todos eles.
Acrescente à classe Ordenador seguinte método:
List ordenarFuderoso(List objetos){
List objetosOrdenados = List.of(objetos);
bool trocouAoMenosUm;
do{
trocouAoMenosUm = false;
for (int i=0; i<objetosOrdenados.length-1; i++){
var atual = objetosOrdenados[i];
var proximo = objetosOrdenados[i+1];
if ( precisa trocar atual pelo proximo? ){
var aux = objetosOrdenados[i];
objetosOrdenados[i] = objetosOrdenados[i+1];
objetosOrdenados[i+1] = aux;
trocouAoMenosUm = true;
}
}
}while(trocouAoMenosUm);
return objetosOrdenados;
}
Basicamente eu troquei nomes de variáveis para que fiquem mais condizentes com um método de nome "ordenarFuderoso" que é capaz de ordenar até o cão chupando manga.
No entanto, a gente tem um pequeno problema com o método ordenarFuderoso: ele tem um trecho escrito em português. E português, como você deve saber, ainda não é linguagem de programação.
A gente tem, na verdade, um problema maior: aparentemente não existe nenhuma expressão em dart ou qualquer outra linguagem que a gente possa escrever ali, simplesmente porque nosso ordenador não tem como advinhar nem tampouco inferir o que o caba que chamar esse método deseja como critério de ordenação (nome, estilo, ibu...) e ordem de ordenação (crescente ou decrescente).
Mas, se a gente não tem como advinhar o que o caba que chama o método quer, a gente tem um jeito muito claro e óbvio de exigir que ele nos dê isso.
- Tô acompanhando não, Professor...
Pense um pouco. Considere que você é um objeto Ordenador que se propõe a ordenar qualquer coisa. Você tem como advinhar qual é a lista de objetos que algum programa bandido vai pedir pra você ordenar?
- Não.
Veja o código e diga o que você acha que foi feito quanto a isso?
- ... a gente pede como parâmetro para o método de ordenar. Quem chamá-lo, que passe a lista do que quiser.
E se você, como objeto Ordenador, também não tem como advinhar se deve ou não trocar um objeto atual por um próximo no meio do processo de ordenação, o que você faz?
- Já sei! Pede como parâmetro também.
Exatamente. A gente pede como parâmetro.
Nosso método de ordenar precisa de uma lista de objetos e também de um objeto "decididor", um objeto que vai decidir, no processo de ordenação, se devemos ou não trocar o objeto atual, lá dentro do laço for, pelo objeto próximo.
Reescreva o método ordenarFuderoso.
List ordenarFuderoso(List objetos, dynamic decididor){
List objetosOrdenados = List.of(objetos);
bool trocouAoMenosUm;
do{
trocouAoMenosUm = false;
for (int i=0; i<objetosOrdenados.length-1; i++){
var atual = objetosOrdenados[i];
var proximo = objetosOrdenados[i+1];
if ( decididor.precisaTrocarAtualPeloProximo(atual,proximo) ){
var aux = objetosOrdenados[i];
objetosOrdenados[i] = objetosOrdenados[i+1];
objetosOrdenados[i+1] = aux;
trocouAoMenosUm = true;
}
}
}while(trocouAoMenosUm);
return objetosOrdenados;
}
Note que, fora a declaração do parâmetro, estamos praticamente com uma versão dartizada do que estava escrito em português. Tínhamos...
precisa trocar atual pelo proximo?
E agora temos...
decididor.precisaTrocarAtualPeloProximo(atual, proximo)
O que a gente precisa agora é de uma classe para ser o tipo desse objeto decididor. Já sabemos o método que essa classe tem que ter e os parâmetros que tem que receber e até o que esse método tem que retornar.
Acrescente a seguinte classe ao arquivo ordenador.dart, fora do código da classe Ordenador.
abstract class Decididor{
bool precisaTrocarAtualPeloProximo(dynamic atual, dynamic proximo);
}
- Classe abstrata, método sem implementação... que djabo é isso?
Classe abstrata é justamente uma classe que pode ter métodos sem implementação. Como a gente não tem como prever que algoritmo de comparação implementar, a gente, bem, deixa sem implementação.
Agora, modifique o parâmetro do método ordenarFuderoso.
List ordenarFuderoso(List objetos, Decididor decididor){
//(...)
}
- Pronto?
Pronto. Agora o método ordenarFuderoso está tirando de onde não tem, está chamando, lá no if, um método decididor.precisaTrocarAtualPeloProximo que nem implementação tem.
- Nam, Professor, tem como não. Sem fazer a comparação em canto nenhum não vejo como o algoritmo de ordenarFuderoso vai funcionar.
Certeza. Sem comparação, sem ordenação. Mas a missão do Ordenador acaba aqui.
Quem quiser "ordenar fuderoso", que passe a lista de objetos e um Decididor com o método implementado.
- Mas e como a gente vai fazer isso se a classe Decididor é abstrata é o método precisaTrocarAtualPeloProximo não está implementado???
A gente vai fazer o que a gente vem fazendo a vida inteira. Herança e sobrescrição. Lembra que a gente herda sempre de StatelessWidget e sobrescreve o método build? Pois bem, aqui a gente vai herdar do Decididor e implementar o método precisaTrocarAtualPeloProximo, conforme as necessidades. Em outras palavras, a gente vai sobrescrever um método sem implementação, vai botar onde não cabe.
Sendo que vai caber, claro.
Quem tem conhecimento para decidir como comparar os elementos das várias listas que devemos ordenar não é a nossa biblioteca util (onde está o arquivo ordenador.dart), que é uma biblioteca genérica que deve funcionar em qualquer projeto.
A biblioteca data, onde está o data_service.dart, é o lugar mais adequado para esse tipo de código.
Acrescente as seguintes classes no arquivo data_service.dart.
class DecididorCervejaNomeCrescente extends Decididor{
@override
bool precisaTrocarAtualPeloProximo(atual, proximo) {
try{
return atual["name"].compareTo(proximo["name"]) > 0;
}catch (error){
return false;
}
}
}
class DecididorCervejaEstiloCrescente extends Decididor{
@override
bool precisaTrocarAtualPeloProximo(atual, proximo) {
try{
return atual["style"].compareTo(proximo["style"]) > 0;
}catch (error){
return false;
}
}
}
class DecididorCervejaNomeDecrescente extends Decididor{
@override
bool precisaTrocarAtualPeloProximo(atual, proximo) {
try{
return atual["name"].compareTo(proximo["name"]) < 0;
}catch (error){
return false;
}
}
}
class DecididorCervejaEstiloDecrescente extends Decididor{
@override
bool precisaTrocarAtualPeloProximo(atual, proximo) {
try{
return atual["style"].compareTo(proximo["style"]) < 0;
}catch (error){
return false;
}
}
}
Em seguida, modifique sutilmente o método ordenarEstadoAtual lá na classe DataService.
void ordenarEstadoAtual(final String propriedade){
List objetos = tableStateNotifier.value['dataObjects'] ?? [];
if (objetos == []) return;
Ordenador ord = Ordenador();
var objetosOrdenados = [];
final type = tableStateNotifier.value['itemType'];
if (type == ItemType.beer && propriedade == "name"){
objetosOrdenados = ord.ordenarFuderoso(objetos, DecididorCervejaNomeCrescente() );
}else if (type == ItemType.beer && propriedade == "style"){
objetosOrdenados = ord.ordenarFuderoso(objetos, DecididorCervejaEstiloCrescente() );
}
emitirEstadoOrdenado(objetosOrdenados, propriedade);
}
Pronto, estamos ordenando sempre chamando o mesmo método. E podemos ordenar qualquer coisa por qualquer critério em qualquer ordem com esse mesmo método.
- Sim, e o polimorfismo?
Bem, isso é polimorfismo. Mais especificamente, lá no seu método ordenarFuderoso, a chamada decididor.precisaTrocarAtualPeloProximo(atual,proximo) será polimórfica. Porque ela poderá desviar para a execução de códigos diferentes. O desvio depende do contexto, que aqui quer dizer que depende do tipo específico do parâmetro decididor quando o método ordenarFuderoso for invocado. Por causa desse potencial desvio, a função pode se comportar de várias (poli) formas (morphus) distintas.
Note que, agora, a gente não precisa implementar vários e vários métodos de ordenação quase iguais. A gente precisa implementar apenas o que a gente não tem como generalizar (totalmente), que é o critério de ordenação e a ordem de ordenação, que estão ambos abstraídos no método (abstrato, sem implementação) da classe Decididor.
Vídeo-aula (minha) sobre polimosfismo, com ênfase em herança de tipos abstratos. Exemplos em Java, mas os princípios são os mesmos.
Vídeo-aula (minha) sobre polimorfismo, com ênfase em herança de classes concretas. Exemplos em Java, mas os princípios são os mesmos.
Faça todas as ordenações, sempre chamando o mesmo método. Tente evitar novas replicações de código, ainda dá pra melhorar. Nas minhas contas, basta uma classe herdando o Decididor para abarcar todas essas ordenações de objetos JSON de uma lapada só.