Projeto Orientado a Componentes, parte III
Agora, uma pausa para reflexão. Esta dicotomia interface versus implementação é difícil de definir. Por baixo dos panos nós temos memória de onde nós lemos e para onde nós escrevemos. O que caracteriza uma interface e o que a distingue de uma implementação?
Essa pergunta só pode ser respondida em um determinado contexto. Um bom sistema escrito em assembler talvez tenha interfaces mais bem definidas que um mau sistema escrito em Java. Porém, está claro para a indústria que certos mecanismos de linguagem favorecem o estabelecimento de boas interfaces no sistema implementado.
Além disso, a noção de interface surge diante de um problema de substituição. É interface aquilo que, mantendo-se estável, permite a substituição daquilo que é implementação. Porém, o que é este permite? Este permite, quando caracterizado, por conseguinte caracteriza o que é interface.
Suponha um sistema escrito em C++ por uma equipe que considera irrisório o tempo de recompilação do código-fonte. Neste contexto, podemos seguramente aceitar que a seguinte substituição mantém estável uma interface.
antes
class Foo {
public:
Foo ();
void
mutate_stuff ();
int
observe_stuff () const;
private:
Bar* m_bar;
};
depois
class Foo {
public:
Foo ();
void
mutate_stuff ();
int
observe_stuff () const;
private:
int m_cache;
shared_ptr<bar> m_bar;
};
Após a alteração acima e uma recompilação o sistema continua a funcionar normalmente. [1] Dizendo de forma mais extensa: neste contexto, a substituição realizada não altera propriedades observáveis externamente da classe, de modo que do ponto de vista de um observador, a classe mantém estável sua interface. (Apenas membros privados foram alterados.)
Agora vamos alterar as nossas premissas. O tempo de recompilação desse sistema é enorme e o ciclo de testes não pode esperar, de modo que a estrutura de classes do sistema foi particionada e esta classe mora dentro de um objeto compartilhado, uma biblioteca dinâmica.
A equipe realiza a substituição, recompila o objeto compartilhado e o entrega a uma equipe de testes.
E os testes falham miseravelmente.
Isto acontece porque, neste novo contexto, há mais propriedades observáveis a considerar, que devem se manter estáveis -- que fazem parte da interface da classe.
Neste caso, o layout de um objeto da classe Foo é uma propriedade observável; um programa compilado criando objetos da definição antes criará objetos com um layout diferente daquele esperado pelo objeto compartilhado que espera objetos dada a definição depois.
A definição depois tem um int no offset onde a definição antes tinha um ponteiro, ela ocupa mais memória etc.
Podemos dizer que o primeiro caso é o caso de um projeto orientado a objetos, onde a alteração de elementos privados da classe não altera a interface; e podemos dizer que o segundo caso é o caso de um projeto orientado a componentes, onde a alteração do layout na memória de uma classe altera sua interface.
Como no segundo caso nós estamos violando a interface do componente, nós não podemos substituir o componente de antes pelo componente de depois impunemente. Dito de trás para a frente, pelo fato de não podermos substituir o componente de antes pelo componente de depois, por definição estamos violando sua interface.
Imagine então o que acontece em um sistema spaghetti, onde todas as classes mantém referência a todas as outras classes; uma violação de interface em um "componente" perturbará implacavelmente todos os outros "componentes".
Quando projetamos um sistema pensando em componentes, ou quando desejamos aplicar a idéia forte de interface para melhorar o nosso projeto diminuindo o acoplamento entre as coisas que são distintas, é útil usar das ferramentas à disposição para dar forma ao que é uma interface.
Linguagens que se propõe a facilitar o desenvolvimento de componentes, como Java e C#, possuem como parte integrante do seu vocabulário e mecanismo nativo uma interface em contraste com as classes. Normalmente algo do tipo:
interface IFoo {
void
mutate_stuff ();
int
observe_stuff () const;
};
Em C++ não existe uma construção análoga mas é possível emulá-la integralmente usando classes, da seguinte maneira:
class IFoo {
virtual
void
mutate_stuff () = 0;
virtual
int
observe_stuff () const = 0;
};
(O leitor astuto observará que esta construção-interface não possui atributos.)
O propósito de usar construções como estas é evidenciar a natureza destes elementos como interfaces -- aquilo que se deseja manter estável -- e diminuir a chance disto que é interface ser inadvertidamente alterado -- causando o desastre. A construção-interface é a melhor amiga do projetista de componentes, permitindo a representação de um conceito de projeto diretamente na linguagem do código-fonte, restringindo as possibilidades de violação durante o processo de implementação, ou ao menos tornando a (necessidade de) violação claramente evidente.
Infelizmente para nós nem tudo que existe na fronteira de um componente pode ser uma interface, já que é preciso comunicar coisas de um componente para outro. E quem pode viver comunicando apenas doubles e bools? Nós queremos comunicar objetos.
Além disso, as construções-interface não são garantia de estabilidade de interfaces, já que há mais sobre o que é publicamente observável em um objeto que sua estrutura: há o seu comportamento.
[1] Estamos, naturalmente, assumindo que o programador não é louco e que a alteração tem algum sentido, exatamente como aparenta ter.
