使用 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 中使用手工、运行时和编译时支持来使用委托模式。