- “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;
}
- 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);
}
- “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();
}
public interface Vehicle {
double getPercentFuelRemaining();
}
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();
}
}
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 emGeometry
. - 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;
}
}
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();
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();
- Essa função acima tem muito conhecimento.
- Se
ctxt
,options
escracthDir
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;
- 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);
- 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;
}
}
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”.
Top comments (0)