直流耦合和交流耦合

    技术2024-01-27  100

    关于本系列

    本系列旨在将您的观点重新定位为实用的心态,帮助您以新的方式看待常见问题并找到改善日常编码的方式。 它探讨了函数式编程的概念,允许在Java语言中进行函数式编程的框架,在JVM上运行的函数式编程语言以及语言设计的一些未来方向。 该系列面向那些了解Java及其抽象如何工作,但很少或没有使用功能语言经验的开发人员。

    面向对象的编程通过封装运动部件使代码易于理解。 函数式编程通过最大程度地减少运动部件来 使代码易于理解 。 — Michael Feathers,《 使用旧版代码》的作者 ,通过Twitter

    每天进行特定的抽象处理都会使其逐渐渗入您的大脑,从而影响您解决问题的方式。 本系列的目标之一是说明解决典型问题的功能方法。 对于本期和下一期,我将通过重构和相关的抽象影响来解决代码重用。

    面向对象的目标之一是使封装和使用状态变得更加容易。 因此,它的抽象倾向于使用状态来解决常见问题,这意味着要使用多个类和交互作用-迈克尔·费瑟斯(Michael Feathers)上面的引用称为“运动部件”。 函数式编程试图通过将零件组合在一起而不是将结构耦合在一起来最大程度地减少运动零件。 对于主要经验是面向对象语言的开发人员来说,这是一个很难理解的概念。

    通过结构重用代码

    必要的(尤其是)面向对象的编程风格使用结构和消息传递作为构建块。 要重用面向对象的代码,请将目标代码提取到另一个类中,然后使用继承来访问它。

    意外的代码重复

    为了说明代码重用及其含义,我将返回前一期中用来说明代码结构和样式的数字分类器版本。 分类器确定正整数是否丰富 , 完善或不足 。 如果数量因子的总和大于数量的两倍,则数量丰富。 如果总和等于数字的两倍,那是完美的; 否则(如果总和小于数字的两倍),则它是不足的。

    您还可以编写使用正整数因子确定其是否为质数的代码(定义为大于1的整数,其唯一因子为1以及数字本身)。 由于这两个问题都取决于许多因素,因此它们是重构(无双关语)并因此说明代码重用样式的良好候选者。

    清单1显示了以命令式形式编写的数字分类器:

    清单1.命令式数字分类器
    import java.util.HashSet; import java.util.Iterator; import java.util.Set; import static java.lang.Math.sqrt; public class ClassifierAlpha { private int number; public ClassifierAlpha(int number) { this.number = number; } public boolean isFactor(int potential_factor) { return number % potential_factor == 0; } public Set<Integer> factors() { HashSet<Integer> factors = new HashSet<Integer>(); for (int i = 1; i <= sqrt(number); i++) if (isFactor(i)) { factors.add(i); factors.add(number / i); } return factors; } static public int sum(Set<Integer> factors) { Iterator it = factors.iterator(); int sum = 0; while (it.hasNext()) sum += (Integer) it.next(); return sum; } public boolean isPerfect() { return sum(factors()) - number == number; } public boolean isAbundant() { return sum(factors()) - number > number; } public boolean isDeficient() { return sum(factors()) - number < number; } }

    我将在第一部分中讨论此代码的派生,因此现在不再重复。 其目的是说明代码重用。 这使我进入清单2中的代码,该代码测试质数:

    清单2.素数测试,命令式编写
    import java.util.HashSet; import java.util.Set; import static java.lang.Math.sqrt; public class PrimeAlpha { private int number; public PrimeAlpha(int number) { this.number = number; } public boolean isPrime() { Set<Integer> primeSet = new HashSet<Integer>() {{ add(1); add(number);}}; return number > 1 && factors().equals(primeSet); } public boolean isFactor(int potential_factor) { return number % potential_factor == 0; } public Set<Integer> factors() { HashSet<Integer> factors = new HashSet<Integer>(); for (int i = 1; i <= sqrt(number); i++) if (isFactor(i)) { factors.add(i); factors.add(number / i); } return factors; } }

    清单2中显示了一些注意事项。 第一个是isPrime()方法中有点奇怪的初始化代码。 这是实例初始化程序的示例。 (有关实例初始化(函数编程附带的Java技术)的更多信息,请参见“ 演化架构和紧急设计:利用可重用代码,第2部分 ”。)

    清单2中的其他有趣项是isFactor()和factors()方法。 注意,它们与ClassifierAlpha类( 清单1 )中的对应物相同。 这是独立实施两个解决方案并意识到您实际上拥有相同功能的自然结果。

    重构以消除重复

    此类复制的解决方案是将代码重构为单个Factors类,如清单3所示:

    清单3.常见的重构因子代码
    import java.util.Set; import static java.lang.Math.sqrt; import java.util.HashSet; public class FactorsBeta { protected int number; public FactorsBeta(int number) { this.number = number; } public boolean isFactor(int potential_factor) { return number % potential_factor == 0; } public Set<Integer> getFactors() { HashSet<Integer> factors = new HashSet<Integer>(); for (int i = 1; i <= sqrt(number); i++) if (isFactor(i)) { factors.add(i); factors.add(number / i); } return factors; } }

    清单3中的代码是使用Extract Superclass重构的结果。 注意,由于两个提取的方法都使用number成员变量,因此将其拖到超类中。 在执行此重构时,IDE问我如何处理访问(访问者对,受保护范围等)。 我选择了受保护的作用域,该作用域将number添加到类中并创建一个构造函数以设置其值。

    一旦我隔离并删除了重复的代码,数字分类器和质数测试器都变得更加简单。 清单4显示了重构的数字分类器:

    清单4.重构的简化数字分类器
    import java.util.Iterator; import java.util.Set; public class ClassifierBeta extends FactorsBeta { public ClassifierBeta(int number) { super(number); } public int sum() { Iterator it = getFactors().iterator(); int sum = 0; while (it.hasNext()) sum += (Integer) it.next(); return sum; } public boolean isPerfect() { return sum() - number == number; } public boolean isAbundant() { return sum() - number > number; } public boolean isDeficient() { return sum() - number < number; } }

    清单5显示了重构的质数测试器:

    清单5.重构的简化质数测试仪
    import java.util.HashSet; import java.util.Set; public class PrimeBeta extends FactorsBeta { public PrimeBeta(int number) { super(number); } public boolean isPrime() { Set<Integer> primeSet = new HashSet<Integer>() {{ add(1); add(number);}}; return getFactors().equals(primeSet); } }

    无论在重构时为number成员选择哪种访问选项,在考虑此问题时都必须处理一个类网络。 通常,这是一件好事,因为它使您能够隔离问题的各个部分,但是当您更改父类时,它也会带来下游后果。

    这是通过耦合进行代码重用的示例:通过number字段的共享状态和超类的getFactors()方法绑定两个元素(在本例中为类)。 换句话说,这可以通过使用语言中的内置耦合规则来实现。 面向对象定义了耦合的交互样式(例如,如何通过继承访问成员变量),因此您具有有关事物如何耦合的预定义规则-很好,因为您可以以一致的方式推理行为。 不要误会我-我并不是在建议使用继承不是一个好主意。 相反,我建议在面向对象的语言中过度使用它来代替具有更好特征的替代抽象。

    通过组合进行代码重用

    在本系列的第二部分中,我展示了Java中数字分类器的功能版本,如清单6所示:

    清单6.数字分类器的更多功能版本
    public class FClassifier { static public boolean isFactor(int number, int potential_factor) { return number % potential_factor == 0; } static public Set<Integer> factors(int number) { HashSet<Integer> factors = new HashSet<Integer>(); for (int i = 1; i <= sqrt(number); i++) if (isFactor(number, i)) { factors.add(i); factors.add(number / i); } return factors; } public static int sumOfFactors(int number) { Iterator<Integer> it = factors(number).iterator(); int sum = 0; while (it.hasNext()) sum += it.next(); return sum; } public static boolean isPerfect(int number) { return sumOfFactors(number) - number == number; } public static boolean isAbundant(int number) { return sumOfFactors(number) - number > number; } public static boolean isDeficient(int number) { return sumOfFactors(number) - number < number; } }

    我还有素数测试器的功能版本(使用纯函数,没有共享状态),其isPrime()方法显示在清单7中。其余代码与清单6中的同名方法相同。

    清单7.质数测试器的功能版本
    public static boolean isPrime(int number) { Set<Integer> factors = factors(number); return number > 1 && factors.size() == 2 && factors.contains(1) && factors.contains(number); }

    就像我使用命令式版本一样,我将重复的代码提取到其自己的Factors类中,将factors方法的名称更改为of以提高可读性,如清单8所示:

    清单8.功能重构的Factors类
    import java.util.HashSet; import java.util.Set; import static java.lang.Math.sqrt; public class Factors { static public boolean isFactor(int number, int potential_factor) { return number % potential_factor == 0; } static public Set<Integer> of(int number) { HashSet<Integer> factors = new HashSet<Integer>(); for (int i = 1; i <= sqrt(number); i++) if (isFactor(number, i)) { factors.add(i); factors.add(number / i); } return factors; } }

    因为功能版本中的所有状态都作为参数传递,所以提取不会伴随共享状态。 提取此类后,就可以重构函数分类器和素数测试器以使用它。 清单9显示了重构的数字分类器:

    清单9.重构的数字分类器
    public class FClassifier { public static int sumOfFactors(int number) { Iterator<Integer> it = Factors.of(number).iterator(); int sum = 0; while (it.hasNext()) sum += it.next(); return sum; } public static boolean isPerfect(int number) { return sumOfFactors(number) - number == number; } public static boolean isAbundant(int number) { return sumOfFactors(number) - number > number; } public static boolean isDeficient(int number) { return sumOfFactors(number) - number < number; } }

    清单10显示了重构的质数测试器:

    清单10.重构的质数测试器
    import java.util.Set; public class FPrime { public static boolean isPrime(int number) { Set<Integer> factors = Factors.of(number); return number > 1 && factors.size() == 2 && factors.contains(1) && factors.contains(number); } }

    请注意,我没有使用任何特殊的库或语言来使第二个版本更具功能。 相反,我是通过使用组合而不是耦合来实现代码重用的。 清单9和清单10都使用Factors类,但是其使用完全包含在各个方法中。

    耦合和组成之间的区别很细微但很重要。 在类似这样的简单示例中,您可以看到代码结构的骨架。 但是,当您最终重构大型代码库时,耦合随处可见,因为这是面向对象语言中的重用机制之一。 了解旺盛的耦合结构的困难损害了面向对象语言中的重用,将有效重用限制在了良好定义的技术领域,例如对象关系映射和窗口小部件库。 当我们编写不太明显的结构化Java代码(例如您在业务应用程序中编写的代码)时,我们无法获得同等级别的重用。

    您可以通过在重构过程中注意IDE所提供的功能,礼貌地拒绝并使用组合来改善命令式版本。

    结论

    作为一个功能更强的程序员进行思考意味着对编码的所有方面的思考都不同。 代码重用是一个显而易见的开发目标,命令式抽象倾向于以不同于函数式程序员解决该问题的方式来解决该问题。 本部分对比了两种代码重用样式:通过继承进行耦合和通过参数进行组合。 下一部分将继续探讨这一重要鸿沟。


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

    相关资源:交流耦合和直流恢复
    Processed: 0.023, SQL: 9