O retorno do ponteiro para objetos compostos viola o encapsulamento

8

Quando eu quero criar um objeto que agregue outros objetos, me desejo dar acesso aos objetos internos em vez de revelar a interface para os objetos internos com funções de passagem.

Por exemplo, digamos que temos dois objetos:

class Engine;
using EnginePtr = unique_ptr<Engine>;
class Engine
{
public:
    Engine( int size ) : mySize( 1 ) { setSize( size ); }
    int getSize() const { return mySize; }
    void setSize( const int size ) { mySize = size; }
    void doStuff() const { /* do stuff */ }
private:
    int mySize;
};

class ModelName;
using ModelNamePtr = unique_ptr<ModelName>;
class ModelName
{
public:
    ModelName( const string& name ) : myName( name ) { setName( name ); }
    string getName() const { return myName; }
    void setName( const string& name ) { myName = name; }
    void doSomething() const { /* do something */ }
private:
    string myName;
};

Digamos que queremos ter um objeto Car que seja composto de um Engine e um ModelName (isso é obviamente inventado). Uma maneira possível de fazer isso seria dar acesso a cada uma dessas

/* give access */
class Car1
{
public:
    Car1() : myModelName{ new ModelName{ "default" } }, myEngine{ new Engine{ 2 } } {}
    const ModelNamePtr& getModelName() const { return myModelName; }
    const EnginePtr& getEngine() const { return myEngine; }
private:
    ModelNamePtr myModelName;
    EnginePtr myEngine;
};

O uso desse objeto ficaria assim:

Car1 car1;
car1.getModelName()->setName( "Accord" );
car1.getEngine()->setSize( 2 );
car1.getEngine()->doStuff();

Outra possibilidade seria criar uma função pública no objeto Car para cada uma das funções (desejadas) nos objetos internos, assim:

/* passthrough functions */
class Car2
{
public:
    Car2() : myModelName{ new ModelName{ "default" } }, myEngine{ new Engine{ 2 } } {}
    string getModelName() const { return myModelName->getName(); }
    void setModelName( const string& name ) { myModelName->setName( name ); }
    void doModelnameSomething() const { myModelName->doSomething(); }
    int getEngineSize() const { return myEngine->getSize(); }
    void setEngineSize( const int size ) { myEngine->setSize( size ); }
    void doEngineStuff() const { myEngine->doStuff(); }
private:
    ModelNamePtr myModelName;
    EnginePtr myEngine;
};

O segundo exemplo seria usado assim:

Car2 car2;
car2.setModelName( "Accord" );
car2.setEngineSize( 2 );
car2.doEngineStuff();

Minha preocupação com o primeiro exemplo é que ele viola o encapsulamento de OO, fornecendo acesso direto aos membros privados.

Minha preocupação com o segundo exemplo é que, à medida que atingimos níveis mais altos na hierarquia de classes, poderíamos terminar com classes "divinas" que possuem interfaces públicas muito grandes (viola o "I" no SOLID).

Qual dos dois exemplos representa melhor design de OO? Ou os dois exemplos demonstram falta de compreensão de OO?

Matthew James Briggs
fonte

Respostas:

5

Eu me pego querendo dar acesso aos objetos internos em vez de revelar a interface para os objetos internos com funções de passagem.

Então, por que então é interno?

O objetivo não é "revelar a interface para o objeto interno", mas criar uma interface coerente, consistente e expressiva. Se a funcionalidade de um objeto interno precisar ser exposta e ocorrer uma passagem simples, faça a passagem. Um bom design é o objetivo, não "evitar a codificação trivial".

Dar acesso a um objeto interno significa:

  • O cliente precisa saber sobre esses componentes internos para usá-los.
  • O acima mencionado significa que a captação desejada é soprada para fora da água.
  • Você expõe os outros métodos e propriedades públicos do objeto interno, permitindo que o cliente manipule seu estado de maneira não intencional.
  • Acoplamento significativamente aumentado. Agora você corre o risco de quebrar o código do cliente, caso modifique o objeto interno, altere a assinatura do método ou substitua o objeto inteiro (altere o tipo).
  • Tudo isso é por que temos a Lei de Demeter. Deméter não diz "bem, se está apenas passando, não há problema em ignorar esse princípio".
radarbob
fonte
Nota lateral: o mecanismo é muito relevante para a interface MaintainableVehicle, mas não é relevante para o DrivableVehicle. O mecânico precisa saber sobre o motor (em todos os detalhes, provavelmente), mas o motorista não. (E os passageiros não precisam saber sobre o volante)
user253751 24/02
2

Não acho que viole necessariamente o encapsulamento para retornar referências ao objeto empacotado, principalmente se forem const. Ambos std::stringe std::vectorfaça isso. Se você pode começar a alterar as partes internas do objeto por baixo dele sem passar por sua interface, isso é mais questionável, mas se você já conseguia fazer isso com setters, o encapsulamento era uma ilusão de qualquer maneira.

Os contêineres são especialmente difíceis de se encaixar nesse paradigma; é difícil imaginar uma lista útil que não possa ser decomposta em cabeça e cauda. Até certo ponto, você pode escrever interfaces como std::find()estas ortogonais ao layout interno da estrutura de dados. Haskell vai além com classes como Dobrável e Traversível. Mas em algum momento, você acabou dizendo que tudo o que queria quebrar o encapsulamento agora está dentro da barreira do encapsulamento.

Davislor
fonte
E particularmente se a referência for a uma classe abstrata que é estendida pela implementação concreta que sua classe usa; você não está revelando a implementação, mas apenas fornecendo uma interface que a implementação deve suportar.
Jules