ti-enxame.com

Funções que simplesmente chamam outra função, má escolha de design?

Eu tenho uma configuração de uma classe que representa um edifício. Este edifício tem uma planta baixa, que tem limites.

A maneira como eu o configurei é assim:

public struct Bounds {} // AABB bounding box stuff

//Floor contains bounds and mesh data to update textures etc
//internal since only building should have direct access to it no one else
internal class Floor {  
    private Bounds bounds; // private only floor has access to
}

//a building that has a floor (among other stats)
public class Building{ // the object that has a floor
    Floor floor;
}

Esses objetos têm suas próprias razões únicas para existir, pois fazem coisas diferentes. No entanto, há uma situação em que quero obter um ponto local para o edifício.

Nesta situação, eu estou essencialmente fazendo:

Building.GetLocalPoint(worldPoint);

Isso então tem:

public Vector3 GetLocalPoint(Vector3 worldPoint){    
    return floor.GetLocalPoint(worldPoint);
}

O que leva a essa função no meu objeto Floor:

internal Vector3 GetLocalPoint(Vector3 worldPoint){
    return bounds.GetLocalPoint(worldPoint);
}

E é claro que o objeto de limites realmente faz as contas necessárias.

Como você pode ver, essas funções são bastante redundantes, pois passam para outra função mais abaixo. Isso não parece inteligente para mim - cheira a código ruim que vai me morder na bunda em algum lugar abaixo da linha com bagunça de código.

Como alternativa, escrevo meu código como abaixo, mas tenho que expor mais ao público, o que meio que não quero fazer:

building.floor.bounds.GetLocalPoint(worldPoint);

Isso também começa a ficar um pouco bobo quando você vai a muitos objetos aninhados e leva a grandes tocas de coelho para obter sua função e você pode acabar esquecendo onde está - o que também cheira a mau design de código.

Qual é a maneira correta de projetar tudo isso?

53
WDUK

Nunca esqueça a Lei de Demeter :

A lei de Deméter (LoD) ou princípio do mínimo conhecimento é uma diretriz de design para o desenvolvimento de software, particularmente programas orientados a objetos. Em sua forma geral, o LoD é um caso específico de acoplamento solto. A diretriz foi proposta por Ian Holland na Northeastern University no final de 1987 e pode ser resumida de forma sucinta de uma das seguintes maneiras: [1]

  • Cada unidade deve ter apenas conhecimento limitado sobre outras unidades: somente unidades "estreitamente" relacionadas à unidade atual.
  • Cada unidade deve conversar apenas com seus amigos; não fale com estranhos.
  • Fale apenas com seus amigos imediatos .

A noção fundamental é que um determinado objeto deve assumir o mínimo possível sobre a estrutura ou propriedades de qualquer outra coisa ( incluindo seus subcomponentes) , de acordo com o princípio de "ocultação de informações".
Pode ser visto como um corolário do princípio do menor privilégio, que determina que um módulo possua apenas as informações e os recursos necessários para seu propósito legítimo.


building.floor.bounds.GetLocalPoint(worldPoint);

Este código viola o LOD. De alguma forma, seu consumidor atual precisa saber:

  • Que o edifício tem um floor
  • Que o piso tenha bounds
  • Que os limites têm um método GetLocalPoint

Mas, na realidade, seu consumidor deve apenas manipular o building, não qualquer coisa dentro do edifício (não deve manipular diretamente os subcomponentes).

Se qualquer dessas classes subjacentes mudar estruturalmente, de repente você também precisará alterar esse consumidor, mesmo que ele esteja a vários níveis da classe que você realmente mudou.
Isso começa a violar a separação de camadas que você possui, pois uma mudança afeta várias camadas (mais do que apenas seus vizinhos diretos).

public Vector3 GetLocalPoint(Vector3 worldPoint){    
    return floor.GetLocalPoint(worldPoint);
}

Suponha que você introduza um segundo tipo de edifício, um sem piso. Não consigo pensar em um exemplo do mundo real, mas estou tentando mostrar um caso de uso generalizado, então vamos supor que EtherealBuilding seja esse caso.

Porque você tem o building.GetLocalPoint método, você pode alterar seu funcionamento sem que o consumidor do seu prédio esteja ciente disso, por exemplo:

public class EtherealBuilding : Building {
    public Vector3 GetLocalPoint(Vector3 worldPoint){    
        return universe.CenterPoint; // Just a random example
    }
}

O que torna isso mais difícil de entender é que não há um caso de uso claro para um edifício sem piso. Não conheço o seu domínio e não posso julgar se/como isso ocorreria.

Mas as diretrizes de desenvolvimento são abordagens generalizadas que dispensam aplicações contextuais específicas. Se mudarmos o contexto, o exemplo fica mais claro:

// Violating LOD

bool isAlive = player.heart.IsBeating();

// But what if the player is a robot?

public class HumanPlayer : Player {
    public bool IsAlive() {
        return this.heart.IsBeating();
    }
}

public class RobotPlayer : Player {
    public bool IsAlive() {
        return this.IsSwitchedOn();
    }
}

// This code works for both human and robot players, and thus wouldn't need to be changed when new (sub)types of players are developed.

