Interfaz separada para métodos de mutación.

11

He estado trabajando en la refactorización de algún código, y creo que podría haber dado el primer paso por la madriguera del conejo. Estoy escribiendo el ejemplo en Java, pero supongo que podría ser agnóstico.

Tengo una interfaz Foodefinida como

public interface Foo {

    int getX();

    int getY();

    int getZ();
}

Y una implementación como

public final class DefaultFoo implements Foo {

    public DefaultFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getZ() {
        return z;
    }

    private final int x;
    private final int y;
    private final int z;
}

También tengo una interfaz MutableFooque proporciona mutadores coincidentes

/**
 * This class extends Foo, because a 'write-only' instance should not
 * be possible and a bit counter-intuitive.
 */
public interface MutableFoo extends Foo {

    void setX(int newX);

    void setY(int newY);

    void setZ(int newZ);
}

Hay un par de implementaciones MutableFooque podrían existir (aún no las he implementado). Uno de ellos es

public final class DefaultMutableFoo implements MutableFoo {

    /**
     * A DefaultMutableFoo is not conceptually constructed 
     * without all values being set.
     */
    public DefaultMutableFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public void setX(int newX) {
        this.x = newX;
    }

    public int getY() {
        return y;
    }

    public void setY(int newY) {
        this.y = newY;
    }

    public int getZ() {
        return z;
    }

    public void setZ(int newZ) {
        this.z = newZ;
    }

    private int x;
    private int y;
    private int z;
}

La razón por la que los he dividido es porque es igualmente probable que se use cada uno. Es decir, es igualmente probable que alguien que use estas clases quiera una instancia inmutable, ya que querrá una mutable.

El caso de uso principal que tengo es una interfaz llamada StatSetque representa ciertos detalles de combate para un juego (puntos de vida, ataque, defensa). Sin embargo, las estadísticas "efectivas", o las estadísticas reales, son el resultado de las estadísticas base, que nunca se pueden cambiar, y las estadísticas entrenadas, que se pueden aumentar. Estos dos están relacionados por

/**
 * The EffectiveStats can never be modified independently of either the baseStats
 * or trained stats. As such, this StatSet must never provide mutators.
 */
public StatSet calculateEffectiveStats() {
    int effectiveHitpoints =
        baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    int effectiveAttack = 
        baseStats.getAttack() + (trainedStats.getAttack() / 4);
    int effectiveDefense = 
        baseStats.getDefense() + (trainedStats.getDefense() / 4);

    return StatSetFactory.createImmutableStatSet(effectiveHitpoints, effectiveAttack, effectiveDefense);
}

los Stats entrenados se incrementan después de cada batalla como

public void grantExperience() {
    int hitpointsReward = 0;
    int attackReward = 0;
    int defenseReward = 0;

    final StatSet enemyStats = enemy.getEffectiveStats();
    final StatSet currentStats = player.getEffectiveStats();
    if (enemyStats.getHitpoints() >= currentStats.getHitpoints()) {
        hitpointsReward++;
    }
    if (enemyStats.getAttack() >= currentStats.getAttack()) {
        attackReward++;
    }
    if (enemyStats.getDefense() >= currentStats.getDefense()) {
        defenseReward++;
    }

    final MutableStatSet trainedStats = player.getTrainedStats();
    trainedStats.increaseHitpoints(hitpointsReward);
    trainedStats.increaseAttack(attackReward);
    trainedStats.increaseDefense(defenseReward);
}

pero no aumentan justo después de la batalla. El uso de ciertos elementos, el empleo de ciertas tácticas, el uso inteligente del campo de batalla pueden otorgar una experiencia diferente.

Ahora para mis preguntas:

  1. ¿Existe un nombre para dividir las interfaces por accesores y mutadores en interfaces separadas?
  2. ¿Es dividirlos de esta manera el enfoque 'correcto' si es igualmente probable que se usen, o hay un patrón diferente y más aceptado que debería usar en su lugar (por ejemplo, Foo foo = FooFactory.createImmutableFoo();que podría regresar DefaultFooo DefaultMutableFooestá oculto porque createImmutableFooregresa Foo)?
  3. ¿Hay alguna desventaja previsible de inmediato al usar este patrón, salvo por complicar la jerarquía de la interfaz?

La razón por la que comencé a diseñarlo de esta manera es porque tengo la mentalidad de que todos los implementadores de una interfaz deben adherirse a la interfaz más simple posible y no proporcionar nada más. Al agregar setters a la interfaz, ahora las estadísticas efectivas se pueden modificar independientemente de sus partes.

Crear una nueva clase para el EffectiveStatSetno tiene mucho sentido ya que no estamos ampliando la funcionalidad de ninguna manera. Podríamos cambiar la implementación y hacer un EffectiveStatSetcompuesto de dos diferentes StatSets, pero creo que esa no es la solución correcta;

