ResumosMIEI

Uma coleção de resumos, para ajudar os que precisam daquele empurrão.


Project maintained by mendess Hosted on GitHub Pages — Theme by mattgraham

The stuff Nestor doesn’t tell you

Neste “resumo” vou esclarecer umas coisas importantes sobre Programação Orientada a Objetos.

Getters and Setters

O lixo dos getters/setters devem ser evitados ao máximo. Aliás, se a tua classe for

class Foo {
    private String bar;

    public String get_bar() {
        return bar;
    }

    public void set_bar(String bar) {
        this.bar = bar;
    }
}

O quão menos encapsulado ficava se bar fosse simplesmente public? É a mesma coisa! Há duas principais razões para encapsulamento:

Efetivamente, este código é equivalente a public String bar. Mas deu mais trabalho e não se ganhou nada.

Mas a solução é fazer tudo public? Não, é fazer coisas que fazem sentido e mais nada.

Por exemplo, pegando no clássico Point.

class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Qual parece ser o mais intuitivo de usar?

public move(int x, int y) {
    this.x += x;
    this.y += y;
}

Ou

public set_x(int x) {
    this.x = x;
}

public set_y(int y) {
    this.y = y;
}

?

Agora podias dizer “Mas e se eu quiser que o ponto passe a ter (x,y) coordenadas e não quero fazer as contas para o mover ate ao sitio”. E para isso relembro te que tens um construtor: p = new Point(x, y);. Setters não servem para nada em 99% dos casos.

Claro que há situações em que um setter faz sentido, mas são raras e normalmente é possível construir uma API melhor. Olhem para os métodos do ArrayList, por exemplo. Quantos setters é que aquilo tem?

Artigo

Herança

“Prefer composition over inheritance whenever possible” – Some smart person probably.

Herança é considerado um dos maiores erros de programação orientada a objetos. Eu podia falar aqui de todas as maneiras em que herança é má ideia, mas há gente mais inteligente que eu que já falou sobre isso.

Artigos

Clones

Muito provavelmente já ouviram dizer que, quando querem retornar uma variável de instância devem retornar um .clone() dessa variável de instância através de um getter.

Isto previne que qualquer alteração que o caller faça à variável de instância retornada não seja refletida no estado interno do objeto, mantendo assim o objeto encapsulado. Esta técnica é um exemplo de um software design denominado defensive programming.
No entanto, usar clones, especialmente neste contexto, traz as suas desvantagens.

Desvantagens

A better way

Anteriormente, mencionei que não fazemos .clone() porque queremos uma cópia. Fazemos .clone() para simular imutabilidade, para indicar que não queremos alterações ao estado interno do nosso objeto.
.clone() é apenas uma das formas de obter este resultado, que acaba por ser um remendo que, como observamos, cria mais problemas do que aqueles que resolve.

Existem alternativas, e no seguinte exemplo vou descrever uma delas.
Esta alternativa permite-nos retornar a verdadeira variável de instância, sem cópias intermediárias, sem exceções, sem broken APIs, e sem modificações inesperadas pelo caller.

(Re) Introducing const.
Provavelmente já ouviste falar em const. É uma keyword presente em várias linguagens, que quando adicionada à declaração de uma variável, marca essa variável como imutável. Isto significa que não lhe podes re-atribuir novos valores com o operador =, e não a podes passar a funções que a modificam. Um exemplo em C:

#include <stdio.h>

void print_array(int const* a, size_t n) {  // 'a' marked as const, so it can't
    for (size_t i = 0; i < n; ++i) {        //  be modified by this function
        printf("%d\n", a[i]);
    }
}

void zero_array(int* a, size_t n) {   // 'a' isn't marked as const, so it can be
    for (size_t i = 0; i < n; ++i) {  // modified by this function
        a[i] = 0;  // mutation occurs here
    }
}

int main(void) {
    int const values[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};  // immutable array
    print_array(values, 10);  // valid, not modifying 'values'
    zero_array(values, 10);  // invalid, modifying 'values'
}

Curiosamente, esta keyword já existe em C desde 1989. Considerando que Java foi criado em 1996, parece que conseguimos regredir.

Adiante, a nossa estratégia aqui é, para cada class que definirmos, definimos uma interface que só declara métodos que não modificam a class, e a class implementa esta interface.
Se a class se chamar Bar, vamos chamar à interface BarView. É uma view de Bar. Podemos ver, mas não tocar.
Sempre que queremos retornar um Bar que não deve ser modificado pelo caller, em vez de retornar um Bar, retornamos uma BarView. Aplicando este design ao exemplo anterior, obtemos o seguinte código:

interface BarView {  // only declares methods that don't mutate Bar
    ArrayList<String> get_strings();
}

class Bar implements BarView {
    private ArrayList<String> strings;

    public Bar() {
        this.strings = new ArrayList<>();
    }

    public ArrayList<String> get_strings() {
        return (ArrayList<String>)
            this.strings.clone();  // a clone still happens if this function is
        }                          // called. We could, in practice, define an
                                   // ArrayListView, but there's a much simpler
                                   // solution (to be written soon TM).

    public void clear_strings() {
        this.strings.clear();
    }
}

class Foo {
    private Bar bar;

    public Foo() {
        this.bar = new Bar();
    }

    public BarView get_bar() {  // now returns an immutable view
        return this.bar;  // implicit cast from Bar to BarView
    }
}

// Takes a Bar and computes some result, but does not modify the Bar.
// Since it doesn't modify the Bar, we can pass a BarView instead.
void compute(BarView bar) { /* ... */ }

var all_foos = new ArrayList<Foo>(/* ... */);
for (var foo : all_foos) {
    compute(foo.get_bar());  // no more unnecessary clones :)
}

Conseguimos definir um método de garantir imutabilidade que:

Este é um padrão tão útil que Kotlin, outra linguagem baseada na JVM, o integrou na sua standard library. Ver List e MutableList como alguns dos muitos exemplos desse padrão.

Nada disto seria necessário

Embora a solução descrita esteja a resolver o problema, está a resolver um problema que não devia existir.
Em programação orientada a objetos, os objetos fazem o trabalho por nós, não o contrário. No exemplo anterior, estamos a “dissecar” um Foo através do método get_bar(), e a fazer o “trabalho” com a função compute(). A abordagem correta, em OOP, seria ter a função compute() como um método de instância de Foo:

class Bar {
    private ArrayList<String> strings;

    public Bar() {
        this.strings = new ArrayList<>();
    }

    public ArrayList<String> get_strings() {
        return (ArrayList<String>) this.strings.clone();
    }

    public void clear_strings() {
        this.strings.clear();
    }
}

class Foo {
    private Bar bar;

    public Foo() {
        this.bar = new Bar();
    }

    public void compute() { /* ... */ }
}

var all_foos = new ArrayList<Foo>(/* ... */);
all_foos.forEach(Foo::compute);  // functional flavour

Isto evita a necessidade de clones, getters, interfaces, e em geral melhora a ergonomia do código. Conseguimos reduzir significativamente a quantidade de boilerplate code. O código ficou mais sucinto, explícito, e legível.

No entanto, isto nem sempre é possível. Pode acontecer que Bar e Foo façam parte de uma biblioteca externa que não possamos modificar. Neste caso, temos de nos conformar com a interface já existente.