bool isAlive = player.IsAlive();

O que prova o motivo pelo qual o método na classe Player (ou em qualquer uma de suas classes derivadas) tem um objetivo, mesmo que sua implementação atual seja trivial .


Sidenote
Por uma questão de exemplo, evitei algumas discussões tangenciais, como abordar a herança. Este não é o foco da resposta.

110
Flater

Se você tiver esses métodos ocasionalmente aqui e ali, pode ser apenas um efeito colateral (ou preço a pagar, se desejar) de um design consistente.

Se você tiver muito deles, consideraria um sinal de que esse design é problemático.

No seu exemplo, talvez não deva existir uma maneira de "obter um ponto local para o edifício" de fora do edifício e, em vez disso, os métodos do edifício devem estar em um nível mais alto de abstração e trabalhar com tais pontos apenas internamente.

21
Michael Borgwardt

A famosa "Lei de Demeter" é uma lei que determina que tipo de código escrever, mas não explica nada de útil. A resposta de Flater é boa porque fornece exemplos, mas eu não chamaria isso de "violação/cumprimento da lei de Demeter". Se a "Lei de Demeter" for aplicada onde você estiver, entre em contato com a Delegacia de Polícia local de Demeter, eles terão prazer em resolver os problemas com você.

Lembre-se de que você sempre domina o código que escreve e, portanto, entre criar "funções de delegação" e não escrevê-las, é uma questão de seu próprio julgamento. Não existe uma linha nítida, portanto, nenhuma regra precisa pode ser definida. Pelo contrário, podemos encontrar casos, como Flater, em que a criação de tais funções é totalmente inútil e onde a criação de tais funções é útil. ( Spoiler: No primeiro caso, a correção é incorporar a função. No último, a correção é criar a função)

Exemplos em que é inútil definir uma função de delegação incluem quando o único motivo seria:

  • Para acessar um membro de um objeto retornado por um membro, quando o membro não é um detalhe de implementação que deve ser encapsulado.
  • Seu membro da interface foi implementado corretamente pelo .NET quase-implementação
  • Para ser compatível com Demeter

Exemplos onde é útil criar uma função de delegação incluem:

  • Factoring uma cadeia de chamadas que é repetida várias vezes
  • Quando o idioma obriga a, por exemplo implementar um membro da interface delegando para outro membro ou simplesmente chamando outra função
  • Quando a função que você chama não está no mesmo nível conceitual que as outras chamadas no mesmo nível (por exemplo, uma chamada LoadAssembly no mesmo nível que a introspecção do plug-in)
1
Laurent LA RIZZA

Esqueça que você conhece a implementação do Building por um momento. Alguém escreveu isso. Talvez um fornecedor que apenas fornece código compilado. Ou algum empreiteiro que realmente começa a escrevê-lo na próxima semana.

Tudo o que você conhece é a interface do Building e as chamadas feitas para essa interface. Todos eles parecem bastante razoáveis, então você está bem.

Agora você veste um casaco diferente e de repente você é o implementador do edifício. Você não conhece a implementação do Floor, apenas conhece a interface. Você usa a interface Floor para implementar sua classe Building. Você conhece a interface do Floor e as chamadas feitas para essa interface para implementar sua classe Building, e todas elas parecem bastante razoáveis, então você está bem novamente.

Em suma, não há problema. Tudo está bem.

1
gnasher729

building.floor.bounds.GetLocalPoint (worldPoint);

é ruim.

Os objetos devem lidar apenas com seus vizinhos imediatos, pois o seu sistema será MUITO difícil de mudar.

0
kiwicomb123

Não há problema em apenas chamar funções. Existem muitos padrões de design que estão usando essa técnica, por exemplo, adaptador e fachada, mas também, em certa medida, padrões como decorador, proxy e muitos outros.

É tudo sobre níveis de abstrações. Você não deve misturar conceitos de diferentes níveis de abstração. Para fazer isso, às vezes é necessário chamar objetos internos para que seu cliente não seja forçado a fazer isso sozinho.

Por exemplo (o exemplo do carro será mais simples):

Você tem objetos de motorista, carro e roda. No mundo real, para dirigir um carro, você tem um motorista fazendo algo diretamente com rodas ou ele só interage com o carro como um todo?

Como saber que algo NÃO está ok:

  • O encapsulamento está quebrado, objetos internos estão disponíveis na API pública. (por exemplo, código como car.Wheel.Move ()).
  • O princípio do SRP está quebrado, os objetos estão fazendo muitas coisas diferentes (por exemplo, preparando o texto da mensagem de email e enviando-o no mesmo objeto).
  • É difícil testar a unidade de uma determinada classe (por exemplo, existem muitas dependências).
  • Existem diferentes especialistas em domínio (ou departamentos da empresa) que lidam com coisas que você lida com a mesma classe (por exemplo, vendas e entrega de pacotes).

Problemas potenciais ao violar a Lei de Demeter:

  • Teste de unidade rígido.
  • Dependência da estrutura interna de outros objetos.
  • Alto acoplamento entre objetos.
  • Expondo dados internos.
0
0lukasz0