直流耦合和交流耦合_耦合和组成,第2部分

上一部分中 ,我演示了代码重用的不同形式。 在面向对象的版本中,我提取了重复的方法,并将它们与protected字段一起移到超类中。 在函数版本中,我将纯函数(没有副作用的函数)提取到自己的类中,通过提供参数值来调用它们。 我将重用机制从通过继承的受保护字段更改为方法参数 。 包含面向对象语言的功能(例如继承)具有明显的好处,但它们也可能具有无意的副作用。 正如一些读者正确评论的那样,正是出于这个原因,许多有经验的OOP开发人员已经学会了不通过继承共享状态。 但是,如果您根深蒂固的范例是面向对象,则有时很难看到替代方案。

在本期中,我将通过语言机制与组合以及可移植代码进行耦合进行对比,以提取可重复使用的代码-这还有助于揭示有关代码重用的主要哲学差异。 首先,我将回顾一个经典的问题:如何在存在继承的情况下编写适当的equals()方法。

重访equals()方法

Joshua Bloch的《 Effective Java》一书中有一节介绍如何编写适当的equals()hashCode()方法(请参阅参考资料 )。 复杂性源于平等语义和继承之间的相互作用。 Java中的equals()方法必须遵守Javadoc为Object.equals()指定的特征:

  • 这是自反的 :对于任何非空参考值x,x.equals(x)应该返回true。
  • 它是对称的 :对于任何非空参考值x和y,当且仅当y.equals(x)返回true时,x.equals(y)才应返回true。
  • 它是可传递的 :对于x,y和z的任何非空引用值,如果x.equals(y)返回true,而y.equals(z)返回true,则x.equals(z)应该返回true。
  • 它是一致的 :对于任何非空引用值x和y,只要未修改对象的equals比较中使用的信息,对x.equals(y)的多次调用将始终返回true或始终返回false。
  • 对于任何非null参考值x,x.equals(null)应该返回false。

