在上一部分中 ,我开始研究传统的“四人一组”(GoF)设计模式(请参阅参考资料 )和功能更多的方法的交叉点。 在本期文章中,我将继续这一旅程,以三种不同的范式展示一个常见问题的解决方案:模式,元编程和函数组合。
如果您的语言支持的主要范例是对象,那么就很容易开始考虑这些术语中每个问题的解决方案。 但是,大多数现代语言都是multiparadigm ,这意味着它们支持对象,元对象,功能和其他范式。 学会使用不同的范式来解决合适的问题是成为更好的开发人员的过程的一部分。
在本期中,我将攻击由Adapter设计模式解决的传统问题:转换接口以使其与不兼容的接口一起使用。 首先,用Java编写的传统方法。
Java中的适配器
适配器模式将类的一个接口转换为兼容的接口。 当两个类在概念上可以一起工作但由于实现细节而不能一起工作时,将使用它。 在此示例中,我创建了一些简单的类来对将方形钉插入圆Kong中的问题进行建模。 如图1所示,方形钉有时会插入圆Kong中,具体取决于钉和Kong的相对大小:
图1.圆Kong中的方形钉
为了确定正方形是否适合圆形,我使用图2所示的公式:
图2.确定正方形是否适合圆形的公式
图2中的公式将正方形的边之一的宽度除以2,将其平方,然后将结果乘以2,然后返回平方根。 如果此值小于圆的半径,则将固定栓钉。
我可以使用处理转换的简单实用程序类来简单地解决方钉/圆Kong问题。 但这是一个更大的问题。 例如,如果我将一个Button调整为适合尚未设计的面板类型,该怎么办才能兼容呢? 方钉和圆Kong的问题是对Adapter设计模式所解决的一般问题的一种简便简化:适配两个不兼容的接口。 为了使方钉能够与圆Kong配合使用,我需要一些类和接口来实现Adapter模式,如清单1所示:
清单1. Java中的方钉和圆Kong
public class SquarePeg {
private int width;
public SquarePeg(int width) {
this.width = width;
}
public int getWidth() {
return width;
}
}
public interface Circularity {
public double getRadius();
}
public class RoundPeg implements Circularity {
private double radius;
public double getRadius() {
return radius;
}
public RoundPeg(int radius) {
this.radius = radius;
}
}
public class RoundHole {
private double radius;
public RoundHole(double radius) {
this.radius = radius;
}
public boolean pegFits(Circularity peg) {
return peg.getRadius() <= radius;
}
}
为了减少Java代码的数量,我添加了一个名为Circularity
的接口,以指示实现者具有一定的半径。 这使我不仅可以使用RoundPeg
,还可以使用RoundPeg
来编写RoundHole
代码。 这是Adapter模式中的一个常见让步,可以简化类型解析。
为了将方形钉安装到圆Kong中,我需要一个适配器,该适配器通过暴露getRadius()
方法来向SquarePeg
添加Circularity
SquarePeg
,如清单2所示:
清单2.方钉适配器
public class SquarePegAdaptor implements Circularity {
private SquarePeg peg;
public SquarePegAdaptor(SquarePeg peg) {
this.peg = peg;
}
public double getRadius() {
return Math.sqrt(Math.pow((peg.getWidth()/2), 2) * 2);
}
}
为了测试我的适配器是否确实允许我在圆Kong中安装适当大小的方形钉,我实现了清单3中所示的测试:
清单3.测试适应
@Test
public void square_pegs_in_round_holes() {
RoundHole hole = new RoundHole(4.0);
Circularity peg;
for (int i = 3; i <= 10; i++) {
peg = new SquarePegAdaptor(new SquarePeg(i));
if (i < 6)
assertTrue(hole.pegFits(peg));
else
assertFalse(hole.pegFits(peg));
}
}
在清单3 ,对于每一个所提出的宽度的,我包裹SquarePegAdaptor
周围创建的SquarePeg
,使hole
的pegFits()
方法返回的智能评价作为对销的健身。
这段代码很简单,因为这是在Java中实现的简单而冗长的模式。 这种范例显然是GoF设计模式的方法。 但是,模式方法不是唯一的方法。
Groovy中的动态适配器
Groovy(请参阅参考资料 )支持Java不支持的几种编程范例,因此我将在其余示例中使用它。 首先,我将实现清单2中的“标准”适配器模式解决方案, 将其移植到Groovy,如清单4所示:
清单4. Groovy中的钉,Kong和适配器
class SquarePeg {
def width
}
class RoundPeg {
def radius
}
class RoundHole {
def radius
def pegFits(peg) {
peg.radius <= radius
}
}
class SquarePegAdapter {
def peg
def getRadius() {
Math.sqrt(((peg.width/2) ** 2)*2)
}
}
清单2中的Java版本和清单4中的Groovy版本之间最明显的区别是冗长。 Groovy旨在通过动态类型化和便利性来消除Java的某些重复性,例如使方法的最后一行自动用作方法的返回值,如getRadius()
方法所示。
清单5中显示了适配器的Groovy版本的测试:
清单5.在Groovy中测试传统适配器
@Test void pegs_and_holes() {
def hole = new RoundHole(radius:4.0)
(4..7).each { w ->
def peg = new SquarePegAdapter(
peg:new SquarePeg(width:w))
if (w < 6 )
assertTrue hole.pegFits(peg)
else
assertFalse hole.pegFits(peg)
}
}
在清单5中 ,我利用了Groovy的另一种便利,调用了Groovy在构造RoundHole
, SquarePegAdaptor
和SquarePeg
时自动生成的名称/值构造函数。
尽管有语法上的限制,但该版本与Java版本类似,并且遵循GoF设计模式范例。 对于来自Java背景的Groovy开发人员来说,将他们的旧经验移植到新语法中是很常见的。 但是,Groovy使用元编程可以更优雅地解决此问题。
使用元编程进行适应
Groovy的突出特点之一是它对元编程的强大支持。 我将使用它通过ExpandoMetaClass
将适配器直接构建到类中。
ExpandoMetaClass
动态语言的一个共同特征是开放类 :重新打开现有类(您的类或系统类,例如String
或Object
)以添加,删除或更改方法的能力。 开放类经常用于特定领域的语言(DSL)和建立流畅的接口。 Groovy中有公开课两种机制: 类和ExpandoMetaClass
。 我的示例仅显示expando语法。
ExpandoMetaClass
使您可以向类或单个对象实例添加新方法。 在调整的情况下,我需要“radiusness”添加到我的SquarePeg
之前,我可以检查,看它是否将适合的圆Kong,如清单6所示:
清单6.使用ExpandoMetaClass
向方钉添加半径
static {
SquarePeg.metaClass.getRadius = { ->
Math.sqrt(((delegate.width/2) ** 2)*2)
}
}
@Test void expando_adapter() {
def hole = new RoundHole(radius:4.0)
(4..7).each { w ->
def peg = new SquarePeg(width:w)
if (w < 6)
assertTrue hole.pegFits(peg)
else
assertFalse hole.pegFits(peg)
}
}
Groovy中的每个类都有一个预定义的metaClass
属性,公开该类的ExpandoMetaClass
。 在清单6中 ,我使用该属性使用熟悉的公式向SquarePeg
类添加getRadius()
方法。 使用ExpandoMetaClass
时,计时很重要; 在尝试在单元测试中调用该方法之前,必须确保已添加该方法。 因此,我在测试类的静态初始化程序中添加了新方法,该方法在加载测试类时将方法添加到SquarePeg
中。 将getRadius()
方法添加到SquarePeg
,我可以将其传递到hole.pegFits
方法中,Groovy的动态类型将处理其余部分。
当然,使用ExpandoMetaClass
比使用更长的模式版本更简洁。 而且它几乎是看不见的-这也是它的缺点之一。 应该谨慎地向现有类中添加批发方法,因为您将便利性换成了不可见的行为,而这种行为很难调试。 在某些情况下(例如在DSL中)以及代表框架对现有基础架构进行的广泛更改,这是可以接受的。
此示例说明了使用元编程范例(修改现有类)来解决适配器问题。 但是,这并不是使用Groovy的动态性解决此问题的唯一方法。
动态适配器
Groovy经过优化,可以与Java完美集成,包括Java相对严格的地方。 例如,在Java中动态生成类很麻烦,但是Groovy可以轻松地处理它。 这表明我可以即时生成一个适配器类,如清单7所示:
清单7.使用动态适配器
def roundPegOf(squarePeg) {
[getRadius:{Math.sqrt(
((squarePeg.width/2) ** 2)*2)}] as RoundThing
}
@Test void functional_adaptor() {
def hole = new RoundHole(radius:4.0)
(4..7).each { w ->
def peg = roundPegOf(new SquarePeg(width:w))
if (w < 6)
assertTrue hole.pegFits(peg)
else
assertFalse hole.pegFits(peg)
}
}
Groovy的文字哈希语法使用方括号,这些方括号出现在清单7的roundPegOf()
方法内。 为了生成实现接口的类,Groovy允许您创建一个哈希,将方法名称作为键,并将实现代码块作为值。 as
运算符使用哈希值生成实现该接口的类,并使用哈希值的键名生成实例方法。 因此,在清单7中 , roundPegOf()
方法创建了一个带有getRadius
作为方法名称的单项哈希(Groovy的哈希键在为字符串时不需要使用双引号)和我熟悉的转换代码作为实现。 as
运算符将其转换为实现RoundThing
接口的类,充当在functional_adaptor()
测试中包裹SquarePeg
创建的适配器。
动态生成类的能力消除了传统模式方法的许多冗长和形式化。 它也比元编程方法更明确:我没有在类中添加新方法; 我正在为调整目的而生成即时包装。 这使用了设计模式范例(添加了适配器类),但是操作和语法都很少。
功能适配器
当您拥有的只是一把锤子时,每个问题都像钉子一样。 如果您唯一的范例是面向对象,则可能会失去查看替代可能性的能力。 在没有一流功能的情况下在语言上花费太多时间的危险之一是过度使用模式来解决问题。 许多模式(观察者,访客和命令,仅举几个例子)都是应用可移植代码的核心机制,这些代码以缺乏高阶功能的语言实现。 我可以丢弃很多对象陷阱,而只需编写一个函数来处理转换。 事实证明,这种方法具有一些优势。
功能!
如果您具有一流的功能(这些功能可以出现在任何其他语言构造可以出现的任何地方,包括外部类),那么您可以编写一个转换函数来为您处理适应性,如清单8中的Groovy代码所示:
清单8.使用一个简单的转换函数
def pegFits(peg, hole) {
Math.sqrt(((peg.width/2) ** 2)*2) <= hole.radius
}
@Test void functional_all_the_way() {
def hole = new RoundHole(radius:4.0)
(4..7).each { w ->
def peg = new SquarePeg(width:w)
if (w < 6)
assertTrue pegFits(peg, hole)
else
assertFalse pegFits(peg, hole)
}
}
在清单8中 ,我创建了一个接受peg
和hole
的函数,并使用它来检查桩钉的适用性。 该方法可行,但是它从面向对象认为属于的Kong中删除了有关适应性的决定。 在某些情况下,将决策外部化而不是调整类可能是有意义的。 这代表了功能范例:接受参数并返回结果的纯函数。
组成
在离开功能性方法之前,我将展示我最喜欢的适配器,该适配器将设计模式和功能性方法结合在一起。 为了说明使用轻量级动态生成器作为一等函数提供的优点,请考虑清单9中的示例:
清单9.通过轻量级动态适配器组成函数
class CubeThing {
def x, y, z
}
def asSquare(peg) {
[getWidth:{peg.x}] as SquarePeg
}
def asRound(peg) {
[getRadius:{Math.sqrt(
((peg.width/2) ** 2)*2)}] as RoundThing
}
@Test void mixed_functional_composition() {
def hole = new RoundHole(radius:4.0)
(4..7).each { w ->
def cube = new CubeThing(x:w)
if (w < 6)
assertTrue hole.pegFits(asRound(asSquare(cube)))
else
assertFalse hole.pegFits(asRound(asSquare(cube)))
}
}
在清单9中 ,我创建了一些小函数,这些函数返回动态适配器,使我能够以方便,易读的方式将适配器链接在一起。 组合函数使函数可以控制和封装其参数发生的变化,而不必担心谁会将其用作参数。 这是一种非常实用的方法,使用了Groovy创建动态包装器类的能力作为实现。
将轻量级动态适配器方法与Java I / O库中笨拙的适配器组合版本进行了对比,如清单10所示:
清单10.笨拙的适配器组成
ZipInputStream zis =
new ZipInputStream(
new BufferedInputStream(
new FileInputStream(argv[0])));
清单10中的示例显示了适配器解决的一个常见问题:混合和匹配组合行为的能力。 缺少一流的功能,Java被迫通过构造函数进行合成。 使用函数包装其他函数并修改其返回在函数式编程中很常见,而在Java中则较少,因为该语言以过多语法的形式增加了摩擦。
结论
如果您始终陷于同一范式中,那么很难看到替代方法的好处,因为它不适合您的世界观。 现代混合范例语言提供了多种设计选择,了解每种范例的工作方式(并与其他范例进行交互)有助于您选择更好的解决方案。 在本期中,我说明了适应性的常见问题,并通过Java和Groovy中的传统适配器设计模式解决了该问题。 接下来,使用Groovy元编程和ExpandoMetaClass
解决问题,然后显示动态适配器类。 您还看到,为适配器类使用轻量级语法可以方便地进行函数组合,这在Java中很麻烦。
在下一部分中,我将继续探索设计模式与功能编程的交集。
翻译自: https://www.ibm.com/developerworks/java/library/j-ft11/index.html