it-swarm-es.com

Funciones que simplemente llaman a otra función, ¿mala elección de diseño?

Tengo una configuración de una clase que representa un edificio. Este edificio tiene un plano de planta, que tiene límites.

La forma en que lo configuro es así:

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;
}

Estos objetos tienen sus propias razones únicas para existir, ya que hacen cosas diferentes. Sin embargo, hay una situación en la que quiero obtener un punto local del edificio.

En esta situación, esencialmente estoy haciendo:

Building.GetLocalPoint(worldPoint);

Esto entonces tiene:

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

Lo que lleva a esta función en mi objeto Floor:

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

Y luego, por supuesto, el objeto de límites realmente hace los cálculos necesarios.

Como puede ver, estas funciones son bastante redundantes, ya que simplemente pasan a otra función más abajo. Esto no me parece inteligente: huele a código malo que me va a morder en el trasero con desorden de código.

Alternativamente, escribo mi código como a continuación, pero tengo que exponer más al público, algo que no quiero hacer:

building.floor.bounds.GetLocalPoint(worldPoint);

Esto también comienza a ser un poco tonto cuando vas a muchos objetos anidados y conduce a grandes agujeros de conejo para obtener tu función dada y puedes terminar olvidando dónde está, lo que también huele a mal diseño de código.

¿Cuál es la forma correcta de diseñar todo esto?

53
WDUK

Nunca olvides la Ley de Demeter :

La Ley del Demeter (LoD) o principio de mínimo conocimiento es una guía de diseño para desarrollar software, particularmente programas orientados a objetos. En su forma general, el LoD es un caso específico de acoplamiento flojo. La directriz fue propuesta por Ian Holland en la Northeastern University a fines de 1987, y puede resumirse sucintamente en cada una de las siguientes formas: [1]

  • Cada unidad debe tener un conocimiento limitado sobre otras unidades: solo unidades "estrechamente" relacionadas con la unidad actual.
  • Cada unidad solo debe hablar con sus amigos; No hables con extraños.
  • Solo habla con tus amigos inmediatos .

La noción fundamental es que un objeto dado debe asumir lo menos posible sobre la estructura o propiedades de cualquier otra cosa ( incluyendo sus subcomponentes) , de acuerdo con el principio de "ocultación de información".
Puede ser visto como un corolario del principio de menor privilegio, que dicta que un módulo posee solo la información y los recursos necesarios para su propósito legítimo.


building.floor.bounds.GetLocalPoint(worldPoint);

Este código viola la LOD. Su consumidor actual de alguna manera debe saber:

  • Que el edificio tiene un floor
  • Que el piso tiene bounds
  • Que los límites tienen un método GetLocalPoint

Pero en realidad, su consumidor solo debe manejar el building, no cualquier cosa dentro del edificio (no debe manejar los subcomponentes directamente).

Si alguna de estas clases subyacentes cambia estructuralmente, de repente también debe cambiar este consumidor, a pesar de que puede estar varios niveles por encima de la clase que usted En realidad cambió.
Esto comienza a infringir la separación de las capas que tiene, ya que un cambio afecta a varias capas (más que solo a sus vecinos directos).

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

Supongamos que introduce un segundo tipo de edificio, uno sin piso. No puedo pensar en un ejemplo del mundo real, pero estoy tratando de mostrar un caso de uso generalizado, así que supongamos que EtherealBuilding es un caso así.

Porque tienes el building.GetLocalPoint método, puede cambiar su funcionamiento sin que el consumidor de su edificio lo sepa, por ejemplo:

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

Lo que hace que esto sea más difícil de entender es que no hay un caso de uso claro para un edificio sin piso. No conozco tu dominio y no puedo juzgar si/cómo ocurriría eso.

Pero las pautas de desarrollo son enfoques generalizados que renuncian a aplicaciones contextuales específicas. Si cambiamos el contexto, el ejemplo se vuelve más 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();

Lo que demuestra el punto por el cual el método en la clase Player (o cualquiera de sus clases derivadas) tiene un propósito, incluso si su implementación actual es trivial .


Sidenote
En aras de un ejemplo, he eludido algunas discusiones tangenciales, como la forma de abordar la herencia. Estos no son el foco de la respuesta.

110
Flater

Si tiene tales métodos ocasionalmente aquí y allá, puede ser solo un efecto secundario (o precio a pagar, si lo desea) de un diseño consistente.

Si tiene mucho de ellos, entonces lo consideraría una señal de que este diseño es problemático.