public class EffectiveStatSet implements StatSet {

    public EffectiveStatSet(StatSet baseStats, StatSet trainedStats) {
        // ...
    }

    public int getHitpoints() {
        return baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    }
}
Zymus
fuente
1
Creo que el problema es que su objeto es mutable incluso si está accediendo a él, su interfaz 'inmutable'. Es mejor tener un objeto mutable con Player.TrainStats ()
Ewan
@gnat, ¿tiene alguna referencia sobre dónde puedo obtener más información al respecto? Según la pregunta editada, no estoy seguro de cómo o dónde podría aplicar eso.
Zymus
@gnat: sus enlaces (y sus enlaces de segundo y tercer grado) son muy útiles, pero las frases de sabiduría pegadizas son de poca ayuda. Solo atrae malentendidos y desprecio.
rwong

Respuestas:

6

Para mí parece que tienes una solución buscando un problema.

¿Existe un nombre para dividir las interfaces por accesores y mutadores en interfaces separadas?

Esto puede ser un poco provocador, pero en realidad lo llamaría "sobrediseñar cosas" o "sobrecomplicar cosas". Al ofrecer una variante mutable e inmutable de la misma clase, ofrece dos soluciones funcionalmente equivalentes para el mismo problema, que difieren solo en aspectos no funcionales como el comportamiento del rendimiento, la API y la seguridad contra los efectos secundarios. Supongo que es porque temes tomar una decisión sobre cuál preferir o porque intentas implementar la característica "constante" de C ++ en C #. Supongo que en el 99% de todos los casos no habrá una gran diferencia si el usuario elige la variante mutable o inmutable, puede resolver sus problemas con uno u otro. Por lo tanto, "la probabilidad de que se use una clase"

La excepción es cuando diseña un nuevo lenguaje de programación o un marco multipropósito que será utilizado por unos diez miles de programadores o más. Entonces, puede escalar mejor cuando ofrece variantes inmutables y mutables de tipos de datos de propósito general. Pero esta es una situación en la que tendrá miles de escenarios de uso diferentes, lo que probablemente no sea el problema que enfrenta, supongo.

Es dividirlos de esta manera el enfoque 'correcto' si es igualmente probable que se usen o si hay un patrón diferente y más aceptado

El "patrón más aceptado" se llama KISS: manténgalo simple y estúpido. Tome una decisión a favor o en contra de la mutabilidad para la clase / interfaz específica en su biblioteca. Por ejemplo, si su "StatSet" tiene una docena de atributos o más, y en su mayoría se cambian individualmente, preferiría la variante mutable y simplemente no modificar las estadísticas base donde no deberían modificarse. Para algo así como una Fooclase con atributos X, Y, Z (un vector tridimensional), probablemente preferiría la variante inmutable.

¿Hay alguna desventaja previsible de inmediato al usar este patrón, salvo por complicar la jerarquía de la interfaz?

Los diseños demasiado complicados hacen que el software sea más difícil de probar, más difícil de mantener y más difícil de evolucionar.

Doc Brown
fuente
1
Creo que te refieres a "KISS - Keep It Simple, Stupid"
Epicblood
2

¿Existe un nombre para dividir las interfaces por accesores y mutadores en interfaces separadas?

Podría haber un nombre para esto si esta separación es útil y proporciona un beneficio que no veo . Si las separaciones no proporcionan un beneficio, las otras dos preguntas no tienen sentido.

¿Puede decirnos algún caso de uso comercial en el que las dos interfaces separadas nos brinden beneficios o esta pregunta es un problema académico (YAGNI)?

Puedo pensar en comprar con un carrito mutable (puede poner más artículos) que puede convertirse en un pedido donde los artículos ya no pueden ser modificados por el cliente. El estado del pedido sigue siendo mutable.

Las implementaciones que he visto no separan las interfaces

  • no hay interfaz ReadOnlyList en java

La versión ReadOnly utiliza el "WritableInterface" y genera una excepción si se utiliza un método de escritura

k3b
fuente
He modificado el OP para explicar el caso de uso principal.
Zymus
2
"no hay interfaz ReadOnlyList en java" - francamente, debería haberla. Si tiene un código que acepta una Lista <T> como parámetro, no puede decir fácilmente si funcionará con una lista no modificable. Código que devuelve listas, no hay forma de saber si puede modificarlas de forma segura. La única opción es confiar en documentación potencialmente incompleta o inexacta, o hacer una copia defensiva por si acaso. Un tipo de colección de solo lectura haría esto mucho más simple.
Jules
@Jules: de hecho, la forma de Java parece ser todo lo anterior: para asegurarse de que la documentación sea completa y precisa, así como hacer una copia defensiva por si acaso. Seguramente escala a proyectos empresariales muy grandes.
rwong
1

