在 Groovy™ 中使用委托模式

作者: Paul King

发布时间:2024-01-28 08:08PM


委托设计模式用于一个类将其部分功能委托给一个或多个辅助对象(称为委托)的情况。它通常用作面向对象继承机制的替代方案,后者允许从超类派生行为。当以这种方式使用时,通常被称为组合优于继承

案例研究

作为案例研究,让我们考虑编写一些代码来跟踪您最喜欢的餐厅或外卖食品店中的菜单项。我们将使用一个 MenuItem 记录来捕获我们可以订购的项目的名称和价格

record MenuItem(String name, int price) { }

对于非常简单的应用程序,这可能就足够了。我们可以简单地使用其他集合类型,如集合、映射或列表的 MenuItem 实例。对于我们的第一个示例,我们将只使用一个列表

var spanishTapas = [
    new MenuItem('Gambas al ajillo', 8),
    new MenuItem('Tortilla de patatas', 6),
    new MenuItem('Calamares a la romana', 7)
]

assert spanishTapas.size() == 3
assert spanishTapas[0].price == 8
assert spanishTapas[-1].name.startsWith('Calamares')
assert spanishTapas.every{ it.price < 10 }

如果我们想构建更复杂的应用程序,我们很快就会发现拥有一个 Menu 类来体现附加功能很有用。让我们看看创建此类类的替代方法。

使用显式“手动”委托

这是各种设计模式指南中经常建议的方法,这些指南讨论了委托模式。我们有一个(或多个)辅助对象,称为委托。对于我们正在定义的类中的许多方法,我们只需调用委托的相应方法。

我们将声明我们的委托辅助器,在本例中是一个列表。我们将定义 addgetAtsize 方法,它们只是将参数传递给委托的同名方法。我们还将添加一个 findByPrice 方法,其中包含少量自己的业务逻辑,但主要使用委托的底层方法。

以下是该类的样子

class Menu {
    private ArrayList<MenuItem> delegate = []

    boolean add(MenuItem newItem) {
        delegate.add(newItem)
    }

    MenuItem getAt(int index) {
        delegate[index]
    }

    int size() {
        delegate.size()
    }

    MenuItem findByPrice(int price) {
        delegate.find{ it.price == price }
    }
}

以下是我们如何使用该类

def vietnamese = new Menu().tap {
    add(new MenuItem('Phở', 7))
    add(new MenuItem('Bánh Mì', 5))
}

assert vietnamese[0].price == 7
assert vietnamese.size() == 2
assert vietnamese.findByPrice(7).name == 'Phở'

这个类不难理解,但如果我们要向 Menu 类中添加更多类似列表的功能,我们会看到更多重复的样板代码。随着类的变大,也很难看出我们主要使用委托模式,只有少数方法(可能)会添加自己的业务逻辑。

此时,我们可能会质疑从继承转向组合/委托是否是一个好主意。例如,从 ArrayList 类扩展将自动派生我们可能感兴趣的许多方法,并且我们将消除一些样板委托方法。但是,如果 Menu 并不是仅仅是一个列表,而是具有附加功能,那么试图将其强行纳入 List 继承层次结构通常不会有好结果。此外,如果事实证明我们需要委托给多个辅助对象,那么继承提供的单一父类模型(Groovy 在这里遵循 Java,只允许一个父类)使得无法使用这种方法。

在研究 Groovy 对委托的特殊支持(它克服了样板代码爆炸的问题)之前,让我们看看 Groovy 对特质的支持。特质是一种克服从单一父超类继承行为限制的机制。

使用特质

Groovy 特质提供了一种强大的行为继承机制。与从单一超类扩展不同,您可以实现多个特质并从多个位置派生行为。已经制定了规则和约定来克服菱形继承问题。特质有点像 Java 的带默认方法的接口,但特质有一些额外的功能,特别是,我们将使用允许继承状态而非仅仅行为的有状态特质。

