Estou desenvolvendo um modelo de objeto que possui várias classes pai / filho diferentes. Cada objeto filho tem uma referência ao seu objeto pai. Posso pensar em (e tentei) várias maneiras de inicializar a referência pai, mas encontro desvantagens significativas em cada abordagem. Dadas as abordagens descritas abaixo, qual é a melhor ... ou o que é ainda melhor.
Não vou garantir que o código abaixo seja compilado; portanto, tente ver minha intenção se o código não estiver sintaticamente correto.
Observe que alguns dos meus construtores de classe filho usam parâmetros (que não sejam pai), mesmo que nem sempre eu os mostre.
O chamador é responsável por definir o pai e adicionar ao mesmo pai.
class Child { public Child(Parent parent) {Parent=parent;} public Parent Parent {get; private set;} } class Parent { // singleton child public Child Child {get; set;} //children private List<Child> _children = new List<Child>(); public List<Child> Children { get {return _children;} } }
Desvantagem: definir pai é um processo de duas etapas para o consumidor.
var child = new Child(parent); parent.Children.Add(child);
Desvantagem: propenso a erros. O chamador pode adicionar um filho a um pai diferente do que foi usado para inicializar o filho.
var child = new Child(parent1); parent2.Children.Add(child);
O pai verifica se o chamador adiciona o filho ao pai para o qual foi inicializado.
class Child { public Child(Parent parent) {Parent = parent;} public Parent Parent {get; private set;} } class Parent { // singleton child private Child _child; public Child Child { get {return _child;} set { if (value.Parent != this) throw new Exception(); _child=value; } } //children private List<Child> _children = new List<Child>(); public ReadOnlyCollection<Child> Children { get {return _children;} } public void AddChild(Child child) { if (child.Parent != this) throw new Exception(); _children.Add(child); } }
Desvantagem: o chamador ainda tem um processo de duas etapas para definir o pai.
Desvantagem: verificação do tempo de execução - reduz o desempenho e adiciona código a cada add / setter.
O pai define a referência do pai da criança (para si mesmo) quando o filho é adicionado / atribuído a um pai. O setter pai é interno.
class Child { public Parent Parent {get; internal set;} } class Parent { // singleton child private Child _child; public Child Child { get {return _child;} set { value.Parent = this; _child = value; } } //children private List<Child> _children = new List<Child>(); public ReadOnlyCollection<Child> Children { get {return _children;} } public void AddChild(Child child) { child.Parent = this; _children.Add(child); } }
Desvantagem: o filho é criado sem uma referência pai. Às vezes, a inicialização / validação requer o pai / mãe, o que significa que alguma inicialização / validação deve ser realizada no pai / mãe da criança. O código pode ficar complicado. Seria muito mais fácil implementar o filho se ele sempre tivesse sua referência principal.
Pai expõe métodos de adição de fábrica para que um filho sempre tenha uma referência pai. O filho é interno. O setter dos pais é privado.
class Child { internal Child(Parent parent, init-params) {Parent = parent;} public Parent Parent {get; private set;} } class Parent { // singleton child public Child Child {get; private set;} public void CreateChild(init-params) { var child = new Child(this, init-params); Child = value; } //children private List<Child> _children = new List<Child>(); public ReadOnlyCollection<Child> Children { get {return _children;} } public Child AddChild(init-params) { var child = new Child(this, init-params); _children.Add(child); return child; } }
Desvantagem: não é possível usar sintaxe de inicialização como
new Child(){prop = value}
. Em vez disso, tem que fazer:var c = parent.AddChild(); c.prop = value;
Desvantagem: É necessário duplicar os parâmetros do construtor filho nos métodos de adição de fábrica.
Desvantagem: Não é possível usar um configurador de propriedades para um filho único. Parece lame que eu preciso de um método para definir o valor, mas fornecer acesso de leitura através de um getter de propriedade. É desequilibrado.
Filho se adiciona ao pai referenciado em seu construtor. O child child é público. Nenhum acesso público de adição do pai.
//singleton class Child{ public Child(ParentWithChild parent) { Parent = parent; Parent.Child = this; } public ParentWithChild Parent {get; private set;} } class ParentWithChild { public Child Child {get; internal set;} } //children class Child { public Child(ParentWithChildren parent) { Parent = parent; Parent._children.Add(this); } public ParentWithChildren Parent {get; private set;} } class ParentWithChildren { internal List<Child> _children = new List<Child>(); public ReadOnlyCollection<Child> Children { get {return _children;} } }
Desvantagem: chamar a sintaxe não é ótimo. Normalmente, chama-se um
add
método no pai, em vez de apenas criar um objeto como este:var parent = new ParentWithChildren(); new Child(parent); //adds child to parent new Child(parent); new Child(parent);
E define uma propriedade em vez de apenas criar um objeto como este:
var parent = new ParentWithChild(); new Child(parent); // sets parent.Child
...
Acabei de aprender que a SE não permite algumas perguntas subjetivas e, claramente, essa é uma pergunta subjetiva. Mas, talvez seja uma boa pergunta subjetiva.
fonte
Respostas:
Eu ficaria longe de qualquer cenário que requer necessariamente que a criança conheça os pais.
Existem maneiras de passar mensagens de filho para pai através de eventos. Dessa forma, o pai, ao adicionar, simplesmente precisa se registrar em um evento que o filho aciona, sem que o filho precise saber diretamente sobre o pai. Afinal, esse provavelmente é o uso pretendido de uma criança que conhece seus pais, para poder usá-los com algum efeito. Exceto que você não gostaria que a criança realizasse o trabalho dos pais, então realmente o que você quer fazer é simplesmente dizer aos pais que algo aconteceu. Portanto, o que você precisa lidar é com um evento no filho, do qual os pais podem tirar vantagem.
Esse padrão também é muito bom se esse evento se tornar útil para outras classes. Talvez seja um exagero, mas também impede que você se atire no pé mais tarde, pois fica tentador querer usar os pais na classe de seus filhos, que apenas acasalam ainda mais as duas classes. A refatoração dessas classes posteriormente consome tempo e pode facilmente criar bugs no seu programa.
Espero que ajude!
fonte
Child
) mantendo um conjunto de observadores (Parent
), que reintroduz a circularidade de maneira inversa (e introduz uma série de questões próprias).Eu acho que sua opção 3 pode ser a mais limpa. Você escreveu.
Não vejo isso como uma desvantagem. De fato, o design do seu programa pode se beneficiar de objetos filhos que podem ser criados sem um pai primeiro. Por exemplo, ele pode facilitar muito o teste de crianças isoladas. Se um usuário do seu modelo esquecer de adicionar um filho ao pai e chamar métodos que esperam que a propriedade pai seja inicializada na classe filho, ele receberá uma exceção de ref nula - que é exatamente o que você deseja: uma falha precoce por um erro uso.
E se você acha que um filho precisa que seu atributo pai seja inicializado no construtor sob todas as circunstâncias por motivos técnicos, use algo como um "objeto pai nulo" como valor padrão (embora isso corra o risco de mascarar erros).
fonte
SetParent
método que seja necessário para suportar a alteração da paternidade de um filho existente (o que pode ser difícil de fazer e / ou sem sentido) ou só poderá ser chamado uma vez. Modelar situações como agregadas (como sugere Mike Brown) pode ser muito melhor do que ter filhos começando sem os pais.Não há nada para evitar alta coesão entre duas classes que são usadas comumente juntas (por exemplo, um Order e LineItem geralmente se referem). No entanto, nesses casos, tenho a tendência de aderir às regras de Design Orientado a Domínio e modelá-las como um Agregado, com o Pai sendo a Raiz Agregada. Isso nos diz que o AR é responsável pela vida útil de todos os objetos em seu agregado.
Portanto, seria mais parecido com o cenário quatro, em que o pai expõe um método para criar seus filhos, aceitando todos os parâmetros necessários para inicializar adequadamente os filhos e adicioná-lo à coleção.
fonte
Eu sugeriria ter objetos "child-factory" que são passados para um método pai que cria um filho (usando o objeto "child factory"), o anexa e retorna uma exibição. O próprio objeto filho nunca será exposto fora do pai. Essa abordagem pode funcionar bem para coisas como simulações. Em uma simulação eletrônica, um objeto "fábrica filho" específico pode representar as especificações para algum tipo de transistor; outro pode representar as especificações para um resistor; um circuito que precisa de dois transistores e quatro resistores pode ser criado com código como:
Observe que o simulador não precisa reter nenhuma referência ao objeto criador filho e
AddComponent
não retornará uma referência ao objeto criado e mantido pelo simulador, mas sim a um objeto que represente uma exibição. Se oAddComponent
método for genérico, o objeto de exibição poderá incluir funções específicas do componente, mas não exporá os membros que o pai usa para gerenciar o anexo.fonte
Ótima lista. Eu não sei qual método é o "melhor", mas aqui está um para encontrar os métodos mais expressivos.
Comece com a classe pai e filho mais simples possível. Escreva seu código com esses. Depois de notar a duplicação de código que pode ser nomeada, você a coloca em um método.
Talvez você entenda
addChild()
. Talvez você consiga algo comoaddChildren(List<Child>)
ouaddChildrenNamed(List<String>)
ouloadChildrenFrom(String)
ounewTwins(String, String)
ouChild.replicate(int)
.Se o seu problema é realmente forçar um relacionamento um para muitos, talvez você deva
Esta não é uma resposta, mas espero que você encontre uma ao ler isso.
fonte
Compreendo que ter links de filho para pai tenha suas desvantagens, conforme observado acima.
No entanto, para muitos cenários, as soluções alternativas com eventos e outras mecânicas "desconectadas" também trazem suas próprias complexidades e linhas extras de código.
Por exemplo, a criação de um evento de Child para ser recebido por seu Parent vinculará os dois, embora de maneira flexível.
Talvez para muitos cenários esteja claro para todos os desenvolvedores o que significa uma propriedade Child.Parent. Para a maioria dos sistemas em que trabalhei, funcionou bem. O excesso de engenharia pode ser demorado e…. Confuso!
Tenha um método Parent.AttachChild () que execute todo o trabalho necessário para vincular o filho ao pai. Todo mundo está claro sobre o que isso "significa"
fonte