5

A container may contain bikes and chairs, both belonging to a person. I would like to check, if the container contains either bikes or chairs of said person. Is this possible without using instanceof?

public class Container {

    public Map<Person, List<Item>> items = new HashMap<>();

    public void add(Person p, Item item) {
        items.get(p).add(item);
    }

    public boolean containsChair(Person owner) {
        for(Item i : items.get(owner)) {
            if(i instanceof Chair) {
                return true;
            }
        }
        return false;
    }

    public boolean containsBike(Person owner) {
        for(Item i : items.get(owner)) {
            if(i instanceof Bike) {
                return true;
            }
        }
        return false;
    }
}

For the purpose of illustration, Item, Bike, Chair, Person are all simplest class stubs:

public class Person { public String name; }
public abstract class Item {}
public class Bike extends Item { public Wheel[] wheels;}
public class Chair extends Item { public Leg[] legs;}
public class Wheel {}
public class Leg {}

In the runner, a Person should be able to add Chairs and Bikes to its container:

import java.util.ArrayList;

public class Runner {
    public static void main(String[] args) {
        Container c = new Container();
        Person p = new Person();

        // Prevent null pointer exception
        c.items.put(p, new ArrayList<>());

        c.add(p, new Chair());

        // True
        System.out.println(c.containsChair(p));
    }
}
TMOTTM
  • 3,286
  • 6
  • 32
  • 63
  • 1
    You can use the visitor pattern and mix it with three lists, one pair type you are searching. Check the answer in https://stackoverflow.com/questions/29458676/how-to-avoid-instanceof-when-implementing-factory-design-pattern – istovatis Oct 09 '18 at 05:55
  • There are multiple options. This thread also will help. https://stackoverflow.com/questions/2790144/avoiding-instanceof-in-java – Rans Oct 09 '18 at 05:58
  • 1
    Possible duplicate of [How to avoid 'instanceof' when implementing factory design pattern?](https://stackoverflow.com/questions/29458676/how-to-avoid-instanceof-when-implementing-factory-design-pattern) – istovatis Oct 09 '18 at 05:58
  • 1
    I suggest you to use visitor pattern, Visitor pattern's purpose with examples It is a good practise to avoide instance of – kaan bobac Oct 09 '18 at 07:37
  • 2
    Stay away from the visitor pattern as much as possible. [It may stab you in the back...](https://softwareengineering.stackexchange.com/questions/48811/what-design-patterns-are-the-worst-or-most-narrowly-defined) – Spotted Oct 09 '18 at 07:58

3 Answers3

5

You could add to class Item an abstract method ItemType getType(). ItemType would be an enum enumerating all possible item types.

public abstract class Item {
    public abstract ItemType getType();
}

public enum ItemType {
    BIKE, CHAIR;
}

Implementation of Chair:

public static class Chair extends Item {
    public Leg[] legs;
    @Override
    public ItemType getType() {
        return ItemType.CHAIR;
    }
}

Then you could define a contains method to search for a the given Person if it has an item with a certain ItemType:

public boolean contains(Person owner, ItemType itemType) {
    return items.get(owner).stream().anyMatch(item ->itemType.equals(item.getType()));
}

Or null-safe regarding the owners items list:

public boolean contains(Person owner, ItemType itemType) {
    return Optional.ofNullable(items.get(owner))
            .map(i -> i.stream().anyMatch(item -> itemType.equals(item.getType())))
            .orElse(false);
}

Usage:

public static void main(String[] args) {
    Container c = new Container();
    Person p = new Person();

    // Prevent null pointer exception
    c.items.put(p, new ArrayList<>());

    c.add(p, new Chair());

    // True
    System.out.println(c.contains(p, ItemType.CHAIR));        
}

EDIT
Following this approach there is no need for instanceof checks. The usage of instanceof can be a hint indicating that the design has some flaws.

LuCio
  • 5,055
  • 2
  • 18
  • 34
1

You can store Bike and Chair in two different datastructure.

public final class Container {
    private final Map<Person, List<Chair>> chairs = new HashMap<>();
    private final Map<Person, List<Bike>> bikes = new HashMap<>();

    public void add(Person p, Chair chair) {
        chairs.putIfAbsent(p, new ArrayList<Chair>());
        chairs.get(p).add(chair);
    }

    public void add(Person p, Bike bike) {
        bikes.putIfAbsent(p, new ArrayList<Bike>());
        bikes.get(p).add(bike);
    }

    public boolean containsChair(Person owner) {
        return chairs.getOrDefault(owner, Collections.emptyList()).size() > 0;
    }

    public boolean containsBike(Person owner) {
        return bikes.getOrDefault(owner, Collections.emptyList()).size() > 0;
    }
}

Note that I also made your instance fields private to hide the fact that data is stored in a Map and avoid the runner code to have the responsibility to instanciate an ArrayList if not existant. Both the class and its fields are also final to achieve a better immutability. Both encapsulation and immutability are considered good practices when doing OOP.

Usage

public static void main(String[] args) {
    Container c = new Container();
    Person p = new Person();
    c.add(p, new Chair());
    System.out.println(c.containsChair(p)); //true
    System.out.println(c.containsBike(p)); //false
}
Spotted
  • 4,021
  • 17
  • 33
  • I thought of this as well. But am trying to stay with one. – TMOTTM Oct 10 '18 at 19:54
  • @TMOTTM Do you really need one ? i.e. is there another method that need to use both `Chair` and `Bike` together ? If not, it's not a smell to go this way. – Spotted Oct 11 '18 at 05:16
  • I'm not sure, because then I could start building separate containers for bikes and chairs, and a container for the two containers. – TMOTTM Oct 11 '18 at 06:13
  • @TMOTTM Either stay with one container or do 2 separate generic containers (without a "super-container"). – Spotted Oct 11 '18 at 06:24
1

What I ended up doing was to add two methods to Item:

public boolean containsBike() {return false;}
public boolean containsChair() {return false;}

While this certainly could be optimized, the check is now done by calling the method of the object:

public boolean containsBike(Person p) {
        boolean hasBike = false;
        // Prevent NullPointerException
        if(containsSomethingOf(p)) {
            for(Item i : items.get(p)) {
                if(i != null) {
                    if (i.containsBike()) {
                        hasBike = true;
                    }
                }
            }
        }
        return hasTrousers;
}

I think this is what is called polymorphism.

TMOTTM
  • 3,286
  • 6
  • 32
  • 63
  • 2
    And where are these methods defined? What is the type of `g`? If these methods are defined on `Item` it*s not how it should be. – LuCio Oct 10 '18 at 20:13
  • I corrected my post, it should have been `containsBike` instead of `isBike`. @LuCio They are in `Item` and then overriden in `Bike` and `Chair`. I find it at least a bit cleaner than using `instanceof` because now the objects know more about themselves. But of course open to further suggestions. Still need to try out the case from @Spotted – TMOTTM Oct 11 '18 at 19:12
  • 1
    These methods are introducing [circular dependencies](https://stackoverflow.com/questions/3646113/circular-dependency-in-java-classes#3646139). More over the abstract [superclass](https://docs.oracle.com/javase/tutorial/java/IandI/subclasses.html) `Item` has knowledge about it's subclasses. These classes aren't [loose coupled](https://en.wikipedia.org/wiki/Loose_coupling). Thus this is not a sustainable design. Using `instanceof` is "_a hint that your design has some flaws._" ([source](https://stackoverflow.com/a/7526896/2838289)). I would reconsider the design. – LuCio Oct 11 '18 at 19:31
  • LuCio is right, now `Item` depends on its subclasses, which it's not supposed to know anything about ! You should either use @LuCio's approach or mine. – Spotted Oct 12 '18 at 09:38