Objeto: criação de um pequeno app com ordenação de tabela por colunas usando polimorfismo e evitando código repetitivo
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
Uma vez mais, vamos partir exatamente de onde paramos na receita anterior, então abra logo o código resultante dela.
Relembrando: a gente tinha uma replicação de algoritmo ultra-sebosa lá no Ordenador. Conseguimos reduzir tudo para um só algoritmo usando uma ideiazinha brilhante da engenharia de software: aparte o que muda do que é estável.
Nosso algoritmo de ordenação ficou assim:
class Ordenador{
List ordenarFuderoso(List objetos, Decididor 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;
}
}
- E o que isso tem a ver com apartar o que muda, Professor?
Tem tudo a ver. Tudo que não está em negrito era estável em todos os algoritmos que a gente tinha. Apenas a parte do teste do if mudava, a cada método, conforme o tipo dos objetos envolvidos (cerveja, nação, café), o critério de ordenação (nome, ibu etc.) e a ordem (crescente/decrescente).
- Tá, mas a gente separou essas coisas?
Separou sim. Antes, o algoritmo de comparação que está na guarda desse if constava, em diversas formas, lá dentro das dezenas de métodos de ordenação. Agora a gente meio que terceirizou a execução desse algoritmo para uma entidade externa que a gente recebe como parâmetro de quem quer que invoque o método de ordenação. Basicamente, a gente tá dizendo: eu ordeno lista de qualquer coisa, me diga apenas como comparar dois objetos dessa lista.
Isso que eu chamei de terceirização se deu através da definição de uma classe abstrata, que funciona como um contrato.
abstract class Decididor{
bool precisaTrocarAtualPeloProximo(dynamic atual, dynamic proximo);
}
O termo terceirização é meu, mas contrato você vai ler numa ruma de texto sobre OO. O termo serve, normalmente, para designar um ente (classe) sem nenhum método implementado, como é o nosso caso. Basicamente, nosso método de ordenação foi escrito recebendo um objeto de uma classe abstrata como parâmetro e chamando um método sem implementação. E esse objeto é um objeto daquela classe Decididor e esse método chamado não tem implementação conhecida pelo Ordenador. Aquela classe Decididor, assim, funciona como um contrato. Ela estabelece o que deve ser feito - ter um método de nome precisaTrocarAtualPeloProximo, receber dois parâmetros dynamic e retornar um valor lógico. Ela não estabelece, no entanto, o como deve ser feito, visto que o método está sem implementação.
Quem quiser usar esse método de ordenação de nosso Ordenador, então, além de passar a lista a ser ordenada, precisa passar como parâmetro, também, o que eu chamo de "terceirizado", um cara que vai cumprir aquele contrato exatamente como estabelecido. Em programês: um objeto de uma classe concreta que herda da classe Decididor e substitui a implementação vazia do método precisaTrocarAtualPeloProximo por uma implementação específica.
Como a gente tinha bem 20 possibilidades de comparação, nosso primeiro instinto foi o de implementar um terceirizado para cada uma delas. Assim, a classe...
class DecididorCervejaNomeCrescente extends Decididor{
@override
bool precisaTrocarAtualPeloProximo(atual, proximo) {
try{
return atual["name"].compareTo(proximo["name"]) > 0;
}catch (error){
return false;
}
}
}
... permite a criação de terceirizados que sabem comparar objetos JSON pela propriedade name (caso das cervejas, por exemplo) em ordem crescente - retornam true se a propriedade name do objeto atual for maior (lexicograficamente) do que a propriedade name do objeto proximo. De forma parecida, a classe...
class DecididorCervejaEstiloCrescente extends Decididor{
@override
bool precisaTrocarAtualPeloProximo(atual, proximo) {
try{
return atual["style"].compareTo(proximo["style"]) > 0;
}catch (error){
return false;
}
}
}
...permite a criação de terceirizados que sabem comparar objetos JSON pela propriedade style (caso também das cervejas) em ordem crescente - retornam true se a propriedade style do objeto atual for maior (lexicograficamente) do que a propriedade style do objeto proximo.
A gente vai ficar com 20 classes dessas para abrangermos todas as comparações possíveis.
Não temos replicação de algoritmo - cada classe de terceirizado implementa um algoritmo diferente. Mas temos muita burocracia - todo o código que não está em destaque vai ser replicado nas 20 implementações, porque a gente precisa dessa burocracia para estabelecer o vínculo das classes concretas (DecididorCervejaNomeCrescente etc.) com o contrato (Decididor).
Temos, também, um efeito colateral dessa reforma trabalhista toda, lá no DataService, na hora do vamo ver...
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() );
}
//mais 18 ifs de presente pra você
emitirEstadoOrdenado(objetosOrdenados, propriedade);
}
- Isso pode ficar assim não, Professor...
Não pode e não vai.
Isso é código de quem programa tomando café com chantilly, botando uma coisa ruim sobre uma coisa maravilhosa que é o café.
A gente pegou o polimorfismo com herança e substituição, que é uma coisa maravilhosa, mas tá com uma ruma de burocracia em cima dele, se brincar dá até diabetes.
E a gente quer café sem açúcar nem nada!
Sua cabeça de atleta de playstation tem que ter apenas uma coisa em mente durante essa receita inteira: o que quer que a gente venha a fazer, precisamos respeitar o contrato. Ou seja, nada de mexer no Decididor. Tampouco no Ordenador.
Assim, precisamos olhar para aquelas 20 classes "terceirizadas" e pensar se não poderíamos ter um tipo só de funcionário que implementasse o contrato mas fosse capaz de executar todos os serviços, porque todos esses serviços são beeem parecidos.
Comece deixando apenas uma classe que herda de Decididor, mudando o nome dela.
class FuncionarioDoMes extends Decididor{
@override
bool precisaTrocarAtualPeloProximo(atual, proximo) {
try{
return atual["name"].compareTo(proximo["name"]) > 0;
}catch (error){
return false;
}
}
}
Não esqueça de apagar todas as demais classes que herdam de Decididor.
Nosso "funcionário do mês" há de ser capaz de fazer todo o serviço sozinho.
Claro que apenas mudar o nome da classe concreta não adianta porra nenhuma, né?
A gente continua ordenando pela propriedade name, por exemplo, e isso é bem limitante.
- Já sei, e se a gente passar a propriedade como parâmetro, assim...
bool precisaTrocarAtualPeloProximo(atual, proximo, String propriedade) {
try{
return atual[propriedade].compareTo(proximo[propriedade]) > 0;
}catch (error){
return false;
}
}
Calma, jovem padawan. Lembre que a gente precisa respeitar o contrato. E o contrato restringe os parâmetros a atual e proximo.
No entanto, sua ideia é boa. A gente precisa ter essa informação em algum lugar para poder comparar por qualquer propriedade dos objetos JSON. O lugar, a lista de parâmetros, é que não é o ideal.
Mas a gente tem os atributos da classe, não é mesmo. Podemos por o cão chupando manga entre os atributos da classe sem desrespeitar em nada o contrato!
Modifique seu FuncionarioDoMes.
class FuncionarioDoMes extends Decididor{
final String propriedade;
FuncionarioDoMes(this.propriedade);
@override
bool precisaTrocarAtualPeloProximo(atual, proximo) {
try{
return atual[propriedade].compareTo(proximo[propriedade]) > 0;
}catch (error){
return false;
}
}
}
Note que a propriedade de ordenação vai continuar sendo definida por que quiser fazer a ordenação (o DataService, no caso), apenas será uma informação passada na construção do objeto FuncionarioDoMes.
Só com isso a gente diminui de 20 para 2 a quantidade de classes necessárias para todas as comparações - porque a gente ainda precisa comparar de forma crescente e decrescente.
Mas a gente toma café sem açúcar, lembre.
Se a gente botou uma informação representando a propriedade de ordenação como atributo da classe FuncionarioDoMes, nada nos impede de empurrar outra prorpiedade indicando se a ordem é crescente ou decrescente.
Vamos lá.
Modifique seu funcionário do mês.
class FuncionarioDoMes extends Decididor{
final String propriedade;
final bool crescente;
FuncionarioDoMes(this.propriedade,[this.crescente = true]);
@override
bool precisaTrocarAtualPeloProximo(atual, proximo) {
try{
final ordemCorreta = crescente ? [atual, proximo]: [proximo, atual] ;
return ordemCorreta[0][propriedade].compareTo(ordemCorreta[1][propriedade]) > 0;
}catch (error){
return false;
}
}
}
- E aqueles colchetes na declaração de parâmetros... o (this.propriedade,[this.crescente = true]) ???
Parâmetro posicional opcional e com valor default. Quem for construir seu FuncionarioDoMes pode passar true, false ou nada (será atribuído true ao atributo crescente) como segundo parâmetro, e sem dar-lhe nome, não é parâmetro nomeado como a gente na maioria das vezes está usando. Note que o primeiro parâmetro também é posicional, mas é obrigatório, não está entre colchetes.
Agora precisamos nos livrar daqueles 20 ifs. Não precisaremos de nenhum deles!!
Vale a pena lembrar como estava o método ordenarEstadoAtual lá no DataService, para efeitos didáticos:
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() );
}
//mais 18 ifs de presente pra você
emitirEstadoOrdenado(objetosOrdenados, propriedade);
}
Entenda que no lugar daquele comentário em vermelho há mais uma penca de ifs.
Agora modifique o método:
void ordenarEstadoAtual(final String propriedade){
List objetos = tableStateNotifier.value['dataObjects'] ?? [];
if (objetos == []) return;
Ordenador ord = Ordenador();
Decididor d = FuncionarioDoMes(propriedade);
var objetosOrdenados = ord.ordenarFuderoso(objetos, d);
emitirEstadoOrdenado(objetosOrdenados, propriedade);
}
Salve, execute e veja a mágica acontecendo.
O app deve responder corretamente a toques em todas as colunas, ordenando-as de forma crescente.
- Massa, Professor, mas e pra fazer a ordem decrescente?
Você faz isso nos exercícios.
O algoritmo para ordenar de forma decrescente já está implementado, será uma questão do que deve ser alterado para que a fábrica do FuncionarioDoMes seja chamada com a informação da ordem e não apenas da propriedade de ordenacao.
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.
Algumas escolhas de nome que eu faço são meramente didáticas. Assim, uma classe cujo objetivo é comparar dois objetos não deve ter o nome "FuncionarioDoMes". Usar ComparadorJSON ou DecididorJSON como nome (a classe só funciona para comparação de objetos JSON) seria uma prática mais adequada de programação. Faça essa modificação.
Atualmente, uma das maneiras mais populares de se promover polimorfismo é o uso das callback functions. Esse nosso contrato Decididor, por exemplo, tem apenas uma função nele, poderia ser trocado facilmente por uma callback function. Escreva um novo método de ordenação na classe Ordenador (não modifique o que já funciona, seja prudente), usando uma callback function em vez da classe abstrata Decididor. Você precisa modificar o arquivo data_service.dart para que seja usado o método de ordenação que recebe uma callback function.
Faça com que a tabela exiba entidades diferentes de Cafés, Cervejas e Nações. Você deve procurar outra(s) API(s) para buscar essas novas entidades. Se não conseguir, pode usar outras entidades acessíveis através da random-api.
Nesse novo app, implemente todas as ordenações, crescente e decrescente, com o devido acerto da interface gráfica. Seu usuário deve poder ver por qual coluna a tabela está ordenada e se a ordem é crescente ou decrescente. É mais fácil do que parece, o componente DataTableWidget sabe trabalhar com isso, exibindo setas pra cima e pra baixo nas colunas.
(Desafio Emocionante) Acrescente um campo de pesquisa (na barra de ações, na parte de cima da IU). A partir da terceira letra digitada, o app deve fazer uma operação de filtragem nos objetos exibidos. Detalhe emocionante: você não pode usar nenhuma função de biblioteca para fazer a filtragem. Em vez disso, deve implementar o seu próprio esquema genérico de filtragem, com polimorfismo, tal qual fizemos com o Ordenador.