Aqui está um cenário comum que é sempre frustrante para mim.
Eu tenho um modelo de objeto com um objeto pai. O pai contém alguns objetos filhos. Algo assim.
public class Zoo
{
public List<Animal> Animals { get; set; }
public bool IsDirty { get; set; }
}
Cada objeto filho possui vários dados e métodos
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
public void MakeMess()
{
...
}
}
Quando o filho muda, nesse caso, quando o método MakeMess é chamado, algum valor no pai precisa ser atualizado. Digamos que quando um certo limite de Animal fez uma bagunça, o sinalizador IsDirty do Zoo precisa ser definido.
Existem algumas maneiras de lidar com esse cenário (que eu saiba).
1) Cada animal pode ter uma referência de zoológico principal para comunicar as alterações.
public class Animal
{
public Zoo Parent { get; set; }
...
public void MakeMess()
{
Parent.OnAnimalMadeMess();
}
}
Essa é a pior opção, pois une Animal ao objeto pai. E se eu quiser um animal que mora em uma casa?
2) Outra opção, se você estiver usando um idioma que suporta eventos (como C #), é pedir que o pai se inscreva para alterar os eventos.
public class Animal
{
public event OnMakeMessDelegate OnMakeMess;
public void MakeMess()
{
OnMakeMess();
}
}
public class Zoo
{
...
public void SubscribeToChanges()
{
foreach (var animal in Animals)
{
animal.OnMakeMess += new OnMakeMessDelegate(OnMakeMessHandler);
}
}
public void OnMakeMessHandler(object sender, EventArgs e)
{
...
}
}
Isso parece funcionar, mas por experiência fica difícil de manter. Se os animais mudarem de zoológico, você precisará cancelar a inscrição de eventos no zoológico antigo e se inscrever novamente no novo zoológico. Isso só piora à medida que a árvore de composição fica mais profunda.
3) A outra opção é mover a lógica para o pai.
public class Zoo
{
public void AnimalMakesMess(Animal animal)
{
...
}
}
Isso parece muito antinatural e causa duplicação de lógica. Por exemplo, se eu tivesse um objeto House que não compartilhe nenhum pai de herança comum com o Zoo.
public class House
{
// Now I have to duplicate this logic
public void AnimalMakesMess(Animal animal)
{
...
}
}
Ainda não encontrei uma boa estratégia para lidar com essas situações. O que mais está disponível? Como isso pode ser simplificado?
fonte
Respostas:
Eu tive que lidar com isso algumas vezes. A primeira vez que usei a opção 2 (eventos) e, como você disse, ficou muito complicado. Se você seguir esse caminho, sugiro que você precise de testes de unidade muito completos para garantir que os eventos sejam executados corretamente e que você não esteja deixando referências pendentes, caso contrário, é muito difícil depurar.
Na segunda vez, acabei de implementar a propriedade dos pais como uma função dos filhos; portanto, mantenha uma
Dirty
propriedade em cada animal e deixe oAnimal.IsDirty
retornothis.Animals.Any(x => x.IsDirty)
. Isso estava no modelo. Acima do modelo, havia um Controlador, e o trabalho do controlador era saber que, depois que eu mudei o modelo (todas as ações no modelo foram passadas pelo controlador, para que ele soubesse que algo havia mudado), ele sabia que precisava chamar certas funções de avaliação, como acionar oZooMaintenance
departamento para verificar seZoo
estava sujo novamente. Como alternativa, eu poderia simplesmente enviar asZooMaintenance
verificações até algum horário programado mais tarde (a cada 100 ms, 1 segundo, 2 minutos, 24 horas, o que fosse necessário).Eu descobri que o último tem sido muito mais simples de manter, e meus medos de problemas de desempenho nunca se materializaram.
Editar
Outra maneira de lidar com isso é um padrão de barramento de mensagens . Em vez de usar um
Controller
exemplo no meu exemplo, você injeta todos os objetos com umIMessageBus
serviço. AAnimal
classe pode publicar uma mensagem, como "Mess Made" e suaZoo
classe pode se inscrever na mensagem "Mess Made". O serviço de barramento de mensagens se encarrega de notificarZoo
quando um animal publica uma dessas mensagens e pode reavaliar suaIsDirty
propriedade.Isso tem a vantagem de
Animals
não precisar mais de umaZoo
referência eZoo
não precisa se preocupar em se inscrever ou cancelar a inscrição em eventos de todosAnimal
. A penalidade é que todas asZoo
classes que assinam essa mensagem terão que reavaliar suas propriedades, mesmo que não fosse um de seus animais. Isso pode ou não ser um grande negócio. Se houver apenas uma ou duasZoo
instâncias, provavelmente está bem.Editar 2
Não descarte a simplicidade da opção 1. Qualquer pessoa que revisitar o código não terá muitos problemas para entendê-lo. Será óbvio para alguém que olha para a
Animal
classe que, quandoMakeMess
é chamado, ele propaga a mensagem até aZoo
e será óbvio para aZoo
classe de onde ela recebe suas mensagens. Lembre-se de que na programação orientada a objetos, uma chamada de método costumava ser chamada de "mensagem". De fato, o único momento em que faz muito sentido romper com a opção 1 é se mais do que apenas oZoo
deve ser notificado se houverAnimal
uma bagunça. Se houvesse mais objetos que precisavam ser notificados, provavelmente eu passaria para um barramento de mensagens ou um controlador.fonte
Fiz um diagrama de classes simples que descreve seu domínio:
Cada
Animal
um tem umaHabitat
bagunça.Ele
Habitat
não se importa com o que ou quantos animais ele tem (a menos que seja fundamentalmente parte do seu design que, neste caso, você o descreveu).Mas
Animal
isso se importa, porque se comportará de maneira diferente em todosHabitat
.Esse diagrama é semelhante ao diagrama UML do padrão de design da estratégia , mas o usaremos de maneira diferente.
Aqui estão alguns exemplos de código em Java (não quero cometer erros específicos em C #).
É claro que você pode fazer seus próprios ajustes nesse design, idioma e requisitos.
Esta é a interface da estratégia:
Um exemplo de concreto
Habitat
. é claro que cadaHabitat
subclasse pode implementar esses métodos de maneira diferente.Claro que você pode ter várias subclasses de animais, onde cada uma delas estraga tudo de maneira diferente:
Esta é a classe do cliente, basicamente explica como você pode usar esse design.
Obviamente, em seu aplicativo real, você pode informar
Habitat
e gerenciar oAnimal
que precisar.fonte
Eu tive bastante sucesso com arquiteturas como a sua opção 2 no passado. É a opção mais geral e permitirá a maior flexibilidade. Mas, se você tiver controle sobre seus ouvintes e não estiver gerenciando muitos tipos de assinatura, poderá se inscrever nos eventos com mais facilidade, criando uma interface.
A opção de interface tem a vantagem de ser quase tão simples quanto a opção 1, mas também permite que você hospede animais com bastante facilidade em um
House
ouFairlyLand
.fonte
Dwelling
e forneça umMakeMess
método. Isso quebra a dependência circular. Então, quando o animal faz uma bagunça, também chamadwelling.MakeMess()
.No espírito de lex parsimoniae , eu vou usar essa, embora eu provavelmente usaria a solução em cadeia abaixo, me conhecendo. (Esse é exatamente o mesmo modelo sugerido por @Benjamin Albert.)
Observe que, se você estivesse modelando tabelas de bancos de dados relacionais, a relação seria inversa: Animal teria uma referência ao Zoo e a coleção de Animais para um Zoo seria um resultado de consulta.
Messable
e, em cada item passível de mensagem, inclua uma referência anext
. Depois de criar uma bagunça, chameMakeMess
o próximo item.Então, o Zoo aqui está envolvido em fazer uma bagunça, porque fica bagunçado também. Ter:
Então agora você tem uma cadeia de coisas que recebem a mensagem de que uma bagunça foi criada.
Opção 2, um modelo de publicação / assinatura pode funcionar aqui, mas parece realmente muito pesado. O objeto e o contêiner têm um relacionamento conhecido, portanto, parece um pouco pesado usar algo mais geral do que isso.
Opção 3: nesse caso em particular, ligar
Zoo.MakeMess(animal)
ouHouse.MakeMess(animal)
não é realmente uma opção ruim, porque uma casa pode ter semântica diferente para ficar bagunçada que um zoológico.Mesmo se você não seguir o caminho da cadeia, parece que existem dois problemas aqui: 1) o problema é sobre a propagação de uma alteração de um objeto para seu contêiner, 2) parece que você deseja desativar uma interface para o recipiente para abstrair onde os animais podem viver.
...
Se você tiver funções de primeira classe, poderá passar uma função (ou delegar) para o Animal para ligar depois que ele fizer uma bagunça. É um pouco como a ideia de cadeia, exceto com uma função em vez de uma interface.
Quando o animal se mover, basta definir um novo delegado.
fonte
Eu iria com 1, mas tornaria o relacionamento pai-filho juntamente com a lógica de notificação em um wrapper separado. Isso remove a dependência do Animal no Zoo e permite o gerenciamento automático dos relacionamentos pai-filho. Mas isso requer que você refaça os objetos na hierarquia em interfaces / classes abstratas primeiro e escreva um wrapper específico para cada interface. Mas isso pode ser removido usando a geração de código.
Algo como :
É assim que alguns ORMs fazem o rastreamento de alterações nas entidades. Eles criam wrappers em torno das entidades e fazem você trabalhar com elas. Esses wrappers geralmente são criados usando reflexão e geração dinâmica de código.
fonte
Duas opções que costumo usar. Você pode usar a segunda abordagem e colocar a lógica para conectar o evento na própria coleção no pai.
Uma abordagem alternativa (que pode realmente ser usada com qualquer uma das três opções) é usar a contenção. Faça um AnimalContainer (ou até mesmo uma coleção) que possa morar na casa ou no zoológico ou qualquer outra coisa. Ele fornece a funcionalidade de rastreamento associada aos animais, mas evita problemas de herança, pois pode ser incluído em qualquer objeto que precise.
fonte
Você começa com uma falha básica: os objetos filhos não devem saber sobre seus pais.
As strings sabem que estão em uma lista? Não. As datas sabem que elas existem em um calendário? Não.
A melhor opção é alterar seu design para que esse tipo de cenário não exista.
Depois disso, considere a inversão de controle. Em vez de
MakeMess
emAnimal
com um efeito colateral ou evento, passarZoo
para o método. A opção 1 é adequada se você precisar proteger os invariantes queAnimal
sempre precisam morar em algum lugar. Não é um pai, mas uma associação de pares então.Ocasionalmente 2 e 3 dão certo, mas o principal princípio arquitetônico a seguir é que as crianças não sabem sobre seus pais.
fonte