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 Foo
definida 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 MutableFoo
que 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 MutableFoo
que 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 StatSet
que 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:
- Existe um nome para dividir interfaces por acessadores e mutadores em interfaces separadas?
- 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 retornarDefaultFoo
ou queDefaultMutableFoo
está oculto porquecreateImmutableFoo
retornaFoo
)? - 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 EffectiveStatSet
não faz muito sentido, pois não estamos estendendo a funcionalidade de forma alguma. Poderíamos mudar a implementação e formar EffectiveStatSet
um 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);
}
}
Respostas:
Para mim, parece que você tem uma solução procurando um problema.
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?
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
Foo
classe com os atributos X, Y, Z (um vetor tridimensional), eu provavelmente preferiria a variante imutável.Projetos complicados demais tornam o software mais difícil de testar, mais difícil de manter e mais difícil de evoluir.
fonte
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
A versão ReadOnly usa o "WritableInterface" e lança uma exceção se um método de gravação for usado
fonte
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
A
concede à classeB
um 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, classeA
) 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).
hitpoints
,attack
,defense
.A
precisar passar um DTO para a classeB
, ele fará uma cópia e passará a cópia. Portanto,B
pode usar o DTO da maneira que desejar (gravando nele), sem afetar o DTO que oA
mantém.grantExperience
função a ser dividida em duas:calculateNewStats
increaseStats
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.hitpoints
,attack
,defense
) armazena a quantidade a ser incrementado para que a capacidade.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.Caso
calculateNewStats
se 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
calculateNewStats
houver uma dependência completa dos objetos jogador e inimigo (ou seja, futuros jogadores e objetos inimigos podem ter novas propriedades ecalculateNewStats
devem ser atualizados para consumir o máximo possível de suas novas propriedades),calculateNewStats
aceite 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.fonte
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
,setY
esetZ
métodos - eles simplesmente retornar uma nova estrutura em vez de modificar o objeto que você chamou por diante.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:
Você teria:
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.fonte
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
FooViewer
ouReadOnlyFoo
a interface somente gravaçãoFooEditor
ouWriteableFoo
ou em Java que acho queFooMutator
já 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
EffectiveStats
compor e imutávelStats
e algo comoStatModifiers
o que ainda compor um conjunto deStatModifier
s que representam qualquer coisa que possa modificar estatísticas (efeitos temporários, itens, localização em alguma área realce, fadiga), mas que a suaEffectiveStats
não precisa entender porqueStatModifiers
Would gerenciar o que eram essas coisas, qual o efeito e que tipo teriam em quais estatísticas. oStatModifier
seria 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,StatModifier
poderia simplesmente expor um método para produzir um novo imutávelStats
baseado em outro,Stats
que seria diferente porque havia sido alterado adequadamente. Você poderia fazer algo assimcurrentStats = statModifier2.modify(statModifier1.modify(baseStats))
e tudo issoStats
pode 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 combaseStats
.fonte