La separación de una interfaz de recopilación mutable y una interfaz de recopilación de contrato de solo lectura es un ejemplo del principio de segregación de interfaz. No creo que haya nombres especiales para cada aplicación de principio.

Observe algunas palabras aquí: "contrato de solo lectura" y "colección".

Un contrato de solo lectura significa que la clase Ale da a la clase Bun acceso de solo lectura, pero no implica que el objeto de colección subyacente sea realmente inmutable. Inmutable significa que nunca cambiará en el futuro, sin importar por ningún agente. Un contrato de solo lectura solo dice que el destinatario no puede cambiarlo; alguien más (en particular, la clase A) puede cambiarlo.

Para hacer que un objeto sea inmutable, debe ser realmente inmutable: debe negar los intentos de cambiar sus datos independientemente del agente que lo solicite.

Lo más probable es que el patrón se observe en objetos que representan una colección de datos: listas, secuencias, archivos (secuencias), etc.


La palabra "inmutable" se convierte en moda, pero el concepto no es nuevo. Y hay muchas formas de usar la inmutabilidad, así como muchas formas de lograr un mejor diseño usando otra cosa (es decir, sus competidores).


Aquí está mi enfoque (no basado en la inmutabilidad).

  1. Defina un DTO (objeto de transferencia de datos), también conocido como tupla de valor.
    • Este DTO contendrá los tres campos: hitpoints, attack, defense.
    • Solo campos: público, cualquiera puede escribir, sin protección.
    • Sin embargo, un DTO debe ser un objeto de usar y tirar: si la clase Anecesita pasar un DTO a la clase B, hace una copia y pasa la copia en su lugar. Por lo tanto, Bpuede usar el DTO como quiera (escribiéndole), sin afectar el DTO que se Amantiene.
  2. La grantExperiencefunción se divide en dos:
    • calculateNewStats
    • increaseStats
  3. calculateNewStats tomará las entradas de dos DTO, uno que representa las estadísticas del jugador y otro que representa las estadísticas del enemigo, y realizará los cálculos.
    • Para la entrada, la persona que llama debe elegir entre las estadísticas básicas, entrenadas o efectivas, de acuerdo con sus necesidades.
    • El resultado será un nuevo DTO donde cada campo ( hitpoints, attack, defense) almacena la cantidad a ser incrementado para esa capacidad.
    • El nuevo DTO de "cantidad para incrementar" no se refiere al límite superior (límite máximo impuesto) para esos valores.
  4. increaseStats es un método en el jugador (no en el DTO) que toma una "cantidad para incrementar" DTO, y aplica ese incremento en el DTO que es propiedad del jugador y representa el DTO entrenable del jugador.
    • Si hay valores máximos aplicables para estas estadísticas, se aplican aquí.

En caso de calculateNewStatsque no dependa de ningún otro jugador o información enemiga (más allá de los valores en los dos DTO de entrada), este método puede ubicarse potencialmente en cualquier parte del proyecto.

Si calculateNewStatsse determina que el sistema tiene una dependencia total del jugador y los objetos enemigos (es decir, los futuros jugadores y objetos enemigos pueden tener nuevas propiedades y calculateNewStatsdeben actualizarse para consumir la mayor cantidad posible de sus nuevas propiedades), entonces calculateNewStatsdeben aceptar esos dos objetos, no simplemente el DTO. Sin embargo, su resultado de cálculo seguirá siendo el DTO incremental, o cualquier objeto simple de transferencia de datos que lleve la información que se utiliza para realizar un incremento / actualización.

rwong
fuente
1

Aquí hay un gran problema: el hecho de que una estructura de datos sea inmutable no significa que no necesitemos versiones modificadas . La versión real inmutable de una estructura de datos proporciona setX, setYy setZmétodos: simplemente devuelven una nueva estructura en lugar de modificar el objeto al que los llamó.

// Mutates mutableFoo
mutableFoo.setX(...)
// Creates a new updated immutableFoo, existing immutableFoo is unchanged
newFoo = immutableFoo.setX(...)

Entonces, ¿cómo le da a una parte de su sistema la capacidad de cambiarlo mientras restringe otras partes? Con un objeto mutable que contiene una referencia a una instancia de la estructura inmutable. Básicamente, en lugar de que tu clase de jugador pueda mutar su objeto de estadísticas mientras le da a cada clase una vista inmutable de sus estadísticas, sus estadísticas son inmutables y el jugador es mutable. En vez de:

// Stats are mutable, mutates self.stats
self.stats.setX(...)

Tendrías:

// Stats are immutable, mutates self, setX() returns new updated stats
self.stats = self.stats.setX(...)

