使用一系列的lambda函数来定义类功能是否合适?

3
最近有个朋友和我讨论,关于使用Lambda函数定义类功能时的争议。当创建具有动态值的对象时,应该使用Lambda来传递动态值,还是在自定义子类中提供重载方法来传递动态值?
考虑以下示例,目标是创建一个带有动态文本和图标属性的自定义标签组件。此标签必须具有getText()getIcon()方法。以下是两个示例:一个使用多个Lambda,另一个定义一个子类。
Lambda 方法:
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();
    }
}

使用:

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

子类化方法:

class Label {
    private String text;
    private Image icon;

    public Label() {
        // some default
    }

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

    // simple getters
}

使用:

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

    public Image getImage() {
        return CLOCK_IMAGE;
    }
}

Label timeLabel = new TimeLabel();

考虑到内部和外部开发人员(包括 API 类)的期望、可读性和可用性,哪种方法更适合?

两种方法都是错误的。两种方法都允许属性更改(最好用currentTimeMillis()演示,而不需要Label实例注意到)。组件不仅包括属性的getter。我认为,这也应该回答你的问题... - Holger
4个回答

2

lambda表达式有其用途,但过度使用可能会导致问题。注意:当您创建lambda表达式时,必须构建一个关于您使用哪些参数的假设。例如,您无法外部定义使用受保护字段的lambda表达式。

Lambdas还带来了一些开销,包括CPU/内存和概念上的开销。它们是在运行时生成的新类,如果您进行性能分析或调试,则工具只能帮助您解决代码问题到某种程度。


3
+1表示“很可能过度使用”。这是用λ演算建模对象的方式。但是一般来说,在Java编程中,这并不是一个很好的方法。在实践中,它取决于行为参数的数量;一个是可以接受的(想想“策略模式”);当你超过一个时,你需要开始证明为什么这比子类化更好,而大多数情况下都不是。 - Brian Goetz
@Brian Goetz:实际上,拥有多个行为参数是反对子类化的一个论点,因为你最终会拥有一个 n×m 子类动物园来处理所有可能性。这里适用“优先使用组合而非继承”的规则,但这并不意味着所有组件都应该具有单个函数的粒度(可实现为 lambda 表达式)。OP 的代码是一个很好的例子。它包含了一个可变属性(基于 System.currentTimeMillis()),但没有通知机制。任何试图解决这个问题的尝试很快就会排除 lambda 表达式... - Holger

0

这两个例子并不相等。第一个例子使用了供应商,推迟了实际内容的创建,直到getter被调用,并且每次调用都会重新创建内容。在第二个例子中,抽象类只是存储数据。

哪个更好取决于您使用它的上下文。如果您希望数据不断地重新生成,则第一个示例是最好的选择。它满足大多数Solid Principles。如果您只想保存数据,则第二个示例更好。但是,第二个示例在Open/Closed原则和可能的Liskof方面存在严重问题。

最好将Label作为接口,并让具体实现根据自己的需要进行操作。但是,您确实希望将这些细节隐藏在API的用户之外。为此,您可以使用静态构造函数来实现。

E.g:

public interface Label {

  public String getText();

}

public class Labels {

   public static Label createCurrentTimeLabel(){
       return new Label{

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

       } 


   }

}

0
这里的lambda方法本质上是策略或委托模式,还允许您更喜欢组合而不是继承,这可以与依赖注入很好地配合使用(这可以帮助简化测试等等)。
我看到的其中一个缺点是,它们在这里使用的lambda可能会错过潜在的封装,您可以通过在自己的类中定义它们来实现这些封装,而不是在实例化Label的位置定义它们,这是您朋友的继承模型做得更好一些。
然而,您朋友的模型错过了您可能正在寻找的策略模式。
一个不错的折衷方案可能是放弃lambda但保留策略的想法。由于您正在消除lambda,因此可以将策略实现的定义扩展到不仅限于get()方法。但是,您必须决定是否需要为每个属性使用单独的策略。
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;
    }
}

策略模式通常用于可以由类封装并提供给某些消费者的算法,这些消费者可能不关心使用哪个算法,只要它符合特定的协议。

委托模式更简单、更自我解释。您将某些操作/控制权委托给外部实体。


0
你需要方法体的动态运行时重新配置吗?使用lambda表达式。否则,使用子类,它们具有巨大的运行时性能优势,在搜索用法时更容易(尝试找出所有可能的get()调用位置),并且更符合惯用语。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接