0

In cases where there is an object handling multiple other objects, how can the ownership be expressed at initialization?

For examples sake, let's say there is a Chicken hatching multiple eggs. It would not be good for the chicks to be without a parent, but the Chicken should also only focus its attention to its chicks. If this kind of relationship should be expressed at initialization, how would it be possible?

Which design pattern would be the best to use in this scenario?

It would be preferable to avoid using null if possible, because that might introduce ambiguity into the system. If it's unavoidable to use it, how can the ambiguity best be minimzed?

Example program:

public class MyClass {

    /**
     * Handles eggs
     * */
    public static class Chicken{
        private Egg[] eggs;
        
        public Chicken(Egg... eggs_){
            eggs = eggs_;
        }
        
        public void accept_signal(String message){
            /* ... */
        }
    }
    
    /**
     * Signals when its ready to hatch
     * */
    public static abstract class Egg{
        private final Chicken parent;
        public Egg(Chicken parent_){
            parent = parent_;
        }
        
        public void hatch(){
            parent.accept_signal("chirp chirp");
        }
    }
    
    /**
     * The resulting children to be handled
     * */
    public static class YellowChick extends Egg{
        public YellowChick(Chicken parent_){
            super(parent_);
        }
    }
    
    public static class BrownChick extends Egg{
        public BrownChick(Chicken parent_){
            super(parent_);
        }
    }
    
    public static class UglyDuckling extends Egg{
        public UglyDuckling(Chicken parent_){
            super(parent_);
        }
    }

    public static void main (String[] args){
        Chicken momma = new Chicken(
        //  new YellowChick(), new BrownChick(), new UglyDuckling()  /* How can these objects be initialized properly? */
        );
    }

}
Dávid Tóth
  • 2,788
  • 1
  • 21
  • 46
  • You can add a method `Chicken#addEgg` and call it in the `Egg` constructor. – Piotr P. Karwasz Jan 27 '21 at 10:16
  • Here the problem is that the number of eggs should be set at initialization ( it's an array ) and also, there is no Chicken object without a list of its eggs... so which comes first then? :D – Dávid Tóth Jan 27 '21 at 11:16

2 Answers2

2

Circular dependencies in software are generally a bad idea.
The solution to a problem like,

Chicken <--> Egg

...is to introduce a middleman that manages the relationship.
The manager can be implemented in different ways.

  • Chicken --> PoultryMediator <-- Egg
    Here the manager acts as a mediator, answering queries and providing references at runtime.
  • Chicken <-- PoultryInjector --> Egg
    Here the manager acts as a dependency injector, wiring up the objects at construction time.

For some related links, see also: Spot problems with circular dependency.

jaco0646
  • 15,303
  • 7
  • 59
  • 83
  • Do you think the Builder pattern can be used as a dependency injector? – Dávid Tóth Jan 28 '21 at 06:40
  • Maybe, but the Builder pattern unnecessarily complicates the issue, because DI is simpler without it. – jaco0646 Jan 28 '21 at 14:39
  • Thank you for your answer! :) Might I ask you to critique the answer I gave, please? – Dávid Tóth Jan 28 '21 at 14:47
  • I think the complexity of the code exceeds the complexity of the problem, i.e. it is over-engineered; but that's fine for an academic exercise. Note that if your code is fully working and runnable, you can post it at https://codereview.stackexchange.com. – jaco0646 Jan 28 '21 at 15:20
  • Thank you for the feedback! :) It's working as a stand-alone example. I did not spare energy on it because the actual problem is more complex, than that, I just wanted to show the issue on a simple example. – Dávid Tóth Jan 29 '21 at 09:53
0

The Builder Pattern can be used as a dependency inector to eliminate the deadlock during initialization. Since the Chicken class depends on an interface ( Egg ), which itself cannot be instantiated, the pattern mentioned in this question can be applied.

There are 3 versions of introducing it for this example. The first is simpler, but it also gives the possibility to access the half-done Chicken class. In each version more and more ambiguity is eliminated at the cost of complexity.

public class MyClass {

    /**
     * Handles eggs
     * */
    public static class Chicken{
        private Egg[] eggs;

        private Chicken(){}
        private void setEggs(Egg... eggs_){ eggs = eggs_; }

        public void accept_signal(String message){ /* ... */ }

        public static class Builder{
            Chicken momma;
            public Builder(){
                momma = new Chicken();
            }

            public Chicken build(Egg... eggs){
                momma.setEggs(eggs);
                return momma;
            }

            public Chicken get(){
                return momma;
            }
        }
    }

    /**
     * Signals when its ready to hatch
     * */
    public static abstract class Egg{
        private final Chicken parent;
        public Egg(Chicken parent_){
            parent = parent_;
        }

        public void hatch(){
            parent.accept_signal("chirp chirp");
        }
    }

    /**
     * The resulting children to be handled
     * */
    public static class YellowChick extends Egg{
        public YellowChick(Chicken parent_){
            super(parent_);
        }
    }

