Reputation: 406
I've been looking at the Supplier and Consumer interfaces of Java 8 and of what I understand it can replace constructor call.
I've seen an example on dzone (link here) where a ShapeFactory is used.
The code is pretty simple, a simple factory for a shape class.
But she uses it as so :
Supplier<ShapeFactory> supplier = ShapeFactory::new;
supplier.get().getShape("rectangle").draw();
But why is it better than doing a simple classical :
ShapeFactory factory = new ShapeFactory();
factory.getShape("rectangle").draw()
This being much simpler and efficient enough. Besides, if the constructor of the ShapeFactory class has argument, Supplier will not work and we will have to use Function or other methods.
So why use the Supplier like this in this precise case ?
Upvotes: 8
Views: 4589
Reputation: 131316
TLDR
Using a Supplier
brings better readability/maintainability than explicit constructor invocation such as the new MyObject()
idiom handled by switch
/if-else if
statements.
It also brings a better type safety that instantiation by reflection, an alternative to the new MyObject()
idiom, often used before Java 8 to address the maintainability concern but that introduces other issues.
Instantiating a Factory
class is not necessarily the best example to illustrate benefits of Supplier
.
So, suppose we want to instantiate Shape
classes from a Factory
class.
A factory of Shape
should instantiate different kind of Shape
(subclasses of it).
1) Without Supplier
and with a new MyShape()
idiom, you will finish with a method that contains some if-else if
/switch
statements that checks a kind of criteria/parameter and instanties the expected class according to this criteria/parameter.
For example :
public static Shape createShape(String criteria) {
if (criteria.equals("circle")){
return new Circle();
}
else if (criteria.equals("square")){
return new Square();
}
...
}
It is bad because when you add a class to handle by the method, you have to change this method with a new if-else if
/switch
so that it takes it in consideration.
It results to a not maintainable code where you may fast create side effects.
2) To avoid this problem, we use often reflection with Class.newInstance()
.
It eliminate the if-else if
/switch
problem but it creates often others as reflection may not work (security issue, class not instantiable, etc...) and you will know it only at runtime.
It results still to a brittle code.
Here are the reasons to favor the Supplier
:
By providing a Supplier
, the check is performed at compile time : if the class is not instantiable, the compiler will emit an error.
Besides, the method that uses/accepts a Supplier<Shape>
doesn't need to use if-else if
/switch
statements.
When you use Supplier
, you encounter generally two cases (not mutually exclusive of course) :
the Supplier<Shape>
objects are instantiated by the factory class.
We can so for example use in the factory a Map
that stores the Supplier<Shape>
instances and
modifying the code to add/remove elements in the map is really cleaner as adding a new branch to a if-else if
/switch
statement as it is much less verbose and the way to change the map populating (add a map.put()
or remove map.put()
statement) is less prone to create side effects.
the Supplier<Shape>
objects are instantiated and provided by the client class.
In this case, the factory doesn't even need to change.
So the map is so even not required.
And from the client side, while the client provides a valid Supplier<Shape>
parameter, it is fine.
Type safety, maintainable code : as you may notice, these two ways using the Supplier<Shape>
address completely drawbacks of new MyShape()
and instantiation by reflection idiom.
I will give two examples to illustrate these two ways.
Example where the Shape Supplier
s are created in the factory :
public class SimpleShapeFactory {
private static Map<String, Supplier<Shape>> shapesByCriteria = new HashMap<>();
static {
shapesByCriteria.put("square", Square::new);
shapesByCriteria.put("circle", Circle::new);
}
public static Shape createShape(String criteria) {
return shapesByCriteria.get(criteria).get();
}
}
The client may invoke it in this way :
Shape square = SimpleShapeFactory.createShape("square");
Shape circle = SimpleShapeFactory.createShape("circle");
This code will not fail at runtime because of the instantiation of Square
or Circle
as it is checked at compile time.
And the task for instantiating Shape
instances are at a same place and easy to change :
static {
shapesByCriteria.put("square", Square::new);
shapesByCriteria.put("circle", Circle::new);
}
Example where the Shape Supplier
s are provided by the client:
public class ComplexShapeFactory {
public static Shape composeComplexShape(List<Supplier<Shape>> suppliers) {
Shape shape = suppliers.get(0);
for (int i = 1; i < suppliers.size() - 1; i++) {
shape = shape.compose(suppliers.get(i + 1).get());
}
return shape;
}
}
The client may create complex shapes by chaining Supplier
in this way as it invokes the method:
Shape squareWithTwoCircles = ComplexShapeFactory.composeComplexShape(Arrays.asList(Square::new, Circle::new, Circle::new));
The check is still done at compile time and as the supplier is provided by the client, the client can add new class of Shape
without making the factory change.
Upvotes: 9
Reputation: 27525
In the example above, it makes no sense to complicate things with the supplier.
A simple constructor call is even easier to read (and is likely to be faster).
Probably, the author of this examle intended to demonstrate how a Supplier
can be invoked.
Suppliers become useful when you need to extract the retrieval (which is not necessarily creation) of an object, kind of "strategy pattern". E.g.:
public void drawRectangle(final Supplier<ShapeFactory> factorySupplier) {
final ShapeFactory factory = factorySupplier.get();
factory.getShape("rectangle").draw();
}
Then, you could call your method like this, and it would create a new factory:
drawRectangle(ShapeFactory::new);
or for example like this:
@Autowired
private ShapeFactory shapeFactoryBean;
...
drawRectangle(() -> shapeFactoryBean);
and it would use an existing factory instead of creating a new one.
Of course, the method from my example could just take a ShapeFactory
as an argument. But one could imagine a situation that, for example, the factory only needs to be accessed in certain situations, so there's no need to create a new factory for the method call, e.g.:
public void drawRectangleConditionally(final Supplier<ShapeFactory> factorySupplier) {
if (something) {
final ShapeFactory factory = factorySupplier.get();
factory.getShape("rectangle").draw();
}
}
...
drawRectangleConditionally(ShapeFactory::new);
// the factory will only be created if it's really needed.
Note that a supplier can be used multiple times, so another use of suppliers is to get a sequence of objects, e.g.:
public List<T> createList(final int n, final Supplier<T> listItemSupplier) {
final List<T> result = new ArrayList<>(n);
for (int i = 0; i < n; ++i) {
result.add(listItemSupplier.get());
}
return result;
}
...
createList(5, MyObject::new); // creates a list of 5 MyObjects
createList(3, () -> "hello"); // creates a list of 3 "hello" strings
createList(10, Random::nextLong); // creates a list of 10 random longs
Upvotes: 7
Reputation: 14999
I don't find this to be a very good example of the factory pattern. I would suggest something like
// "old code"
interface ShapeFactory {
Shape create();
}
class CircleFactory implements ShapeFactory {
Shape create() { return new Circle(); }
}
class CircleFactory implements ShapeFactory {
Shape create() { return new Circle(); }
}
class ShapeDrawer {
ShapeFactory factory;
public void setFactory(ShapeFactory f) { factory = f; }
public void draw() {
factory.create().draw();
}
}
// called with
shapeDrawer.setFactory(new CircleFactory());
shapeDrawer.draw();
shapeDrawer.setFactory(new RectangleFactory());
shapeDrawer.draw();
// with Supplier
class ShapeDrawer {
Supplier<Shape> factory;
public void setFactory(Supplier<Shape> f) { factory = f; }
public void draw() {
factory.get().draw();
}
}
// called like this
shapeFactory.setFactory(Circle::new);
shapeDrawer.draw();
shapeFactory.setFactory(Rectangle::new);
shapeDrawer.draw();
So, there's a lot less boilerplate code around the use of the factory pattern.
Upvotes: 1
Reputation: 70899
A lot of times, a person puts in a lot of effort to learn the new technology. Then they become heavily invested in the new technology, and start to use it preferentially over any other approach.
There's a variant of an old saying "When you have a hammer ever job looks like a nail".
I've seen Streams used when a simple for loop would have been easier to read and maintain, due to an attempt to embed flitering and switch-like logic in the stream. I've seen for loops that were simple enough that they read better as streams.
In my estimation, this person learned Streams really well, well enough to blend the construction paradigm into their Streams in a stream-like manner. I don't think it was a wise choice, but I would guess that if you asked them about it, their response might be "go learn streams".
Upvotes: 4