Princípio de Segregação de Interface: O que fazer se as interfaces tiverem sobreposição significativa?

9

De desenvolvimento, princípios, padrões e práticas de software ágil: Pearson New International Edition :

Às vezes, os métodos invocados por diferentes grupos de clientes se sobrepõem. Se a sobreposição for pequena, as interfaces para os grupos deverão permanecer separadas. As funções comuns devem ser declaradas em todas as interfaces sobrepostas. A classe do servidor herdará as funções comuns de cada uma dessas interfaces, mas as implementará apenas uma vez.

Tio Bob, fala sobre o caso quando há pouca sobreposição.

O que devemos fazer se houver sobreposição significativa?

Diga que temos

Class UiInterface1;
Class UiInterface2;
Class UiInterface3;

Class UiIterface : public UiInterface1, public UiInterface2, public UiInterface3{};

O que devemos fazer se houver sobreposição significativa entre UiInterface1e UiInterface2?

q126y
fonte
Quando me deparei com uma interface fortemente sobreposta, crio uma interface pai, que agrupa os métodos comuns e depois herdo desse método comum para criar especializações. MAS! Se você nunca deseja que alguém use a interface comum sem a especialização, na verdade você precisa fazer a duplicação de código, porque se você introduzir a interface comum padrão, as pessoas poderão usá-la.
Andy Andy
A pergunta é um pouco vaga para mim, pode-se responder com muitas soluções diferentes, dependendo dos casos. Por que a sobreposição cresceu?
Arthur Havlicek

Respostas:

1

Fundição

Isso quase certamente será uma tangente completa à abordagem do livro citado, mas uma maneira de se adaptar melhor ao ISP é adotar uma mentalidade de elenco em uma área central da sua base de código usando uma QueryInterfaceabordagem no estilo COM.

Muitas das tentações de projetar interfaces sobrepostas em um contexto puro de interface geralmente vêm do desejo de tornar as interfaces "auto-suficientes" mais do que executar uma responsabilidade precisa, semelhante a um atirador de elite.

Por exemplo, pode parecer estranho projetar funções de cliente como esta:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `position` and `parenting` parameters should point to the 
// same object.
Vec2i abs_position(IPosition* position, IParenting* parenting)
{
     const Vec2i xy = position->xy();
     auto parent = parenting->parent();
     if (parent)
     {
         // If the entity has a parent, return the sum of the
         // parent position and the entity's local position.
         return xy + abs_position(dynamic_cast<IPosition*>(parent),
                                  dynamic_cast<IParenting*>(parent));
     }
     return xy;
}

... além de muito feio / perigoso, já que estamos perdendo a responsabilidade de fazer a conversão propensa a erros no código do cliente usando essas interfaces e / ou passando o mesmo objeto como argumento várias vezes para vários parâmetros do mesmo função. Por isso, muitas vezes acabamos querendo projetar uma interface mais diluída que consolide as preocupações de IParentinge IPositionem um só lugar, como IGuiElementalgo que se torna suscetível de se sobrepor às preocupações de interfaces ortogonais que também serão tentadas a ter mais funções-membro para a mesma razão de "auto-suficiência".

Responsabilidades de Mistura vs. Elenco

Ao projetar interfaces com uma responsabilidade ultra-singular e totalmente destilada, a tentação geralmente é aceitar algumas downcasting ou consolidar interfaces para cumprir várias responsabilidades (e, portanto, seguir o ISP e o SRP).

Usando uma abordagem no estilo COM (apenas a QueryInterfaceparte), adotamos a abordagem de downcasting, mas consolidamos o casting em um local central na base de código e podemos fazer algo mais como isto:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should implement `IPosition` and optionally `IParenting`.
Vec2i abs_position(Object* obj)
{
     // `Object::query_interface` returns nullptr if the interface is
     // not provided by the entity. `Object` is an abstract base class
     // inherited by all entities using this interface query system.
     IPosition* position = obj->query_interface<IPosition>();
     assert(position && "obj does not implement IPosition!");
     const Vec2i xy = position->xy();

     IParenting* parenting = obj->query_interface<IParenting>();
     if (parenting && parenting->parent()->query_interface<IPosition>())
     {
         // If the entity implements IParenting and has a parent, 
         // return the sum of the parent position and the entity's 
         // local position.
         return xy + abs_position(parenting->parent());
     }
     return xy;
}

... é claro, espero que com invólucros com segurança de tipo e tudo o que você possa construir centralmente para obter algo mais seguro do que ponteiros brutos.

Com isso, a tentação de projetar interfaces sobrepostas é frequentemente atenuada ao mínimo absoluto. Ele permite que você crie interfaces com responsabilidades muito singulares (às vezes apenas uma função de membro) que você pode misturar e combinar tudo o que quiser sem se preocupar com o ISP e obter a flexibilidade de digitar pseudo-pato em tempo de execução em C ++ (embora, é claro, com a compensação das penalidades de tempo de execução para consultar objetos para ver se eles suportam uma interface específica). A parte do tempo de execução pode ser importante em, por exemplo, uma configuração com um kit de desenvolvimento de software em que as funções não terão as informações em tempo de compilação dos plug-ins com antecedência que implementam essas interfaces.

Modelos