在我们的案例研究中,现在假设我们希望增强 Menu 类,使其也具有日期的概念。我们可能在每周的不同日期有不同的菜单,或者我们可能随着季节性食材的变化而更新菜单,或者为特殊的庆祝日提供特殊菜单。

我们将探索两种特质,一种用于类似列表的行为,一种用于类似日期的行为。我们将首先研究类似列表的行为,为了简单起见,我们将遵循与上述显式委托示例非常相似的方法,但会添加几个额外的方法。

以下是我们的特质可能的样子

trait HasList {
    List<MenuItem> listDelegate = []
    boolean add(MenuItem item) { listDelegate.add(item) }
    MenuItem getAt(int index) { listDelegate[index] }
    boolean any(Closure predicate) { listDelegate.any(predicate) }
    boolean contains(MenuItem item) { listDelegate.contains(item) }
}

接下来是我们的日期行为特质。为了简单起见,让我们从一个简单的 isBefore 方法开始,它允许我们检查一个菜单的预期日期是否早于另一个菜单的日期。这只是一个直接映射到我们的 LocalDate 委托提供的方法之一的示例。

以下是我们的特质可能的样子

trait HasDate {
    LocalDate dateDelegate
    boolean isBefore(LocalDate other) { dateDelegate.isBefore(other) }
}

现在我们可以使用这两个特质创建一个类

class Menu implements HasList, HasDate {
    Menu(LocalDate date) { dateDelegate = date }
}

与显式委托示例相比,有几个优点

  • 我们正在使用委托模式变得清晰

  • 如果我们要添加一些方法,比如我们之前使用的 findByPrice,那么很明显,这样的方法是可以在委托模式之外添加额外业务逻辑的地方

  • 如果我们在其他场景中需要类似列表或类似日期的行为,我们现在有一些通用的特质可以重用

以下是我们如何使用我们的新类

def italianWednesday = new Menu(LocalDate.of(2024, 1, 24)).tap {
    add(new MenuItem('Spaghetti Bolognese', 10))
    add(new MenuItem('Gnocchi di Patate', 11))
    add(new MenuItem('Tiramisù', 9))
}

def italianThursday = new Menu(LocalDate.of(2024, 1, 25)).tap {
    add(new MenuItem('Fettuccine al Pomodoro', 12))
    add(new MenuItem('Pizza Margherita', 10))
    add(new MenuItem('Pannacotta', 10))
}

assert italianWednesday[0].price == 10
assert !italianWednesday.any{ italianThursday.contains(it) }
assert italianWednesday.isBefore(italianThursday.dateDelegate)

这里我们正在进行与之前看到的类似的价格检查,然后由于我们可能想要强调多样性,我们正在检查周三和周四的菜单项是否重叠,然后我们正在检查第一个菜单的日期是否早于第二个菜单。

使用动态语言特性

Groovy 有几个动态特性可以促进委托。 Groovy 文档展示了一种使用 Groovy 的 ExpandoMetaClass 的方法。在这里,我们将展示一种利用 Groovy 的 methodMissing 钩子来实现的方法

class Menu {
    private ArrayList<MenuItem> delegate = []

    def findByPrice(int price) {
        delegate.find{ it.price == price }
    }

    def methodMissing(String name, args) {
        delegate."$name"(*args)
    }
}

这里我们有一个显式方法 findByPrice,我们无法直接委托。所有其他方法调用都被拦截并导向我们的委托。

以下是我们如何使用该类

def frenchBakery = new Menu().tap {
    add(new MenuItem('Croissant', 4))
    add(new MenuItem('Baguette', 5))
}

assert frenchBakery[0].price == 4
assert frenchBakery.size() == 2
assert frenchBakery.findByPrice(4).name == 'Croissant'

通常,如果我们需要委托属性(考虑 getter),我们可以扩展我们的类以利用 propertyMissing 方法。也可以委托给多个委托,但我们 methodMissing 方法中的逻辑会变得稍微复杂一些。

