DEV Community

Jonilson Sousa
Jonilson Sousa

Posted on • Updated on

Anotações Capítulo 6: Objects and Data Structures

  • “Há um motivo para declararmos nossas variáveis como privadas. Não queremos que ninguém dependa delas”;
  • Assim temos a liberdade para alterar o tipo ou a implementação;
  • Porque, então, tantos programadores adicionam automaticamente métodos de acesso em seus objetos, como se fossem públicas?

Abstração de dados

  • Representação de dados de um ponto no plano cartesiano:
  • Caso concreto:
public class Point {
    public double x;
    public double y;
}
Enter fullscreen mode Exit fullscreen mode
  • Caso abstrato:
public interface Point {
    double getX();
    double getY();
    void setCartesian(double x, double y);
    double getR();
    double getTheta();
    void setPolar(double r, double theta);
}
Enter fullscreen mode Exit fullscreen mode
  • “Assim, no “caso abstrato” não há como dizer se a implementação possui coordenadas retangulares ou polares. Pode não ser nenhuma! E ainda assim a interface representa de modo claro uma estrutura de dados.”
  • “Os métodos exigem uma regra de acesso. Você pode ler as coordenadas individuais independente, mas deve configurá-las juntas como uma operação atômica”.
  • Já no “caso concreto” está implementada em coordenadas retangulares, e nos obriga a manipulá-las independentemente. Isso expõe a implementação.
  • A implementação no “caso concreto” seria exposta mesmo se as variáveis fossem privadas e estivéssemos usando métodos únicos de escrita e leitura de variáveis.
  • “Ocultar a implementação não é só uma questão de colocar uma camada de funções entre as variáveis. Ocultar a implementação tem a ver com abstrações!”;
  • Uma classe não passa suas variáveis simplesmente por meio de métodos getters e setters.
  • Em vez disso, ele expõe interfaces abstratas que permitem que seus usuários manipulem a essência dos dados, sem precisar saber sua implementação.
  • Veja as listagens abaixo, a primeira usa termos concretos para comunicar o nível de combustível de um veículo, enquanto a segunda faz isso com a abstração da porcentagem.
public interface Vehicle {
    double getFuelTankCapacityInGallons();
    double getGallonsOfGasoline();
}
Enter fullscreen mode Exit fullscreen mode
public interface Vehicle {
    double getPercentFuelRemaining();
}
Enter fullscreen mode Exit fullscreen mode

No primeiro caso, você tem certeza de que são apenas métodos acessores (getter e setter). No segundo, não há como saber o tipo de dados.

  • Nos casos acima, o segundo é preferível. Não queremos expor os detalhes de nossos dados.
  • Queremos expressar nossos dados de forma abstrata. Isso não se consegue meramente através de interfaces e/ou getters e setters. É preciso pensar bastante na melhor maneira de representar os dados que um objeto contém.
  • A pior opção é adicionar levianamente métodos getter e setter.

Anti-Simetria de Dados/Objeto

  • Os objetos usam abstrações para esconder seus dados, e expõem as funções que operam em tais dados.
  • As estruturas de dados expõem seus dados e não possuem funções significativas.
  • Veja a classe de forma procedural:
public class Square {
    public Point topLeft;
    public double side;
}

public class Rectangle {
    public Point topLeft;
    public double height;
    public double width;
}

public class Circle {
    public Point center;
    public double radius;
}

public class Geometry {
    public final double PI = 3.141592653589793;

    public double area(Object shape) throws NoSuchShapeException {
        if (shape instanceof Square) {
            Square s = (Square)shape;
            return s.side * s.side;
        } else if (shape instanceof Rectangle) {
            Rectangle r = (Rectangle)shape;
            return r.height * r.width;
        } else if (shape instanceof Circle) {
            Circle c = (Circle)shape;
            return PI * c.radius * c.radius;
        }
        throw new NoSuchShapeException();
    }
}
Enter fullscreen mode Exit fullscreen mode

Nesse exemplo a classe Geometry opera sobre as três outras classes que são simples estruturas de dados sem qualquer atividade.

  • Esse exemplo anterior é procedural, mas nem sempre. Se adicionássemos uma função perimeter() à Geometry, as demais classes não seriam afetadas! Assim como outras que dependesse delas!
  • Porém se adicionarmos uma nova classe shape, teremos que alterar todas as funções em Geometry.
  • Agora uma solução orientada a objeto:
public class Square implements Shape {
    private Point topLeft;
    private double side;

    public double area() {
        return side*side;
    }
}

public class Rectangle implements Shape {
    private Point topLeft;
    private double height;
    private double width;

    public double area() {
        return height * width;
    }
}

public class Circle implements Shape {
    private Point center;
    private double radius;
    public final double PI = 3.141592653589793;

    public double area() {
        return PI * radius * radius;
    }
}
Enter fullscreen mode Exit fullscreen mode

O método area() é polifórmico, assim não é necessária a classe Geometry. Portanto, se adicionarmos uma nova forma, nenhuma das funções existentes serão afetadas, mas se adicionarmos uma nova função, todas as classes shape deverão ser alteradas.

  • Duas definições complementares, mas praticamente opostas: “O código procedural (usado em estruturas de dados) facilita a adição de novas funções sem precisar alterar as estruturas de dados existentes. O código orientado a objeto (OO), por outro lado, facilita a adição de novas classes sem precisar alterar as funções existentes”.
  • O inverso também é verdade: “O código procedural dificulta a adição de novas estruturas de dados, pois todas as funções teriam de ser alteradas. O código OO dificulta a adição de novas funções, pois todas as classes teriam de ser alteradas”.
  • “O que é difícil para a OO é fácil para o procedural, e o que é difícil para o procedural é fácil para a OO”.
  • Quando desejarmos adicionar novos tipos de dados em vez de novas funções, os objetos e OO são mais apropriados.
  • Por outro lado, se desejarmos adicionar novas funções em vez de tipos de dados, estruturas de dados e código procedural são mais adequados.
  • Programadores experientes sabem que a ideia de que tudo é um objeto “é um mito”. Às vezes, você realmente “deseja” estruturas de dados simples com procedimentos operando nelas.