En su ejemplo, tal vez no debería haber una forma de "obtener un punto local para el edificio" desde el exterior del edificio y en su lugar los métodos del edificio deberían estar en un nivel más alto de abstracción y trabajar con tales apunta solo internamente.

21

La famosa "Ley de Demeter" es una ley que dicta qué tipo de código escribir, pero no explica nada útil. La respuesta de Flater está bien porque da ejemplos, pero no los llamaría "violación/cumplimiento de la ley de Demeter". Si se aplica la "Ley de Deméter" donde se encuentra, póngase en contacto con su comisaría de policía de Deméter local, estarán encantados de resolver los problemas con usted.

Recuerde que siempre es dueño del código que escribe y, por lo tanto, entre crear "delegar funciones" y no escribirlas, es una cuestión de su propio juicio. No hay una línea nítida, por lo que no se puede definir una regla definida. Por el contrario, podemos encontrar casos, como lo hizo Flater, en los que crear tales funciones son simplemente inútiles, y en donde crear tales funciones son útiles. ( Spoiler: En el primer caso, la solución es alinear la función. En el último, la solución es crear la función)

Los ejemplos en los que es inútil definir una función de delegación incluyen cuándo la única razón sería:

  • Para acceder a un miembro de un objeto devuelto por un miembro, cuando el miembro no es un detalle de implementación que debe encapsularse.
  • Su miembro de interfaz está implementado correctamente por .NET's cuasi-implementación
  • Para cumplir con Demeter

Los ejemplos en los que es útil crear una función de delegación incluyen:

  • Factorizando una cadena de llamadas que se repite una y otra vez
  • Cuando el idioma te obliga, p. implementar un miembro de la interfaz delegando a otro miembro o simplemente llamando a otra función
  • Cuando la función que llama no está en el mismo nivel conceptual que las otras llamadas en el mismo nivel (por ejemplo, una llamada LoadAssembly en el mismo nivel que la introspección del complemento)
1
Laurent LA RIZZA

Olvídate de conocer la implementación de Building por un momento. Alguien más lo ha escrito. Tal vez un proveedor que solo le da código compilado. O algún contratista que realmente comienza a escribirlo la próxima semana.

Todo lo que sabe es la interfaz de Building y las llamadas que realiza a esa interfaz. Todos parecen bastante razonables, así que estás bien.

Ahora te pones un abrigo diferente y de repente eres el implementador de Building. No conoce la implementación de Floor, solo conoce la interfaz. Utiliza la interfaz Floor para implementar tu clase Building. Conoces la interfaz de Floor y las llamadas que haces a esa interfaz para implementar tu clase de Building, y todas parecen bastante razonables, por lo que estás bien nuevamente.

En general, no hay problema. Todo esta bien.

1
gnasher729

building.floor.bounds.GetLocalPoint (worldPoint);

es malo.

Los objetos solo deben tratar con sus vecinos inmediatos porque su sistema será MUY difícil de cambiar de otra manera.

0
kiwicomb123

Está bien solo llamar a funciones. Hay muchos patrones de diseño que utilizan esa técnica, por ejemplo, adaptador y fachada, pero también algunos patrones como decorador, proxy y muchos más.

Se trata de niveles de abstracciones. No debes mezclar conceptos de diferentes niveles de abstracciones. Para hacerlo, a veces necesita llamar a objetos internos para que su cliente no se vea obligado a hacerlo él mismo.

Por ejemplo (el ejemplo del automóvil será más simple):

Tienes objetos Driver, Car y Wheel. En el mundo real, para conducir un automóvil, ¿tiene un conductor que hace algo directamente con ruedas o solo interactúa con el automóvil en general?

Cómo saber que algo NO está bien:

  • La encapsulación está rota, los objetos internos están disponibles en la API pública. (por ejemplo, código como car.Wheel.Move ()).
  • El principio SRP está roto, los objetos están haciendo muchas cosas diferentes (por ejemplo, preparando el texto del mensaje de correo electrónico y realmente enviándolo en el mismo objeto).
  • Es difícil probar la clase en particular (por ejemplo, hay muchas dependencias).
  • Hay diferentes expertos en dominios (o departamentos de la empresa) que manejan cosas que usted maneja en la misma clase (por ejemplo, ventas y entrega de paquetes).

Posibles problemas al romper la Ley de Demeter:

  • Prueba de unidad dura.
  • Dependencia de la estructura interna de otros objetos.
  • Alto acoplamiento entre objetos.
  • Exponer datos internos.
0
0lukasz0