Se os modelos são uma possibilidade (temos as informações necessárias em tempo de compilação com antecedência, que não são perdidas no momento em que obtemos um objeto, por exemplo), podemos simplesmente fazer isso:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should have `position` and `parent` methods.
template <class Entity>
Vec2i abs_position(Entity& obj)
{
     const Vec2i xy = obj.xy();
     if (obj.parent())
     {
         // If the entity has a parent, return the sum of the parent 
         // position and the entity's local position.
         return xy + abs_position(obj.parent());
     }
     return xy;
}

... é claro que, nesse caso, o parentmétodo teria que retornar o mesmo Entitytipo; nesse caso, provavelmente queremos evitar interfaces diretas (já que muitas vezes eles querem perder informações de tipo em favor do trabalho com ponteiros de base).

Sistema Entidade-Componente

Se você começar a adotar a abordagem no estilo COM mais do ponto de vista da flexibilidade ou do desempenho, geralmente acabará com um sistema de componente de entidade semelhante ao que os mecanismos de jogo aplicam no setor. Nesse ponto, você estará completamente perpendicular a muitas abordagens orientadas a objetos, mas o ECS pode ser aplicável ao design da GUI (um lugar que eu contemplei usar o ECS fora de um foco orientado a cena, mas o considerei tarde demais depois). adotando uma abordagem no estilo COM para tentar lá).

Observe que esta solução no estilo COM está completamente disponível no que diz respeito aos designs dos kits de ferramentas da GUI, e o ECS seria ainda mais, portanto, não é algo que será apoiado por muitos recursos. No entanto, isso definitivamente permitirá que você atenue as tentações de projetar interfaces com responsabilidades sobrepostas a um mínimo absoluto, muitas vezes tornando-a uma preocupação.

Abordagem pragmática

A alternativa, é claro, é relaxar um pouco a guarda ou projetar interfaces em um nível granular e, em seguida, começar a herdá-las para criar interfaces mais grosseiras que você usa, como as IPositionPlusParentingderivadas de ambos IPositioneIParenting(espero que com um nome melhor que isso). Com interfaces puras, ele não deve violar o ISP tanto quanto as abordagens hierárquicas profundas monolíticas comumente aplicadas (Qt, MFC etc.), onde a documentação geralmente sente a necessidade de ocultar membros irrelevantes, devido ao nível excessivo de violação do ISP com esses tipos. projetos), portanto, uma abordagem pragmática pode simplesmente aceitar alguma sobreposição aqui e ali. No entanto, esse tipo de abordagem no estilo COM evita a necessidade de criar interfaces consolidadas para cada combinação que você usará. A preocupação com a "auto-suficiência" é completamente eliminada nesses casos, e isso frequentemente elimina a fonte definitiva de tentação para projetar interfaces com responsabilidades sobrepostas que desejam lutar com o SRP e o ISP.


fonte
11

Essa é uma decisão que você deve fazer caso a caso.

Antes de tudo, lembre-se de que os princípios do SOLID são exatamente isso ... princípios. Eles não são regras. Eles não são uma bala de prata. Eles são apenas princípios. Isso não tira a importância deles, você deve sempre se inclinar para segui-los. Porém, no segundo em que introduzirem um nível de dor, você deve abandoná-los até precisar deles.

Com isso em mente, pense por que você está separando suas interfaces em primeiro lugar. A idéia de uma interface é dizer "Se esse código consumidor exigir que um conjunto de métodos seja implementado na classe que está sendo consumida, preciso definir um contrato para a implementação: Se você me fornecer um objeto com essa interface, eu posso trabalhar com isso."

O objetivo do ISP é dizer "Se o contrato que eu exijo é apenas um subconjunto de uma interface existente, não devo impor a interface existente em nenhuma classe futura que possa ser passada para o meu método".

Considere o seguinte código:

public interface A
{
    void X();
    void Y();
}

public class Foo
{
     public void ConsumeX(A a)
     {
         a.X();
     }
}

Agora temos uma situação em que, se queremos passar um novo objeto para o ConsumeX, ele deve implementar X () e Y () para ajustar o contrato.

Então, devemos mudar o código, agora, para se parecer com o próximo exemplo?

public interface A
{
    void X();
    void Y();
}

public interface B
{
    void X();
}

public class Foo
{
     public void ConsumeX(B b)
     {
         b.X();
     }
}

O ISP sugere que devemos, então devemos nos inclinar para essa decisão. Mas, sem contexto, é difícil ter certeza. É provável que estendamos A e B? É provável que eles se estendam independentemente? É provável que B algum dia implemente métodos que A não exija? (Caso contrário, podemos fazer A derivar de B.)

Esta é a decisão que você deve fazer. E, se você realmente não tiver informações suficientes para fazer essa ligação, provavelmente deverá usar a opção mais simples, que pode muito bem ser o primeiro código.

Por quê? Porque é fácil mudar de idéia mais tarde. Quando você precisar dessa nova classe, basta criar uma nova interface e implementar ambas na sua antiga classe.

pdr
fonte
11
"Antes de tudo, lembre-se de que os princípios do SOLID são exatamente isso ... princípios. Eles não são regras. Eles não são uma bala de prata. Eles são apenas princípios. Isso não é para tirar a importância deles, você deve sempre se apoiar para segui-los. Mas no segundo em que introduzirem um nível de dor, você deve abandoná-los até precisar deles. ". Isso deve estar na primeira página de todo livro de padrões / princípios de design. Um deve aparecer também a cada 50 páginas como lembrete.
Christian Rodriguez