Padrões para propagação alteram um modelo de objeto ..?


22

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?


Você está certo sobre # 1 ser ruim, e eu também não estou interessado em # 2; geralmente você deseja evitar efeitos colaterais e, em vez disso, aumenta-os. Em relação à opção 3, por que você não pode fatorar AnimalMakeMess em um método estático que todas as classes podem chamar?
Doval

4
O número 1 não é necessariamente ruim se você se comunicar por meio de uma interface (IAnimalObserver) em vez da classe Parent específica.
Coredump 22/03

Respostas:


11

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 Dirtypropriedade em cada animal e deixe o Animal.IsDirtyretorno this.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 o ZooMaintenancedepartamento para verificar se Zooestava sujo novamente. Como alternativa, eu poderia simplesmente enviar as ZooMaintenanceverificaçõ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 Controllerexemplo no meu exemplo, você injeta todos os objetos com um IMessageBusserviço. A Animalclasse pode publicar uma mensagem, como "Mess Made" e sua Zooclasse pode se inscrever na mensagem "Mess Made". O serviço de barramento de mensagens se encarrega de notificar Zooquando um animal publica uma dessas mensagens e pode reavaliar sua IsDirtypropriedade.

Isso tem a vantagem de Animalsnão precisar mais de uma Zooreferência e Zoonão precisa se preocupar em se inscrever ou cancelar a inscrição em eventos de todos Animal. A penalidade é que todas as Zooclasses 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 duas Zooinstâ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 Animalclasse que, quando MakeMessé chamado, ele propaga a mensagem até a Zooe será óbvio para a Zooclasse 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 o Zoodeve ser notificado se houver Animaluma bagunça. Se houvesse mais objetos que precisavam ser notificados, provavelmente eu passaria para um barramento de mensagens ou um controlador.


5

Fiz um diagrama de classes simples que descreve seu domínio: insira a descrição da imagem aqui

Cada Animal um tem uma Habitat bagunça.

Ele Habitatnã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 Animalisso se importa, porque se comportará de maneira diferente em todos Habitat.

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:

public interface Habitat {
    public void messUp(float magnitude);

    public float getCleanliness();
}

Um exemplo de concreto Habitat. é claro que cada Habitatsubclasse pode implementar esses métodos de maneira diferente.

public class Zoo implements Habitat {
    public float cleanliness = 1;

    public float getCleanliness() {
        return cleanliness;
    }

    public void messUp(float magnitude) {
        cleanliness -= magnitude;
    }
}

Claro que você pode ter várias subclasses de animais, onde cada uma delas estraga tudo de maneira diferente:

public class Animel {
    private Habitat habitat;

    public void makeMess() {
        habitat.messUp(.05f);
    }

    public Animel addTo(Habitat habitat) {
        this.habitat = habitat;
        return this;
    }
}

Esta é a classe do cliente, basicamente explica como você pode usar esse design.

public class ZooKeeper {
    public Habitat zoo = new Zoo();

    public ZooKeeper() {
        new Animal()
            .addTo( zoo )
            .makeMess();

        if (zoo.getCleanliness() < 0.5f) {
            System.out.println("The zoo is really messy");
        } else {
            System.out.println("The zoo looks clean");
        }
    }
}

Obviamente, em seu aplicativo real, você pode informar Habitate gerenciar o Animalque precisar.


3

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.

interface MessablePlace
{
  void OnMess(object sender, MessEvent e);
}

class MessEvent
{
  String DetailsOrWhatever;
}

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 Houseou FairlyLand.


3
  • A opção 1 é realmente bastante direta. Isso é apenas uma referência posterior. Mas generalize-o com a interface chamada Dwellinge forneça um MakeMessmétodo. Isso quebra a dependência circular. Então, quando o animal faz uma bagunça, também chama dwelling.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.

  • Levando essa idéia adiante, você poderia usar uma arquitetura encadeada. Ou seja, crie uma interface Messablee, em cada item passível de mensagem, inclua uma referência a next. Depois de criar uma bagunça, chame MakeMesso próximo item.

Então, o Zoo aqui está envolvido em fazer uma bagunça, porque fica bagunçado também. Ter:

Zoo implements Messable
House implements Messable
Animal implements Messable
   Messable next

   MakeMess()
       messy = true
       next.MakeMess

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)ou House.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.

public Animal
    Function afterMess

    public MakeMess()
        messy = true
        afterMess()

Quando o animal se mover, basta definir um novo delegado.

  • Levado ao extremo, você pode usar a Programação Orientada a Aspectos (AOP) com conselhos "depois" sobre MakeMess.

2

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 :

public interface IAnimal
{
    string Name { get; set; }
    int Age { get; set; }

    void MakeMess();
}

public class Animal : IAnimal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void MakeMess()
    {
        // makes mess
    }
}

public class ZooAnimals
{
    class AnimalInZoo : IAnimal
    {
        public IAnimal _animal;
        public ZooAnimals _zoo;

        public AnimalInZoo(IAnimal animal, ZooAnimals zoo)
        {
            _animal = animal;
            _zoo = zoo;
        }

        public string Name { get { return _animal.Name; } set { _animal.Name = value; } }
        public int Age { get { return _animal.Age; } set { _animal.Age = value; } }

        public void MakeMess()
        {
            _animal.MakeMess();
            _zoo.IsDirty = true;
        }
    }

    private Collection<AnimalInZoo> animals = new Collection<AnimalInZoo>();

    public IAnimal Add(IAnimal animal)
    {
        if (animal is AnimalInZoo)
        {
            var inZoo = (AnimalInZoo)animal;
            if (inZoo._zoo != this)
            {
                // animal is in a different zoo, what to do ?
                // either move animal to this zoo
                // or throw an exception so caller is forced to remove the animal from previous zoo first
            }
        }

        var anim = new AnimalInZoo(animal, this);
        animals.Add(anim);
        return anim;
    }

    public IAnimal Remove(IAnimal animal)
    {
        if (!(animal is AnimalInZoo))
        {
            // animal is not in zoo, throw an exception?
        }
        var inZoo = (AnimalInZoo)animal;
        if (inZoo._zoo != this)
        {
            // animal is in a different zoo, throw an exception?
        }

        animals.Remove(inZoo);
        return inZoo._animal;
    }

    public bool IsDirty { get; set; }
}

É 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.


1

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.


0

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 MakeMessem Animalcom um efeito colateral ou evento, passar Zoopara o método. A opção 1 é adequada se você precisar proteger os invariantes que Animalsempre 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.


Eu suspeito que isso é mais como um botão de envio em um formulário e uma seqüência de caracteres em uma lista.
Svidgen

1
@svidgen - Depois passe um retorno de chamada. Mais infalível que um evento, mais fácil de raciocinar e sem referências impertinentes a coisas que ele não precisa saber.
Telastyn
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.