¿Ver la diferencia? El objeto de estadísticas es completamente inmutable, pero las estadísticas actuales del jugador son una referencia mutable al objeto inmutable. No es necesario hacer dos interfaces: simplemente haga que la estructura de datos sea completamente inmutable y administre una referencia mutable a ella donde la esté utilizando para almacenar el estado.

Esto significa que otros objetos en su sistema no pueden confiar en una referencia al objeto de estadísticas: el objeto de estadísticas que tienen no será el mismo objeto de estadísticas que tiene el jugador después de que el jugador actualice sus estadísticas.

Esto tiene más sentido de todos modos, ya que conceptualmente no son realmente las estadísticas las que están cambiando, son las estadísticas actuales del jugador . No es el objeto de estadísticas. La referencia al objeto de estadísticas que tiene el objeto jugador. Por lo tanto, otras partes de su sistema, dependiendo de esa referencia, deberían estar haciendo referencia explícita player.currentStats()o algo así en lugar de obtener el objeto de estadísticas subyacente del jugador, almacenarlo en algún lugar y confiar en que se actualice a través de mutaciones.

Jack
fuente
1

Wow ... esto realmente me lleva de vuelta. Intenté esta misma idea varias veces. Fui demasiado terco para renunciar porque pensé que habría algo bueno que aprender. He visto a otros probar esto también en código. La mayoría de las implementaciones que he visto llamada de sólo lectura interfaces como la suya FooViewero ReadOnlyFooy la escritura única interfaz del FooEditoro WriteableFoo, o en Java Creo que vi FooMutatoruna vez. No estoy seguro de que haya un vocabulario oficial o incluso común para hacer las cosas de esta manera.

Esto nunca hizo nada útil para mí en los lugares donde lo probé. Lo evitaría por completo. Haría lo que otros sugieren y daré un paso atrás y consideraré si realmente necesitas esta noción en tu idea más amplia. No estoy seguro de que haya una manera correcta de hacer esto, ya que nunca guardé nada del código que produje mientras lo intentaba. Cada vez que me retiraba después de un esfuerzo considerable y cosas simplificadas. Y con eso quiero decir algo similar a lo que otros dijeron sobre YAGNI, KISS y DRY.

Un posible inconveniente: la repetición. Tendrá que crear estas interfaces para muchas clases potencialmente e incluso para una sola clase debe nombrar cada método y describir cada firma al menos dos veces. De todas las cosas que me queman en la codificación, tener que cambiar varios archivos de esta manera me causa la mayor cantidad de problemas. Finalmente termino olvidando hacer los cambios en un lugar u otro. Una vez que tenga una clase llamada Foo e interfaces llamadas FooViewer y FooEditor, si decide que sería mejor si lo llamara Bar, debe refactorizar-> cambiar el nombre tres veces a menos que tenga un IDE realmente increíblemente inteligente. Incluso encontré que este era el caso cuando tenía una cantidad considerable del código proveniente de un generador de código.

Personalmente, no me gusta crear interfaces para cosas que también tienen una sola implementación. Significa que cuando viajo por el código no puedo saltar directamente a la única implementación directamente, tengo que saltar a la interfaz y luego a la implementación de la interfaz o al menos presionar algunas teclas adicionales para llegar allí. Esas cosas se suman.

Y luego está la complicación que mencionaste. Dudo que vuelva a seguir este camino en particular con mi código nuevamente. Incluso para el lugar que mejor encaja en mi plan general para el código (un ORM que construí), saqué esto y lo reemplacé con algo más fácil de codificar.

Realmente pensaría más en la composición que mencionaste. Tengo curiosidad por qué sientes que sería una mala idea. Lo que habría esperado a tener EffectiveStatscomponer e inmutable Statsy algo parecido a StatModifierslo que componer más un conjunto de StatModifiers que representan lo que pudiera modificar las estadísticas (efectos temporales, artículos, ubicación en un área de mejora, fatiga), sino que su EffectiveStatsno tendría que entender, porque StatModifiersWould administrar cuáles eran esas cosas y cuánto efecto y qué tipo tendrían en qué estadísticas. losStatModifiersería una interfaz para las diferentes cosas y podría saber cosas como "estoy en la zona", "cuándo desaparece el medicamento", etc., pero ni siquiera tendría que permitir que ningún otro objeto supiera tales cosas. Solo tendría que decir qué estadística y cómo se modificó en este momento. Mejor aún, StatModifierpodría simplemente exponer un método para producir un nuevo inmutable Statsbasado en otro Statsque sería diferente porque se había alterado adecuadamente. Entonces podrías hacer algo como currentStats = statModifier2.modify(statModifier1.modify(baseStats))y todo Statspuede ser inmutable. Ni siquiera codificaría eso directamente, probablemente recorrería todos los modificadores y aplicaría cada uno al resultado de los modificadores anteriores que comienzan con baseStats.

Jason Kell
fuente