9. REPRESENTAÇÃO & FORMATO

“Sem requisitos e projeto, programação acaba sendo a arte de adicionar bugs a um arquivo de texto em branco.”

Louis Srygley

O modelo de objetos, isto é, o próprio código escrito na forma de uma classe com atributos, já é uma representação, embora bastante abstrata, das informações. No entanto, uma mesma informação pode ser representada de várias formas, seja de forma resumida ou completa, exata ou aproximada. Por exemplo, de onde estou até a porta há uma distância de 150cm, que também pode ser expressa como 1,5m, 59" (59 polegadas), aproximadamente 5' (5 pés) ou até 3 cúbitos. Este capítulo trata das representações alternativas para os objetos e de como converter o objeto para elas e a partir delas.

9.1 Conceito de Representação do Objeto

Os próprios objetos começam com uma ideia, que é uma representação mental: distância, carrinho de compras, postagem, like, enfim, seja uma entidade física ou intangível.

A representação dos objetos em código varia segundo as linguagens, porém, nas linguagens class-based (baseadas em classes) geralmente significa descrevê-los através de classes e atributos, e deste “molde” construir instâncias. Este processo, como já foi discutido, envolve deixar de fora uma série de detalhes para tornar sua implementação viável, passo este conhecido como abstração.

Retomando o exemplo da distância, apresentado no início do capítulo, uma classificação de objetos distância poderia ser realizada como no pseudocódigo a seguir:

// Distância.pseucocódigo
classe Distância

  atributo privado centímetros : inteiro

  construtor (parâmetro cm = 0)
    atributo centímetros = cm
  fim construtor

  método centímetros : textual
    retorna atributo centímetros + "cm"
  fim método

  método polegadas : textual
    retorna (atributo centímetros * 0.3937) + " pol."
  fim método

fim classe

// App.pseudocódigo
usando Distância de Distância.pseudocódigo
procedimento App
  distancia_ate_a_porta = nova Distância(150)
  imprime(distancia_ate_a_porta.centímetros) // 150cm
  imprime(distancia_ate_a_porta.polegadas) // 59,0551 pol.
fim procedimento

Considerando o exemplo anterior, a classe Distância precisa de um padrão para a informação, uma unidade interna. No exemplo, foi codificado como centímetros, no entanto, poderia ser outra unidade. A noção de distância pode ser representada em várias unidades. Na verdade, não só em várias unidades, mas em várias notações, como as 59,0551 pol poderiam ser expressas como 59,0551", ou até aproximada, como 59,1".

São valiosos, nestes casos, dois conceitos previamente discutidos: a abstração e o encapsulamento. A abstração para a simplificação do objeto ao ponto de torná-lo representável em código. O encapsulamento para garantir que o modo como ele é representado internamente, como o atríbuto privado centímetros, não “vaze” para fora. Sendo a representação interna oculta, os usuários do objeto podem tê-lo em qualquer representação por sua interface, como nos métodos centímetros e polegadas (e outros poderiam ser adicionados).

As representações alternativas também servem tipicamente para expressar o objeto para apresentação, seja visual, audível, etc. Geralmente, se busca uma representação textual primeiro, que será usada nas interfaces com usuários, como uma aplicação web ou desktop que precisa apresentar (ou imprimir) o objeto. Por isso, é bastante comum oferecer uma representação textual (String), mesmo que simplificada. Por exemplo, considere o seguinte pseudocódigo:

// Celular.pseucocódigo
classe Celular

  atributo privado marca : textual
  atributo privado modelo : textual
  atributo privado memória : número inteiro
  atributo privado nrosérie : textual
  atributo privado so : textual
  // ... dúzias de outros de atributos

  método textualmente : textual
    retorna atributo marca   + " " +
            atributo modelo  + " " +
            atributo memória + "GB"
  fim método

fim classe

No exemplo, a classe e objetos do tipo Celular teriam dúzias de atributos, mas para uma impressão rápida, como um imprime(cel) poderiam ser usados apenas alguns atributos chave que discriminem o aparelho de forma resumida. Neste caso, o método para_textual permitiria escrever o código imprime(celular.textualmente), que providenciaria uma saída como Motorália M2020 64GB, por exemplo.

Enfim, podem ser adicionados quantos métodos de conversão, ou representações alternativas, for necessário. Objetos também podem ser instanciados a partir de representações variadas. Logo, nos tópicos a seguir será discutido como, na prática, se fornece representações e se cria objetos a partir delas.

9.2 A convenção dos métodos toTipo e fromTipo

