Interface separada para métodos de mutação

11

Eu tenho trabalhado na refatoração de algum código e acho que posso ter dado o primeiro passo na toca do coelho. Estou escrevendo o exemplo em Java, mas suponho que possa ser agnóstico.

Eu tenho uma interface Foodefinida como

public interface Foo {

    int getX();

    int getY();

    int getZ();
}

E uma implementação 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;
}

Eu também tenho uma interface MutableFooque fornece mutadores correspondentes

/**
 * 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);
}

Existem algumas implementações MutableFooque podem existir (ainda não as implementei). Uma delas é

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

A razão pela qual eu os dividi é porque é igualmente provável que cada um seja usado. Ou seja, é igualmente provável que alguém que use essas classes deseje uma instância imutável, como é que eles desejem uma instância mutável.

O principal caso de uso que tenho é uma interface chamada StatSetque representa certos detalhes de combate de um jogo (pontos de vida, ataque, defesa). No entanto, as estatísticas "efetivas", ou as estatísticas reais, são resultado das estatísticas básicas, que nunca podem ser alteradas, e das estatísticas treinadas, que podem ser aumentadas. Esses dois estão 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);
}

as estatísticas treinadas são aumentadas após cada batalha 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);
}

mas eles não aumentam logo após a batalha. Usando certos itens, empregando certas táticas, o uso inteligente do campo de batalha pode garantir uma experiência diferente.

Agora, para minhas perguntas:

  1. Existe um nome para dividir interfaces por acessadores e mutadores em interfaces separadas?
  2. Dividi-los dessa maneira é a abordagem 'correta', se é igualmente provável que sejam usados, ou existe um padrão diferente e mais aceito que eu deveria usar (por exemplo, o Foo foo = FooFactory.createImmutableFoo();que poderia retornar DefaultFooou que DefaultMutableFooestá oculto porque createImmutableFooretorna Foo)?
  3. Existem desvantagens imediatamente previsíveis no uso desse padrão, exceto por complicar a hierarquia da interface?

A razão pela qual comecei a projetá-lo dessa maneira é porque sou da opinião de que todos os implementadores de uma interface devem aderir à interface mais simples possível e não fornecem mais nada. Adicionando setters à interface, agora as estatísticas efetivas podem ser modificadas independentemente de suas partes.

Criar uma nova classe para o EffectiveStatSetnão faz muito sentido, pois não estamos estendendo a funcionalidade de forma alguma. Poderíamos mudar a implementação e formar EffectiveStatSetum composto de dois diferentes StatSets, mas acho que essa não é a solução certa;

public class EffectiveStatSet implements StatSet {

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

    public int getHitpoints() {
        return baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    }
}
Zymus
fonte
1
Acho que o problema é que seu objeto é mutável, mesmo que você esteja acessando minha interface 'imutável'. É melhor ter um objeto mutável com Player.TrainStats ()
Ewan
@gnat você tem alguma referência de onde eu possa aprender mais sobre isso? Com base na pergunta editada, não tenho certeza de como ou onde eu poderia aplicar isso.
Zymus
@gnat: seus links (e seus links de segundo e terceiro graus) são muito úteis, mas frases cativantes de sabedoria são de pouca ajuda. Atrai apenas mal-entendidos e desprezo.
Rwong 02/08/2015

Respostas:

6

Para mim, parece que você tem uma solução procurando um problema.

Existe um nome para dividir interfaces por acessadores e mutadores em interfaces separadas?

Isso pode ser um pouco provocador, mas na verdade eu chamaria isso de "projetar coisas demais" ou "complicar demais". Ao oferecer uma variante mutável e imutável da mesma classe, você oferece duas soluções funcionalmente equivalentes para o mesmo problema, que diferem apenas em aspectos não funcionais, como comportamento de desempenho, API e segurança contra efeitos colaterais. Eu acho que é porque você tem medo de decidir qual preferir ou porque tenta implementar o recurso "const" do C ++ em C #. Eu acho que em 99% de todos os casos não fará muita diferença se o usuário escolher a variante mutável ou imutável, ele pode resolver seus problemas com um ou outro. Assim, "probabilidade de uma classe ser usada"

A exceção é quando você cria uma nova linguagem de programação ou uma estrutura de múltiplos propósitos que será usada por cerca de dez milhares de programadores ou mais. Na verdade, ele pode ser melhor dimensionado quando você oferece variantes imutáveis ​​e mutáveis ​​de tipos de dados de uso geral. Mas esta é uma situação em que você terá milhares de cenários de uso diferentes - o que provavelmente não é o seu problema, eu acho?

Está dividindo-os dessa maneira, a abordagem "correta", se for igualmente provável que sejam usados ​​ou se houver um padrão diferente e mais aceito

O "padrão mais aceito" é chamado KISS - mantenha-o simples e estúpido. Tome uma decisão a favor ou contra a mutabilidade para a classe / interface específica em sua biblioteca. Por exemplo, se o seu "StatSet" tiver uma dúzia de atributos ou mais, e eles forem alterados principalmente individualmente, eu preferiria a variante mutável e simplesmente não modificaria as estatísticas básicas onde elas não deveriam ser modificadas. Para algo como uma Fooclasse com os atributos X, Y, Z (um vetor tridimensional), eu provavelmente preferiria a variante imutável.

Existem desvantagens imediatamente previsíveis no uso desse padrão, exceto por complicar a hierarquia da interface?

Projetos complicados demais tornam o software mais difícil de testar, mais difícil de manter e mais difícil de evoluir.

Doc Brown
fonte
1
Eu acho que você quer dizer "KISS - Keep It Simple, Stupid"
Epicblood 08/08/15
2

Existe um nome para dividir interfaces por acessadores e mutadores em interfaces separadas?

Poderia haver um nome para isso se essa separação for útil e fornecer um benefício que eu não vejo . Se as separações não fornecem um benefício, as outras duas perguntas não fazem sentido.

Você pode nos dizer algum caso de uso de negócios em que as duas interfaces separadas nos beneficiem ou essa questão é um problema acadêmico (YAGNI)?

Posso pensar numa loja com um carrinho mutável (você pode colocar mais artigos) que pode se tornar um pedido em que os artigos não podem mais ser alterados pelo cliente. O status do pedido ainda é mutável.

Implementações que eu vi não separam as interfaces

  • não há interface ReadOnlyList em java

A versão ReadOnly usa o "WritableInterface" e lança uma exceção se um método de gravação for usado

k3b
fonte
Modifiquei o OP para explicar o caso de uso principal.
Zymus
2
"não existe interface ReadOnlyList em java" - francamente, deveria haver. Se você tem um pedaço de código que aceita uma Lista <T> como parâmetro, não é possível dizer com facilidade se ele funcionará com uma lista não modificável. Código que retorna listas, não há como saber se você pode modificá-las com segurança. A única opção é confiar em documentação potencialmente incompleta ou imprecisa ou fazer uma cópia defensiva, por precaução. Um tipo de coleção somente leitura tornaria isso muito mais simples.
Jules
@ Jules: na verdade, o caminho Java parece estar acima: para garantir que a documentação esteja completa e precisa, além de fazer uma cópia defensiva, por precaução. Certamente é dimensionado para projetos corporativos muito grandes.
rwong
1

A separação de uma interface de coleção mutável e uma interface de coleção de contrato somente leitura é um exemplo do princípio de segregação de interface. Não acho que haja nomes especiais dados a cada aplicação de princípio.

Observe algumas palavras aqui: "contrato somente leitura" e "coleção".

Um contrato somente leitura significa que a classe Aconcede à classe Bum acesso somente leitura, mas não implica que o objeto de coleção subjacente seja realmente imutável. Imutável significa que nunca mudará no futuro, independentemente de qualquer agente. Um contrato somente leitura diz que o destinatário não tem permissão para alterá-lo; outra pessoa (em particular, classe A) tem permissão para alterá-lo.

Para tornar um objeto imutável, ele deve ser verdadeiramente imutável - ele deve negar tentativas de alterar seus dados, independentemente do agente que o solicitou.

O padrão é provavelmente observado em objetos que representam uma coleção de dados - listas, sequências, arquivos (fluxos) etc.


A palavra "imutável" se torna moda, mas o conceito não é novo. E existem muitas maneiras de usar a imutabilidade, bem como muitas maneiras de obter um melhor design usando outra coisa (ou seja, seus concorrentes).


Aqui está a minha abordagem (não baseada na imutabilidade).

  1. Defina um DTO (objeto de transferência de dados), também conhecido como tupla de valor.
    • Este DTO conterá os três campos: hitpoints, attack, defense.
    • Apenas campos: público, qualquer um pode escrever, sem proteção.
    • No entanto, um DTO deve ser um objeto descartável: se a classe Aprecisar passar um DTO para a classe B, ele fará uma cópia e passará a cópia. Portanto, Bpode usar o DTO da maneira que desejar (gravando nele), sem afetar o DTO que o Amantém.
  2. A grantExperiencefunção a ser dividida em duas:
    • calculateNewStats
    • increaseStats
  3. calculateNewStats pegará as entradas de dois DTO, um representando as estatísticas do jogador e outro representando as estatísticas do inimigo, e fará os cálculos.
    • Para a entrada, o chamador deve escolher entre as estatísticas base, treinadas ou efetivas, de acordo com suas necessidades.
    • O resultado será uma nova DTO onde cada campo ( hitpoints, attack, defense) armazena a quantidade a ser incrementado para que a capacidade.
    • O novo DTO "quantidade a incrementar" não se refere ao limite superior (limite máximo imposto) para esses valores.
  4. increaseStats é um método no player (não no DTO) que usa um DTO de "quantia para incrementar" e aplica esse incremento no DTO que pertence ao jogador e representa o DTO treinável do jogador.
    • Se houver valores máximos aplicáveis ​​a essas estatísticas, eles serão aplicados aqui.

Caso calculateNewStatsse verifique que não depende de nenhum outro jogador ou informação do inimigo (além dos valores no DTO de duas entradas), esse método pode estar localizado em qualquer lugar do projeto.

Se calculateNewStatshouver uma dependência completa dos objetos jogador e inimigo (ou seja, futuros jogadores e objetos inimigos podem ter novas propriedades e calculateNewStatsdevem ser atualizados para consumir o máximo possível de suas novas propriedades), calculateNewStatsaceite essas duas objetos, não apenas o DTO. No entanto, seu resultado de cálculo ainda será o DTO de incremento ou qualquer objeto simples de transferência de dados que transporta as informações usadas para executar um incremento / atualização.

rwong
fonte
1

Há um grande problema aqui: apenas porque uma estrutura de dados é imutável não significa que não precisamos de versões modificadas dela . A verdadeira versão imutável de uma estrutura de dados que fornecem setX, setYe setZmétodos - eles simplesmente retornar uma nova estrutura em vez de modificar o objeto que você chamou por diante.

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

Então, como você dá a uma parte do seu sistema a capacidade de alterá-lo enquanto restringe outras partes? Com um objeto mutável que contém uma referência a uma instância da estrutura imutável. Basicamente, em vez de sua classe de jogadores poder mudar seu objeto de estatísticas, dando a todas as outras classes uma visão imutável de suas estatísticas, suas estatísticas são imutáveis ​​e o jogador é mutável. Ao invés de:

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

Você teria:

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

Veja a diferença? O objeto de estatísticas é completamente imutável, mas as estatísticas atuais do jogador são uma referência mutável ao objeto imutável. Não há necessidade de criar duas interfaces - simplesmente torne a estrutura de dados completamente imutável e gerencie uma referência mutável a ela, onde você a está usando para armazenar o estado.

Isso significa que outros objetos no seu sistema não podem confiar em uma referência ao objeto de estatísticas - o objeto de estatísticas que eles têm não será o mesmo objeto de estatísticas que o jogador possui depois que o jogador atualiza suas estatísticas.

De qualquer forma, isso faz mais sentido, já que conceitualmente não são as estatísticas que estão mudando, são as estatísticas atuais do jogador . Não é o objeto de estatísticas. A referência ao objeto de estatísticas que o objeto do jogador possui. Portanto, outras partes do seu sistema, dependendo dessa referência, devem fazer referência explícita player.currentStats()ou algo parecido, em vez de localizar o objeto de estatísticas subjacente do jogador, armazená-lo em algum lugar e confiar na atualização através de mutações.

Jack
fonte
1

Uau ... isso realmente me leva de volta. Eu tentei essa mesma idéia algumas vezes. Eu era teimosa demais para desistir, porque pensei que haveria algo bom para aprender. Eu já vi outros tentarem isso no código também. A maioria das implementações que eu vi chamam de interfaces somente leitura como a sua FooViewerou ReadOnlyFooa interface somente gravação FooEditorou WriteableFooou em Java que acho que FooMutatorjá vi uma vez. Não sei se existe um vocabulário oficial ou mesmo comum para fazer as coisas dessa maneira.

Isso nunca fez nada de útil para mim nos lugares em que tentei. Eu evitaria isso completamente. Eu faria o que os outros estão sugerindo, dar um passo atrás e considerar se você realmente precisa dessa noção em sua idéia maior. Não tenho certeza de que haja uma maneira correta de fazer isso, pois nunca guardei nenhum código que produzi enquanto tentava fazer isso. Cada vez eu desistia depois de um esforço considerável e de coisas simplificadas. E com isso quero dizer algo parecido com o que os outros disseram sobre YAGNI, KISS e DRY.

Uma desvantagem possível: repetição. Você precisará criar essas interfaces para muitas classes potencialmente e, mesmo para apenas uma classe, precisará nomear cada método e descrever cada assinatura pelo menos duas vezes. De todas as coisas que me queimam na codificação, ter que alterar vários arquivos dessa maneira me causa mais problemas. Acabo esquecendo de fazer as alterações em um lugar ou outro. Depois de ter uma classe chamada Foo e interfaces chamadas FooViewer e FooEditor, se você decidir que seria melhor se você chamasse de Bar, precisará refatorar-> renomear três vezes, a menos que tenha um IDE realmente incrivelmente inteligente. Eu até descobri que esse era o caso quando eu tinha uma quantidade considerável de código proveniente de um gerador de código.

Pessoalmente, não gosto de criar interfaces para coisas que possuem apenas uma única implementação também. Isso significa que, quando viajo pelo código, não posso simplesmente pular diretamente para a única implementação, tenho que pular para a interface e depois para a implementação da interface ou, pelo menos, pressionar algumas teclas extras para chegar lá. Essas coisas se somam.

E depois há a complicação que você mencionou. Duvido que alguma vez eu vá desse jeito em particular com meu código novamente. Mesmo no lugar em que isso se encaixava melhor no meu plano geral para o código (um ORM que eu criei), puxei isso e o substitui por algo mais fácil de codificar.

Eu realmente pensaria mais na composição que você mencionou. Estou curioso por que você acha que seria uma má ideia. Eu teria esperado para ter EffectiveStatscompor e imutável Statse algo como StatModifierso que ainda compor um conjunto de StatModifiers que representam qualquer coisa que possa modificar estatísticas (efeitos temporários, itens, localização em alguma área realce, fadiga), mas que a sua EffectiveStatsnão precisa entender porque StatModifiersWould gerenciar o que eram essas coisas, qual o efeito e que tipo teriam em quais estatísticas. oStatModifierseria uma interface para as diferentes coisas e poderia saber coisas como "estou na zona", "quando o remédio se esgota", etc ... mas não precisaria deixar que outros objetos soubessem dessas coisas. Seria necessário apenas dizer qual estatística e como foi modificada no momento. Melhor ainda, StatModifierpoderia simplesmente expor um método para produzir um novo imutável Statsbaseado em outro, Statsque seria diferente porque havia sido alterado adequadamente. Você poderia fazer algo assim currentStats = statModifier2.modify(statModifier1.modify(baseStats))e tudo isso Statspode ser imutável. Eu nem codificaria isso diretamente, provavelmente passaria por todos os modificadores e aplicaria cada um ao resultado dos modificadores anteriores, começando com baseStats.

Jason Kell
fonte