    public static class BrownChick extends Egg{
        public BrownChick(Chicken parent_){
            super(parent_);
        }
    }

    public static class UglyDuckling extends Egg{
        public UglyDuckling(Chicken parent_){
            super(parent_);
        }
    }

    public static void main (String[] args){
        Chicken.Builder mommaBuilder = new Chicken.Builder();
        Chicken momma = mommaBuilder.build(
            new YellowChick(mommaBuilder.get()), 
            new BrownChick(mommaBuilder.get()), 
            new UglyDuckling(mommaBuilder.get())
        );
        System.out.println("Success!");
    }

}

The other version aims to restrict access to the half-done Chicken class only to the relevant classes ( subclasses of Egg ) using a solution from this question:

import java.util.Objects;

public class MyClass {

    /**
     * Handles eggs
     * */
    public static class Chicken{
        private Egg[] eggs;

        private Chicken(){}
        private void setEggs(Egg... eggs_){ eggs = eggs_; }

        public void accept_signal(String message){
            /* ... */
        }

        public static class Builder{
            Chicken momma;
            public Builder(){
                momma = new Chicken();
            }

            public Chicken build(Egg... eggs){
                momma.setEggs(eggs);
                return momma;
            }

            public Chicken get(Egg.Token token){
                Objects.requireNonNull(token, "Only call this in function from a valid Builder");
                return momma;
            }
        }
    }

    /**
     * Signals when its ready to hatch
     * */
    public static abstract class Egg{
        public static class Token{ private Token(){}}
        private final Chicken parent;
        public Egg(Chicken.Builder builder){
            parent = builder.get(new Token());
        }

        public void hatch(){
            parent.accept_signal("chirp chirp");
        }
    }

    /**
     * The resulting children to be handled
     * */
    public static class YellowChick extends Egg{
        public YellowChick(Chicken.Builder builder){
            super(builder);
        }
    }

    public static class BrownChick extends Egg{
        public BrownChick(Chicken.Builder builder){
            super(builder);
        }
    }

    public static class UglyDuckling extends Egg{
        public UglyDuckling(Chicken.Builder builder){
            super(builder);
        }
    }

    public static void main (String[] args){
        Chicken.Builder mommaBuilder = new Chicken.Builder();
        Chicken momma = mommaBuilder.build(
            new YellowChick(mommaBuilder), 
            new BrownChick(mommaBuilder), 
            new UglyDuckling(mommaBuilder)
        );
        System.out.println("Success!");
    }

}

Although this still allows each chick to be built with different builder instances, and still be added under a Chicken. To patch this up, an exception can be thrown whenever a Chicken has a Chick from another nest ( Chicken.Builder ):

import java.util.HashMap;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class MyClass {

    /**
     * Handles eggs
     * */
    public static class Chicken{
        private Egg[] eggs;

        private Chicken(){}
        private void setEggs(Egg... eggs_){ eggs = eggs_; }

        public void accept_signal(String message){
            /* ... */
        }

        public static class Builder{
            Chicken momma;
            HashSet<Integer> accessors;
            public Builder(){
                momma = new Chicken();
                accessors = new HashSet<>();
            }

            public Chicken build(Egg... eggs){
                for(Egg egg : eggs){
                    if(!accessors.contains(egg.getToken()))
                        throw new IllegalStateException("Unable to accept step-chick!");
                }
                momma.setEggs(eggs);
                return momma;
            }

            public Chicken get(Egg.Token token){
                Objects.requireNonNull(token, "Only call this in function from a valid Builder");
                accessors.add(token.hashCode());
                return momma;
            }
        }
    }

    /**
     * Signals when its ready to hatch
     * */
    public static abstract class Egg{
        public static class Token{ private Token(){}}
        private final Chicken parent;
        private final Token token = new Token();
        public Egg(Chicken.Builder builder){
            parent = builder.get(token);
        }
        public int getToken(){return token.hashCode();}
        public void hatch(){
            parent.accept_signal("chirp chirp");
        }
    }

    /**
     * The resulting children to be handled
     * */
    public static class YellowChick extends Egg{
        public YellowChick(Chicken.Builder builder){
            super(builder);
        }
    }

    public static class BrownChick extends Egg{
        public BrownChick(Chicken.Builder builder){
            super(builder);
        }
    }

    public static class UglyDuckling extends Egg{
        public UglyDuckling(Chicken.Builder builder){
            super(builder);
        }
    }

    public static void main (String[] args){
        Chicken.Builder mommaBuilder = new Chicken.Builder();
        Chicken momma = mommaBuilder.build(
            new YellowChick(mommaBuilder),
            new BrownChick(mommaBuilder),
            new UglyDuckling(mommaBuilder)
        );
        System.out.println("Success!");
    }

}

Dávid Tóth
  • 2,788
  • 1
  • 21
  • 46