这种动态方法的一个区别是,委托方法不会出现在我们的 Menu 类文件中,因为它们是在运行时发现的。如果使用委托类的新版本,这可能会使我们的生活稍微轻松一些,我们将自动委托给任何新方法。同样,如果我们在运行时向委托添加方法,这种方法可以很乐意地委托给这些方法。然而,它的缺点是委托方法不会出现在我们的 Menu 类文件中,这意味着它们不会出现在我们的 Groovydoc 中,并且根据我们的工具的智能程度,我们可能会有不太理想的 IDE 补全。

接下来我们将研究一种克服这些缺点的方法。

使用 @Delegate 转换

Groovy 还通过 @Delegate 转换提供编译时委托支持。这里我们用 @Delegate 注解来标注两个属性(我们的委托)。

@TupleConstructor(includes='date')
class Menu {
    @Delegate
    final ArrayList<MenuItem> items = []
    @Delegate
    final LocalDate date
}

这会自动添加样板方法,类似于我们之前在显式委托中为两个类中的每个公共方法所看到的(总共约 120 个方法)。此外,我们的委托实现的任何接口也会自动添加到我们的 Menu 类的 implements 子句中。

以下是使用方法

def bistroTuesday = new Menu(LocalDate.of(2024, 1, 16)).tap {
    add(new MenuItem('Tacos', 12))
    add(new MenuItem('Chicken Parma', 15))
}
def bistroFriday = new Menu(LocalDate.of(2024, 1, 19)).tap {
    add(new MenuItem('Chicken Parma', 15))
    add(new MenuItem('Fish & Chips', 12))
}

assert bistroTuesday.any{ bistroFriday.contains(it) }
assert bistroTuesday.isBefore(bistroFriday)
assert bistroTuesday instanceof List
assert bistroFriday instanceof ChronoLocalDate

这里我们正在检查至少一个项目是否出现在两个菜单上,并且周二的菜单是否早于周五的菜单。

如果标准委托选项不是我们需要的,我们可以通过使用注解属性来自定义为我们生成的代码。例如,如果我们不需要实现所有的列表和日期方法,我们可以通过使用 includes 注解属性,只委托我们感兴趣的方法。这样就只带来了感兴趣方法的委托样板代码。在这种情况下,我们还需要禁用委托接口的自动收集,因为我们不再实现接口中列出的所有方法。我们通过使用 interfaces 注解属性来做到这一点。

以下是我们如何编写我们的类(仅用于列表状功能)

class Menu {
    @Delegate(includes='add,forEach,get,size', interfaces=false)
    final ArrayList<MenuItem> items = []
}

以下是使用方法

def japanese = new Menu().tap {
    add(new MenuItem('Sushi', 8))
    add(new MenuItem('Vegetarian Ramen', 12))
    add(new MenuItem('Vegetable Gyoza', 12))
    add(new MenuItem('Teriyaki Tofu', 12))
}

assert japanese.size() == 4
japanese.forEach{ it.price % 4 == 0 }
assert japanese.get(3).price == 12
assert japanese !instanceof List

我们可以看到该类确实具有感兴趣的方法,因为我们在示例中使用了这些方法,并且它没有实现 List 接口,如最后的断言所示。

Groovy 对委托模式的使用

Groovy 还在许多地方内部使用委托模式,包括闭包。您通常不会在普通代码中这样做,但您可以像此示例所示设置和更改闭包的委托

var sizeClosure = { size() }
sizeClosure.delegate = 5..6
assert sizeClosure() == 2
sizeClosure.delegate = 'foo'
assert sizeClosure() == 3

虽然这个例子看起来可能没什么用,但这项技术是构建器和其他嵌套闭包在底层运行的基础。

这是另一个涉及使用 ExpandoMetaClass 动态添加几个方法到整数的示例

Integer.metaClass {
    twice { multiply(2) }     // (1)
    thrice { delegate * 3 }   // (2)
}
assert 3.twice() == 2.thrice()
  1. 隐式

  2. 显式

结论

我们已经了解了如何在 Groovy 中手动使用委托模式,以及如何使用 Groovy 特殊的运行时和编译时支持。