A lei de Demeter

  • “Um módulo não deve enxergar o interior dos objetos que ele manipula”.
  • Objetos escondem seus dados e expõem as operações.
  • Um objeto não deve expor sua estrutura interna por meio dos métodos acessores, pois isso seria expor sua estrutura interna.
  • A lei fala que um método “f” de uma classe “C” só deve chamar os métodos de: C, um objeto criado por “f”, um objeto passado como parâmetro para “f”, um objeto dentro de uma instância da variável “C”.
  • O método não deve chamar os métodos em objetos retornados por qualquer outra das funções permitidas.
  • “Fale apenas com conhecidos, não com estranhos”.
  • Um exemplo que viola essa lei:
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
Enter fullscreen mode Exit fullscreen mode

Trem descarrilhado

  • Esse código acima costuma ser chamado de acidente de trem, pois parece com um monte de carrinhos de trem acoplados.
  • Cadeias de chamadas como essa geralmente são consideradas descuidadas e devem ser evitadas. É melhor dividi-las assim:
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
Enter fullscreen mode Exit fullscreen mode
  • Essa função acima tem muito conhecimento.
  • Se ctxt, options e scracthDir forem objetos, então este código viola a lei, já que sua estrutura interna está exposta. Porém, se forem apenas estruturas de dados sem atividades, então elas naturalmente expõem suas estruturas internas, e não se aplica a lei.
  • O uso de assessores (getters) confunde as questões. Se o código tiver escrito como abaixo, não teríamos dúvidas:
final String outputDir = ctxt.options.scratchDir.absolutePath;
Enter fullscreen mode Exit fullscreen mode
  • Seria menos confuso se as estruturas de dados tivessem apenas variáveis públicas e nenhuma função, e os objetos apenas variáveis privadas e funções públicas.

Híbridos

  • Essa confusão leva a estruturas híbridas ruins, que são metade objeto e metade estrutura de dados.
  • Funções que fazem algo significativo, e variáveis ou métodos de acesso e de alteração públicos, que para todos os efeitos, tornam públicas as variáveis privadas, deixando com que funções externas usem tais variáveis de forma como um programa procedural usaria.
  • Esses híbridos dificultam tanto a adição de novas funções como de novas estruturas de dados.
  • São a pior coisa em ambas as condições. Evite criá-los.

Estruturas ocultas

  • Se ctxt for um objeto, devemos dizê-lo para fazer algo, não devemos perguntá-lo sobre sua estrutura interna.
  • Devemos sempre pedir para que o próprio objeto faça a ação:
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
Enter fullscreen mode Exit fullscreen mode
  • Esse código acima é algo razoável para um objeto fazer.
  • Dessa forma o ctxt esconde sua estrutura interna e evita que a função atual viole a Lei de Demeter ao navegar por objetos os quais ela não deveria enxergar.

Objetos de transferência de dados

  • A forma perfeita de uma estrutura de dados é uma classe com variáveis públicas e nenhuma função.
  • Geralmente chama-se isso de objeto de transferência de dados, ou DTO.
  • Os DTOs, são estruturas muito úteis, para se comunicar com banco de dados ou mensagens e assim por diante.
  • O formulário “bean” abaixo, têm variáveis privadas manipuladas por métodos de escrita e leitura, e o aparente encapsulamento dos beans parece fazer alguns puristas da OO se sentirem melhores:
public class Address {
    private String street;
    private String streetExtra;
    private String city;
    private String state;
    private String zip;

    public Address(String street, String streetExtra, String city, String state, String zip) {
        this.street = street;
        this.streetExtra = streetExtra;
        this.city = city;
        this.state = state;
        this.zip = zip;
    }

    public String getStreet() {
        return street;
    }

    public String getStreetExtra() {
        return streetExtra;
    }

    public String getCity() {
        return city;
    }

    public String getState() {
        return state;
    }

    public String getZip() {
        return zip;
    }
}
Enter fullscreen mode Exit fullscreen mode

O Active Record

  • São formas especiais de DTOs;
  • São estruturas de dados com variáveis públicas;
  • Mas eles tipicamente possuem métodos de navegação, como save e find.
  • São traduções diretas das tabelas de bancos de dados ou de outras fontes de dados.
  • Costumamos ver desenvolvedores tratando essas estruturas de dados como se fossem objetos, colocando regras de negócios. Isso é complicado, pois cria um híbrido.
  • A solução para o ponto anterior é tratar o Active Record como uma estrutura de dados e criar objetos separados que contenham as regras de negócio e que ocultem seus dados internos.

Conclusão

  • Objetos expõem as ações e ocultam os dados. Assim, facilita a adição de novos tipos de objetos sem precisar modificar as ações existentes e dificulta a inclusão de novas atividades em objetos existentes.
  • As estruturas de dados expõem os dados e não possuem ações significativas. Assim, facilita a adição de novas ações às estruturas de dados existentes e dificulta a inclusão de novas estruturas de dados em funções existentes.
  • Às vezes buscamos flexibilidade para adicionar novos tipos de dados, e optamos por objetos.
  • Às vezes buscamos flexibilidade para adicionar novas ações, e optamos por estrutura de dados e procedimentos.
  • “Bons desenvolvedores de software entendem essas questões sem preconceito e selecionam a abordagem que melhor se aplica no momento”.

Discussion (0)