Quando um objeto de um determinado tipo (de uma determinada classe) pode ser convertido para outro tipo é praxe escrever um método toTipo (paraTipo se fosse em Português). E quando se converte de outro tipo para o objeto em questão, é usado fromTipo (doTipo se fosse Português). E quando se trata do formato, pode ser usada a mesma convenção.

Por exemplo, considere um objeto para representar valor monetário, dinheiro ou moeda. Uma representação em pseucódigo poderia ser a seguinte:

// Dinheiro.pseucódigo
classe Dinheiro
  atributo privado reais : número inteiro
  atributo privado centavos : número inteiro

  construtor (reais = 0, centavos = 0)
    atributo reais = reais
    atributo reais = centavos / 100
    atributp centavos = centavos % 100
  fim construtor
  // método para representação textual, ex: "R$ 99,99"
  método paraTextual : Textual
    retorna "R$ " + atributo reais + "," + atributo centavos
  fim método
  // método para representação decimal, ex: 99.99
  método paraNumérico : número real
    retorna atributo reais + atributo centavos / 100.0
  fim método
fim classe

No exemplo apresentado, uma instância pode ser obtida com dindin = novo Dinheiro(9, 99) e depois sua representação textual com imprime(dindin.paraTextual) que retornaria o texto R$ 9,99. O método paraNumérico retornaria a representação decimal 9.99. Portanto, temos a mesma informação presentada de três formas, flexibilizando a apresentação do objeto.

Mas como é o caso contrário? Isto é, como é possível instanciar um Dinheiro a partir de uma informação textual ou decimal. Uma opção, viável, seria sobrecarregar o construtor, ou seja, adicionar mais dois construtores, um recebendo um valor textual e outro decimal. No entanto, uma opção alternativa aos construtores são métodos descritivos de conversão de representação, como doTexto ou doNumero, para obter instâncias de Dinheiro a partir destes métodos responsáveis por fabricá-los.

A questão com os métodos fábrica é que eles devem estar disponíveis para instanciar o objeto antes de sua construção, isto é, eles não são métodos do objeto, pois não há objeto. Métodos que precisam ser chamados sem o objeto são chamados métodos estáticos, pois existem sem a instância e seu estado e pertecem, logo, à classe.

A implementação de um método estático é simples como colocar a palavra-chave estático à frente do método como a seguir:

// Dinheiro.pseucódigo
classe Dinheiro
  // ... <- atributos, constutor e métodos omitidos

  // recebe valor no formato textual, ex: "R$ 12,34"
  método estático doTexto(txt : Textual) : Dinheiro
    // decompor a representação textual para passar ao construtor
    partes = txt.separa(" ", ",") // ["R$", "12", "34"]
    // retorna novo Dinheiro(12, 34)
    retorna novo Dinheiro(inteiro de partes[1], inteiro de partes[2])
  fim método
  // recebe valor no formato decimal, ex: 12.34
  método estático doDecimal(valor : número real) : Dinheiro
    // novo Dinheiro (reais = 12, centavos = 1234.0 - 1200 = 34)
    retorna novo Dinheiro (inteiro de valor, valor * 100 - inteiro de valor * 100)
  fim método
fim classe
// App.pseudocódigo
usando Dinheiro de Dinheiro.pseucódigo
procedimento App

  // A instrução de instanciação "novo" acontece
  // dentro dos métodos fábrica que recebem as
  // representações textual e decimal:

  dindin1 = Dinheiro.doTexto("R$ 1,99")
  dindin2 = Dinheiro.doDecimal(1.99)

fim procedimento

O código do exemplo anterior demonstra o uso de métodos fábrica estáticos doTipo para instanciar Dinheiro. O método doTexto, por exemplo, recebe o Dinheiro na sua representação string e, internamente, decompõe o texto para o obter a representação objeto, isto é, popular os atributos reais e centavos.

A implementação nas linguagens de programação é semelhante ao pseudocódigo apresentado, com poucas variações. Se a implementação fosse em Java ou C# e no idioma Inglês seria Dinheiro.fromString e Dinheiro.fromDouble. Por exemplo, considere a representação de cor, como ela é representada digitalmente. Cores são representadas digitalmente através de números de 24bits reservados 8 bits para o canal vermelho (Red), 8 para o verde (Green) e mais 8 bits para o azul (Blue). Este sistema é conhecido como RGB. Os números de 8 bits abrangem de 0 a 255 no sistema decimal. Portanto, uma class Cor com atributos int red, green, blue seria de bom tamanho inicialmente para a representação objeto de uma cor:

// Cor.java
public class Cor {
  // atributos encapsulados
  private int red, green, blue;

