3

In a recent discussion, a friend and I have disagreed over the following use of lambda functions to define class functionality. When creating an object with dynamic values, should the dynamic values be passed using lambdas, or provided using overriden methods in a custom subclass?

Consider the following example, in which the goal is to have a custom label component with dynamic text and icon traits. This label must have methods getText() and getIcon(). Following are two examples: one using multiple lambdas, and one defining a subclass.

Lambda approach:

class Label {
    private Supplier<String> text;
    private Supplier<Image> icon;

    public Label(Supplier<String> text, Supplier<Image> icon) {
        this.text = text;
        this.icon = icon;
    }

    public String getText() {
        return text.get();
    }

    public Image getIcon() {
        return icon.get();
    }
}

Use:

Label timeLabel = new Label(
    () -> System.currentTimeMillis(),
    () -> CLOCK_IMAGE
);

Subclass approach:

class Label {
    private String text;
    private Image icon;

    public Label() {
        // some default
    }

    public Label(String text, Image icon) {
        // set fields
    }

    // simple getters
}

Use:

class TimeLabel extends Label {
    public String getText() {
        return System.currentTimeMillis();
    }

    public Image getImage() {
        return CLOCK_IMAGE;
    }
}

Label timeLabel = new TimeLabel();

Which of these approaches is more appropriate, considering expectations, readability, and usability for both internal and external developers (including as a class for an API)?

FThompson
  • 28,352
  • 13
  • 60
  • 93
  • Both approaches are wrong. Both allow the properties to change (best demonstrated with `currentTimeMillis()` without the `Label` instance noticing. A component consists of more than just getters for properties. I think, that should also answer your question… – Holger Jan 18 '16 at 11:44

4 Answers4

2

Lambdas have their uses, but this is likely to be over use. Note: when you create a lambda you have to build in an assumption about which parameters you are using. You couldn't externally define a lambda which uses a protected field for example.

Lambdas also come with some overhead, both in terms of CPU/memory and conceptual overhead. They are a new class generated at runtime and if you are profiling or debugging it the tools can only help you untangle the code so much.

Peter Lawrey
  • 525,659
  • 79
  • 751
  • 1,130
  • 3
    +1 to "likely to be overuse". This is how objects are modeled with the lambda calculus. But in general, its not a very good way to program in Java. In practice, it depends on the number of behavioral parameters; one is fine (think "strategy pattern"); when you get above one, you need to start justifying why this is better than subclassing, and most of the time, it's not. – Brian Goetz Jan 16 '16 at 18:51
  • @Brian Goetz: actually, having more than behavioral parameter is an argument *against* subclassing as you would end up having a zoo of `n×m` subclasses for all possibilities then. The rule “prefer composition over inheritance” applies here, however, that doesn’t imply that all components should have the granularity of single functions (implementable as lambda expression). The OP’s code is a good example. It incorporates a mutable property (based on `System.currentTimeMillis()`) but no notification mechanism. Any attempt to solve this will soon rule out lambda expressions… – Holger Jan 18 '16 at 11:51
0

These two examples are not equal. The first example uses suppliers which defer the creation of the actual content until either getter is invoked and will repeatedly create the content anew at each invocation. In the second example the abstract class just stores the data.

Which is better depends on the context you are using it in. If you want your data to be continuously generated anew then the first example is the best. It satisfies most of the Solid Principles. If you want to just hold the data, then the second example is better. However the second example has serious problems with the Open/Closed principle and possibly Liskof.

You would be better off by making Label an interface and let the specific implementations do what is best for them. You do want to hide these details from the users of your API though. Do do that you could behind a facade with static constructors.

E.g:

public interface Label {

  public String getText();

}

public class Labels {

   public static Label createCurrentTimeLabel(){
       return new Label{

          public String getText() {
            return System.currentTimeMillis();
          }

       } 


   }

}
M.P. Korstanje
  • 10,426
  • 3
  • 36
  • 58
0

Do you need dynamic runtime reconfiguration of the method's bodies? Use lambdas. Otherwise, use subclasses, which have tremendous runtime peformance benefits, are easier when searching for usages (have fun finding out all the possible invocation locations of get()) and much more idiomatic.

Tassos Bassoukos
  • 16,017
  • 2
  • 36
  • 40
0

The lambda approach here is essentially the Strategy or Delegate pattern, and also allows you to prefer composition over inheritance which can play nicely with dependency injection (which can help make testing easier, etc. etc.).

One of the downsides I see is that lambdas as they're used here can miss out on potential encapsulation that you could achieve by defining these in their own classes instead of defining them where the Label is instantiated, which is what your friend's inheritance model does a little better.

However, your friend's model misses out on the strategy pattern which you might be going for.

A nice compromise might be to forget the lambdas but keep the idea of the Strategy. And since you're eliminating the lambdas, you can expand the definition of your Strategy implementations beyond just the get() method. But you have to decide whether you need individual strategies for each property.

class Label {
    private LabelSupplier supplier;

    public Label(LabelSupplier supplier) {
        this.supplier = supplier;
    }

    public String getText() {
        return supplier.getText();
    }

    public Image getIcon() {
        return supplier.getIcon();
    }
}

interface LabelSupplier {
    String getText();
    Image getIcon();
}

class TimeLabelSupplier implements LabelSupplier {
    public String getText() {
        return System.currentTimeMillis();
    }

    public Image getImage() {
        return CLOCK_IMAGE;
    }
}

The Strategy pattern is usually used for algorithms that can be encapsulated by classes and provided to some consumer that may not care which algorithm is used so long as it conforms to a particular protocol.

The Delegate pattern is simpler and more self-explanatory. You're deferring some actions/control to some outside entity.

drhr
  • 2,261
  • 2
  • 17
  • 35