Classes Amigas tem acesso ao estado (privado) de seus objetos. Apesar dessa prática ser entendida por muitos desenvolvedores como uma quebra do encapsulamento, na verdade ela pode até aprimorá-lo.
Photo by Ben White on Unsplash
Classes Amigas existem na linguagem C++ a muito tempo, assim como na linguagem Object Pascal.
Enquanto na linguagem C++ é necessária uma sintaxe específica, em Object Pascal basta declararmos as classes na mesma unit para que elas sejam “amigas”. E não importa se elas fazem parte ou não de uma hierarquia de herança.
Eu conheço alguns desenvolvedores que não lidam muito bem com isso. Para eles, o que é privado nunca deveria ser acessado por outra instância além do próprio objeto.
Eles estão certos.
Mas essa é uma regra geral. Precisamos saber quando “quebrar” as regras, se isso for aumentar a qualidade do software.
Eu também fui contra a essa feature by design por muito tempo. A muitos anos atrás eu declarava as classes em units separadas, mesmo que seus objetos existissem apenas para “conversar” entre si, ou seja, mesmo que tais objetos fossem bem “íntimos”.
A fim de seguir a “regra de ouro” do encapsulamento, eu declarava tais classes em units separadas pensando que eu estava desenvolvendo mais Orientado a Objetos protegendo seus estados.
Mas, ao mesmo tempo, eu declarava as propriedades de acesso — também conhecidos com Get/Set — para que o estado desses objetos pudessem ser acessados por alguma instância de fora!
E isso não faz sentido.
Eu estava escrevendo e complicando mais o código, sem ganhar nenhum benefício, já que toda essa “proteção”, na verdade, não existia.
Então, eu voltei a rever esse conceito das Classes Amigas.
Eu diria que Classes Amigas enfraquecem o encapsulamento local (na unit) afim de fortalecer o encapsulamento global (por todo o projeto).
Ainda assim, é claro, isso irá depender de alguns fatores. Utilizá-las é mais uma exceção à regra e deve ser muito bem pensado afim de obter mais prós do que contras.
Vamos a um exemplo: Imagine que você tem uma classe que representa um livro. Na interface da classe desse livro nós só queremos ter o básico que, no nosso exemplo, irá conter métodos para retornar o título, autor e ISBN.
type
IBook = interface
function Title: string;
function Author: string;
function ISBN: string;
end;
TBook = class(TInterfacedObject, IBook)
private
FDoc: IXMLDocument;
public
constructor Create(const Doc: IXMLDocument);
function Title: string;
function Author: string;
function ISBN: string;
end;
Temos uma interface IBook
e também uma classe que a implementa.
O código é bem simples e de fácil entendimento.
A classe TBook
recebe uma instância de IXMLDocument
proveniente de algum lugar. Não importa.
Esse objeto XML possui toda a informação referente a um livro.
Hoje, apenas os 3 atributos estariam contidos no XML mas tenha em mente que ele pode ser alterado. Poderíamos adicionar mais nós de informação futuramente, se assim o desejarmos.
Internamente a classe TBook
irá fazer o parser do XML, retornando suas informações através dos respectivos métodos.
Imaginemos que tudo funciona bem por meses e muitas outras classes podem ter implementado a mesma interface IBook
. Por exemplo, TDbBook
, TEmptyBook
, TNullBook
, TJSONBook
, TXMLBook
, etc.
Então, uma nova regra de negócio é solicitada: é necessário saber o título original e o ano de publicação.
Se alterarmos a interface atual, teremos que implementar os novos métodos em todas as classes que a implementam. Isso é bastante trabalho e pode não ser uma boa ideia.
Outra opção é implementar uma nova classe, especializando IXMLDocument
como uma interface para ser implementada por uma classe de dados. Por exemplo, IBookData
. Todo o código de parser que antes estava contido em TBook
, deverá ser migrado para a nova classe, refatorando não só TBook
mas também todas as outras implementações de IBook
que, porventura, tenham o construtor parecido. Isso também é bastante trabalho, talvez mais trabalho do que a primeira opção.
A outra opção (mas pode haver muitas) é utilizar Classes Amigas criando apenas o necessário sem alterar nenhuma outra classe, escrevendo menos e de forma mais simples.
type
ICompletedBook = interface(IBook)
function OriginalTitle: string;
function Year: Integer;
end;
TCompletedBook = class(TBook, ICompletedBook)
public
function OriginalTitle: string;
function Year: Integer;
end;
Agora temos uma nova interface e uma nova classe que a implementa. Ambos utilizando herança, pois ambas são muito íntimas dos seus predecessores.
Dentro dos métodos da classe TCompletedBook
, será necessário ter acesso ao atributo privado de TBook
denominado FDoc
.
Entretanto, se ambas as classes são amigas, ou seja, declaradas na mesma unit, não haverá problemas para fazer isso:
function TCompletedBook.OriginalTitle: string;
begin
Result := FDoc.Node('original-title').AsString;
end;
function TCompletedBook.Year: Integer;
begin
Result := FDoc.Node('year').AsInteger;
end;
O atributo FDoc
é acessível, mesmo sendo privado.
Você poderia pensar em alterar a visibilidade desse atributo para protected
e deixar que até mesmo o código de outros usuários possam herdar de TBook
, mas isso seria um erro.
Ao tornar os atributos acessíveis para qualquer classe, você enfraquece o encapsulamento global e pode perder o controle do código. Seria muito mais difícil fazer alguma alteração em TBook
pois não há como saber, com certeza, quem está utilizando os atributos privados.
Finalmente, ao utilizar o modelo de Classes Amigas, você sabe quais classes deverão ser alteradas se houver alguma refatoração, pois todas elas estarão declaradas na mesma unit, obrigatoriamente.
Suas classes serão menores e mais simples.
Há seres humanos que são mais ligados intimamente a uns do que outros — mesmo todos sendo provenientes da mesma “classe” — que sabem seus gostos, desejos e segredos mais íntimos.
A mesma lógica pode também ser aplicada a apenas algumas classes onde seus objetos são mais amigos, concedendo acesso irrestrito uns aos outros, mas bloqueando esse acesso ao mundo exterior.
Até logo.