在他的示例中,Bloch创建了两个类( PointColorPoint ,并尝试创建对这两种方法均适用的equals()方法。 尝试忽略继承的类中的多余字段会破坏对称性,而尝试解决它会破坏可传递性。 乔什·布洛赫(Josh Bloch)为这个问题提供了可怕的预后:

在保留等价合约的同时,根本没有办法扩展可实例化的类并添加方面。

当您不必关心继承的可变字段时,实现相等要简单得多。 添加诸如继承的耦合机制会产生细微的差别和陷阱。 (事实证明,有一种方法可以解决此问题,该方法保留了继承,但是以添加其他依赖方法为代价。请参见Inheritance和canEqual()侧栏。)

回想一下迈克尔·费瑟斯(Michael Feathers)的报价,该报价引出了本系列的前两期文章:

面向对象的编程通过封装运动部件使代码易于理解。 函数式编程通过最大程度地减少运动部件来使代码易于理解。

实现equals()的困难说明了Feathers的活动部件隐喻。 继承是一种耦合机制:它将两个实体与关于可见性,方法分派等的明确定义的规则绑定在一起。 在Java之类的语言中,多态性也与继承联系在一起。 这些耦合点使Java成为面向对象的语言。 但是,允许移动部件会带来后果,尤其是在语言层面。 众所周知,直升机很难飞行,因为飞行员的四个肢体中的每一个都有控制装置。 移动一个控件会影响其他控件,因此飞行员必须善于处理每个控件对另一个控件的副作用。 语言部分就像直升机控件一样:您不能随便添加(或更改)它们来影响其他所有部分。

继承是面向对象语言的自然组成部分,因此大多数开发人员都忽略了它实际上是一种耦合机制的事实。 当奇怪的事情破裂或不起作用时,您只需学习(有时是不可思议的)规则来缓解问题并继续前进。 但是,这些隐式耦合规则会影响您对代码基本方面的思考方式,例如如何实现重用,可扩展性和相等性。

如果Bloch离开悬而未决的平等问题,那么有效的Java可能不会那么成功。 取而代之的是,他利用它作为机会重新引入了本书前面的好建议:相对于继承,更喜欢组合 。 Bloch对equals()问题的解决方案使用组合而不是耦合。 它完全避免继承,使ColorPoint 拥有对Point实例的引用,而不是成为Point的类型。

组成与继承

组合-以传递的参数加一流函数的形式-在函数式编程库中经常作为重用机制出现。 与面向对象的语言相比,功能语言在更粗粒度的级别上实现了重用,从而提取了具有参数化行为的通用机制。 面向对象的系统由对象组成,这些对象通过向其他对象发送消息(或更具体地,在其他对象上执行方法)进行通信。 图1表示一个面向对象的系统:

图1.面向对象的系统
面向对象的系统图,将类表示为圆圈,它们之间的箭头指示它们之间发送的消息。

当发现有用的类及其对应消息的集合时,您将提取该类图以进行重用,如图2所示:

图2.提取图表的有用部分
该图显示了图1中的圆圈和箭头的子集(类和消息)已被提取为较小的子集。

毫不奇怪,软件工程领域中最流行的书之一是《 设计模式:可重用的面向对象软件的元素》 (请参阅参考资料 ),它是图2所示的完全提取类型的目录。 通过模式进行重用非常普遍,以至于其他许多书籍也对此类摘录进行了分类(并提供了不同的名称)。 设计模式运动对软件开发界来说是一个巨大的福音,因为它提供了术语和示例。 但是,从根本上讲,通过设计模式进行的重用是细粒度的:一种解决方案(例如Flyweight模式)与另一种解决方案(Memento模式)正交。 设计模式解决的每个问题都是高度特定的,这使模式有用,因为您经常可以找到与您当前问题相匹配的模式,但由于它对问题是如此特定,因此狭义有用。

函数式程序员也需要可重用的代码,但是他们使用不同的构建块。 而不是试图创建结构之间公知的关系(偶联),函数编程尝试提取粗粒度的重用机制-地基于类别理论部分,数学的一个分支,类型的对象之间定义的关系(态射)(参见相关主题 )。 大多数应用程序都是用元素列表来做事的,所以一种功能性的方法是围绕列表以及上下文化的可移植代码的思想构建重用机制。 功能语言依赖于一流的功能 (可以出现在任何其他语言构造可以出现的任何地方的功能)作为参数和返回值。 图3说明了此概念:

图3.通过粗粒度机制和可移植代码进行重用
通过粗粒度机制和可移植代码表示重用的图

图3中 ,齿轮箱代表通常处理某些基本数据结构的抽象,黄色框代表将代码封装在其中的可移植代码。

通用积木

在本系列的第二部分中,我使用Functional Java库构造了一个数字分类器示例(请参阅参考资料 )。 该示例使用了三个不同的构建块,但没有说明。 我现在将调查这些构建基块。

褶皱

编号分类器中的一种方法对所有收集的因子求和,该方法如清单1所示:

清单1.函数编号分类器中的sum()方法
public int sum(List<Integer> factors) {
    return factors.foldLeft(fj.function.Integers.add, 0);
}

最初, 清单1中的单行主体如何执行求和运算尚不清楚。 这个例子是在名为catamorphisms列表转换的一般家庭的特定类型-从一种形式到另一种转换(参见相关主题 )。 在这种情况下, 折叠操作是指将列表的每个元素与下一个元素组合在一起的转换,为整个列表累积一个结果。 左折将列表从种子值开始向左折叠,并依次组合列表的每个元素以产生最终结果。 图4说明了折叠操作:

图4.折叠操作
该图显示了具有元素3、5、7、3和1的列表上的折叠操作(加法)。最终结果为19。

因为加法是可交换的,所以执行foldLeft()foldRight()都没有关系。 但是某些操作(包括减法和除法)关心顺序,因此存在对称的foldRight()方法来处理这些情况。

清单1使用了Functional Java提供的add枚举; 它包括最常用的数学运算。 但是,如果您需要更完善的条件呢? 考虑清单2中的示例:

清单2.带有用户提供的条件的foldLeft()
static public int addOnlyOddNumbersIn(List<Integer> numbers) {
    return numbers.foldLeft(new F2<Integer, Integer, Integer>() {
        public Integer f(Integer i1, Integer i2) {
            return (!(i2 % 2 == 0)) ? i1 + i2 : i1;
        }
    }, 0);
}

因为Java尚没有lambda块形式的一流函数(请参阅参考资料 ),所以Functional Java被迫临时使用泛型。 内置的F2类具有正确的折叠操作结构:它创建一个方法,该方法接受两个整数参数(这两个值相互折叠)和返回类型。 清单2中的示例仅在第二个数字为奇数的情况下才返回两个数字的总和,从而对奇数进行求和,否则仅返回第一个数字。

筛选

列表的另一种常见操作是过滤 :通过基于某些用户定义的条件过滤列表中的项目来创建较小的列表。 过滤如图5所示:

图5.过滤列表
该图说明了对从1到12的整数列表进行过滤以生成该列表的较小子集的示意图。

过滤时,您将生成另一个列表(或集合),该列表(或集合)的大小可能小于此示例,这取决于过滤条件。 在数字分类器示例中,我使用过滤来确定数字的因数,如清单3所示:

清单3.使用过滤确定因素
public boolean isFactor(int number, int potential_factor) {
    return number % potential_factor == 0;
}

public List<Integer> factorsOf(final int number) {
    return range(1, number + 1)
            .filter(new F<Integer, Boolean>() {
                public Boolean f(final Integer i) {
                    return isFactor(number, i);
                }
            });
}

清单3中的代码创建了一个从1到目标数字的数字范围(作为List ),然后使用isFactor()方法(在列表顶部定义filter()应用filter()方法来消除那些不是目标数量的因素。

清单3中所示的相同功能可以通过带有闭包的语言更简洁地实现。 清单4中显示了一个Groovy版本:

清单4.过滤操作的Groovy版本
def isFactor(number, potential) {
  number % potential == 0;
}

def factorsOf(number) {
  (1..number).findAll { i -> isFactor(number, i) }
}

filter()的Groovy版本是findAll() ,它接受指定您的过滤条件的代码块。 方法的最后一行是方法的返回值,在这种情况下,它是因素的列表。

制图

map操作通过将功能应用于每个元素将集合转换为新集合,如图6所示:

图6.将函数映射到集合
将函数映射到集合的图。输入(i)集合的元素为1、2和4。将28 / i函数应用于这些元素中的每一个都会产生具有元素28、​​14和7的新集合。

在数字分类器示例中,我在factorsOf()方法的优化版本中使用了映射,如清单5所示:

清单5.使用Functional Java的map()优化的因素查找器方法
public List<Integer> factorsOfOptimized(final int number) {
    final List<Integer> factors = range(1, (int) round(sqrt(number) + 1))
            .filter(new F<Integer, Boolean>() {
                public Boolean f(final Integer i) {
                    return isFactor(number, i);
                }
            });
    return factors.append(factors.map(new F<Integer, Integer>() {
        public Integer f(final Integer i) {
            return number / i;
        }
    }))
   .nub();
}

清单5中的代码首先收集直到目标数平方根的因子列表,并将其保存在factors变量中。 然后,我将新集合追加到factors map()factors列表上的map()函数生成map() ,应用代码生成对称(平方根上方的匹配因子)列表。 最后一个nub()方法可确保列表中不存在重复项。

通常,Groovy版本要简单得多,如清单6所示,因为灵活的类型和代码块是一等公民:

清单6. Groovy优化因素
def factorsOfOptimized(number) {
  def factors = (1..(Math.sqrt(number))).findAll { i -> isFactor(number, i) }
  factors + factors.collect({ i -> number / i})
}

方法名称有所不同,但是清单6中的代码执行与清单5中的代码相同的任务:获取从1到平方根的数字范围,将其过滤为因子,然后通过映射每个变量将列表附加到其上具有产生对称因子的函数的列表值。

重新探讨功能完善性

有了高阶函数,确定数字是否完美的整个问题就变成了Groovy中的几行代码,如清单7所示:

清单7. Groovy完美数查找器
def factorsOf(number) {
  (1..number).findAll { i -> isFactor(number, i) }
}

def isPerfect(number) {
    factorsOf(number).inject(0, {i, j -> i + j}) == 2 * number
}

当然,这是一个与数字分类有关的虚构示例,因此很难归纳为不同类型的代码。 但是,我注意到使用支持这些抽象的语言(无论它们是否为功能性语言)的项目的编码风格发生了重大变化。 我首先在Ruby on Rails项目中注意到了这一点。 Ruby具有与使用闭包块相同的列表操作方法,而collect()map()inject()出现频率令我震惊。 一旦习惯了在工具箱中使用这些工具,就会发现自己一次又一次地使用它们。

结论

学习像函数式编程这样的新范式的挑战之一是学习新的构建基块,并将它们“窥见”出来作为潜在的解决方案。 在函数式编程中,您的抽象要少得多,但是每个抽象都是通用的(通过一流的函数添加了特定性)。 由于功能编程在很大程度上依赖于传递的参数和组成,因此您掌握的规则更少,可以了解运动部件之间的相互作用,从而使工作变得更加轻松。

函数式编程通过抽象出通用的机器零件来实现代码重用,这些机器零件可以通过高阶函数进行自定义。 本文重点介绍了面向对象语言中固有的耦合机制引入的一些困难,这些困难引发了对类图的常见收集方式的讨论,以产生可重用的代码。 这是设计模式的领域。 然后,我展示了基于类别理论的粗粒度机制如何使您能够利用语言设计人员编写(和调试)的代码来解决问题。 在每种情况下,解决方案都是简洁明了的。 这通过组合参数和功能来创建通用行为来说明代码重用。

在下一部分中,我将进一步研究JVM上几种动态语言的功能特性:Groovy和JRuby。


翻译自: https://www.ibm.com/developerworks/java/library/j-ft6/index.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值