  public Cor(int red, int green, int blue) {
    // salva-guardas / validação
    if (red < 0 || red > 255) {
      throw new IllegalArgumentException("Red fora do invervalo 0-255");
    }
    if (green < 0 || green > 255) {
      throw new IllegalArgumentException("Green fora do invervalo 0-255");
    }
    if (blue < 0 || blue > 255) {
      throw new IllegalArgumentException("Blue fora do invervalo 0-255");
    }
    this.red = red;
    this.green = green;
    this.blue = blue;
  }
  // acessores
  public int getRed()   { return this.red; }
  public int getGreen() { return this.green; }
  public int getBlue()  { return this.blue; }

}
// App.java
public class App {
  public static void main(String[] args) {
    // ok para instanciar uma cor
    Cor chocolate = new Cor(210, 105, 30);
  }
}

No código anterior está uma classe básica para representar cores como objetos. O nome da cor bem como os parâmetros foram obtidos no site da W3C em https://www.w3.org/wiki/CSS/Properties/color/keywords. O World Wide Web Consortium mantém a padronização da web, incluindo as palavras-chave para cores que são aceitas pelos navegadores como Edge, Firefox, Chrome, etc. Na linguagem CSS, as cores são representadas a partir de vários sistemas, não apenas o RGB, pois também há o HSL (Hue Saturation Light) e outros. Mesmo em RGB, ela pode se representada pelo nome padronizado, três números decimais (como foi implementado) e como # (hash) e os três números no formato hexadecimal. A cor chocolate, por exemplo, pode ser representada textualmente como rgb(210, 105, 30), #d2691e, hsl(25, 86, 47) e cmyk(0, 50, 86, 18). Para conhecer mais sobre representação codificada de cores é possível experimentar no https://coolors.co - eu já deixei a cor “chocolate web” selecionada. Portanto, a meta agora é permitir que a representação objeto seja convertida para um textual rgb e #hexadecimal, com os métodos toRGBString e toHexString a seguir:

// Cor.java
public class Cor {
  // ... atributos, construtor e acessores omitidos

  public String toRGBString() {
    // os três %d (Decimal integer) são substituídos pelos três inteiros em String.format
    return String.format("rgb(%d, %d, %d)", this.red, this.green, this.blue);
  }

  public String toHexString() {
    // o especificador de formato %x (heXa) converte o inteiro em hexadecimal
    return String.format("#%x%x%x", this.red, this.green, this.blue);
  }
}
// App.java
public class App {
  public static void main(String[] args) {
    // ok para instanciar uma cor
    Cor chocolate = new Cor(210, 105, 30);
    // os métodos "to" em ação
    System.out.println(chocolate.toRGBString()); // rgb(210, 105, 30)
    System.out.println(chocolate.toHexString()); // #d2691e
  }
}

No código anterior foram implementados dois métodos to. O tipo de saída para ambos foi string, variando o formato (rgb ou hex). Outros métodos to poderiam ser incluídos, seja para novos tipos ou formatos. Por exemplo, caso seja útil entregar uma representação em array poderia ser implementado como: int[] toArray() { return new int[]{this.red, this.green, this.blue}; }. Completando este exemplo, considere métodos para instanciar Cor a partir de um hexadecimal, isto é, o método from a seguir:

// Cor.java
public class Cor {
  // ... atributos, construtor, acessores e métodos "to" omitidos

  // aqui está a palavra-chave static, que permite invocar este método antes de invocar "new"
  public static Cor fromHexString(String hashHexadecimal) {
    // string  #d2691e
    // indexes 0123456
    // convertendo a substring "d2" para inteiro, 16 significa base hexa
    int red   = Integer.parseInt(hashHexadecimal.substring(1, 3), 16);
    int green = Integer.parseInt(hashHexadecimal.substring(3, 5), 16);
    int blue  = Integer.parseInt(hashHexadecimal.substring(5, 7), 16);
    return new Cor(red, green, blue);
  }
}
// App.java
public class App {
  public static void main(String[] args) {
    // instanciar a partir do método fábrica estático fromHexString
    Cor chocolate = Cor.fromHexString("#d2691e")
    // ok, a cor foi instanciada da string para objeto
    // e pode ser convertida de volta para string:
    System.out.println(chocolate.toRGBString()); // rgb(210, 105, 30)
  }
}

Considerando o exemplo anterior, outros métodos from poderiam ser adicionados. Uma cor poderia ser instanciada a partir de uma string RGB, um número inteiro, um array de inteiros, etc. É preciso apenas atentar à diferença dos métodos from para os métodos to, que é a declaração de static para poder ser invocado da classe, como foi feito em Cor.fromHexString"#d2691e").

9.3 Representação string | sobrescrevendo toString

Na maioria das linguagens a conversão do objeto para textual (string) é um caso especial e antecipado pela plataforma. Isto é, geralmente há uma estrutura prévia e convenções para esta transformação. O motivo desta padronização acontecer está em como as linguagens usam métodos de impressão, como print, cout, var_dump, etc, de objetos. Elas (as linguagens, e nós programadores e programadoras) precisam que o objeto seja representado de um modo inteligível.

Como o método paraTextual é implementado varia de linguagem para linguagem. Na linguagem Python, por exemplo, é adicionado um método str às classes que determinam a string resultante da representação objeto, como em:

# Retangulo.py
class Retangulo:
  def __init__(self, largura, altura):
    self.largura = largura
    self.altura  = altura

  def __str__(self):
    return '{L}x{A}'.format(L = self.largura, A = self.altura)

r = Retangulo(40, 60) # Em Python não é necessário `new`
print(r) # imprime 40x60 como especificado em __str__

Enquanto Python baseia-se na convenção str, por outro lado, na linguagem Java, todos os objetos já possuem o método toString, embora não muito útil de início, herdado da classe Object (semelhante ao equals e hashCode). Portanto, para implementar a representação textual em Java é necessário sobrescrever o método toString. Isto requer que o método siga a assinatura da classe pai Object sendo @Overrride public String toString(), como no exemplo a seguir:

// Circulo.java
public class Circulo {
  // ...
}
// Retangulo.java
public class Retangulo {
  private int largura, altura;
  public Retangulo(int largura, int altura) {
    this.largura = largura;
    this.altura  = altura;
  }
  public int getLargura() { return this.largura; }
  public int getAltura()  { return this.altura; }

  @Override
  public String toString() {
    return String.format("%dx%d", this.largura, this.altura);
  }
}
// App.java
public class App {
  public static void main(String[] args) {

    Retangulo r = new Retangulo(40, 60);
    // Classe retangulo tem o método toString definido
    System.out.println(r); // imprime 40x60

    // A classe Circulo não tem toString,
    // logo imprime a classe e identidade por padrão
    Circulo c = new Circulo(40);
    System.out.println(c); // imprime Circulo@6ff3c5b5
  }
}

No exemplo anterior foram apresentadas duas classes Retangulo e Circulo. O método toString em Retangulo permite definir como será impresso nas chamadas print e concatenação com strings. Sem a sobrescrita de toString o comportamento padrão, em Java, é imprimir a classe e o id do objeto, como foi no caso do `Circulo, no exemplo.

9.4 Considerações

Qualquer informação digitalizada pode ser tratada e transformada para e de objetos. Contudo, nem sempre é necessário pensar em todos os cenários e popular as classes com métodos to e from. A introdução de métodos de conversão de representações e formatos deve ser bem planejada, para evitar esforço desnecessário. Por fim, este capítulo, além de introduzir a questão da representação e formato, ainda abordou assuntos colaterais, como a sobrescrita, a formatação de strings e até um pouco sobre representação das cores. Os detalhes específicos da linguagem e mais informações sobre o espaço de cores podem ser encontrados facilmente na internet.

9.5 Exercícios

A seguir são apresentados dois exercícios para trabalhar com representações e transformações.

Representação string de Time

Resgatando o exercício Time, sobrescreva o método toString para que a hora seja representada textualmente como hh:mm:ss. Adicione, também, um método toInteger, que devolve o tempo total em segundos (ex.: 57432 que representa 15h57m12s), e um toDouble, que devolve o tempo em horas e decimais (ex.: 19.64 que representa 19h38m24s). É possível também adicionar um método fábrica estático fromString para receber uma hora como 18:10:13 e transformá-la num objeto Time. Escreva testes que cubram estes casos.

Representação string de Fracao

Retomando o exemplo da Fração do Capítulo 7, implemente conforme os casos de teste a seguir:

// App.java
public class App {
  public static void main(String[] args) {
    Fracao frac1 = new Fracao(1, 5);
    double valor1 = frac1.toDouble();
    System.out.println(valor1); // 0.2

    Fracao frac2 = new Fracao(10, 5);
    double valor2 = frac2.toDouble();
    System.out.println(valor2); // 2.0

    String texto1 = frac1.toString();
    System.out.println(texto1); // 1/5

    String texto2 = frac2.toString();
    System.out.println(texto2); // 10/5

    Fracao frac3 = Fracao.fromDouble(0.4375);

    System.out.println(frac3.numerador == 7);
    System.out.println(frac3.denominador == 16);
    System.out.println(frac3); // 7/16    (invoca implicitamente o toString)

    Fracao frac4 = Fracao.fromString("5/6");
    System.out.println(frac3.numerador == 5);
    System.out.println(frac3.denominador == 6);
    System.out.println(frac4); // 5/6
  }
}