Scala中具有类型类和隐式的四个模式的帮派(第2部分)

类型类是库创建者和维护者的强大工具。 它们减少了样板,打开了要扩展的库,并充当了编译时间开关。 同样,GoF模式也是旨在提高代码质量的软件组织模式的集合。 上一篇博客文章探讨了使用一种带有类型类和隐式模式的模式 ,即桥模式 。 在这篇文章中,我将继续使用适配器模式

适配器模式是Scala社区中使用GoF模式的最广泛使用和公认的类型类 。 标准库包括几个示例,例如OrderingNumeric 。 与任何经过良好设计和实施的图书馆一样,它们的使用对图书馆使用者而言是透明且不可见的。 许多
来到Scala的人们可能甚至没有意识到就使用了这些。 如果您已经熟悉适配器模式 ,请跳过下一部分。

适配器图案

适配器模式 (有时称为包装器模式)是一种OO设计概念,旨在解决代码重复的问题并在存在完全不同的接口的情况下促进代码重用。 它通过统一通用接口周围的代码来做到这一点,并封装了“编程到接口”的GoF理念 从这个简短的描述中, 桥接器适配器模式可能似乎无法区分,但是它们的用途却截然不同。 桥接模式用于允许N个概念在N个接口之后独立变化,而适配器模式用于在基础概念相同时将N个接口缩减为一个。 就是说,它用作“胶水”以将不一致的API绑定或适配在一起(因此得名)。

适配器更是实用代码构建的便捷工具。 常见的用例是必须要有一个预先存在的组件或库才能在不适合容纳它的代码库中工作。 通常,仅当库需要版本之间的向后兼容性或用作将来库用户的扩展点时,才预先构建它们。 那是什么样子? 让我们以两个添加接口为例:

trait Addable{
  def add(x: Int, y: Int) = x+y
}
trait Summable{
  def plus(x: Int)(y: Int) = x+y
}

其中累加型的‘加’法公开咖喱 可添加的形式‘增加’的方法。 如果我们想使这两个接口在纯OO世界中可以互操作,我们将产生:

class Add2SumAdapter(addable: Addable) extends Summable{
  def plus(x: Int)(x: Int) = addable add (x, y)
}
class Sum2AddAdapter(sum: Summable) extends Addable{
  def add(x: Int, x: Int) = sum plus (x) (y)
}

实质上是将一个接口封装为另一个接口。 库解耦。 顺便说一句,有人争辩说,当我们朝着一种编程的功能风格发展时,我们可以减轻适配器模式的需求样板 。 在FP语言中, 适配器可以转换为currying功能组成提升 。 动态变化很重要,因为缓解并不能消除。 一个人需要考虑上述函数的类型签名,以了解适应性需求在功能方面如何继续满足:

type Plus = (Int => Int => Int)
type Add = ((Int, Int) => Int)

无法避免类型不匹配。 它必须在某个地方处理。

类型类中的适配器模式

在深入探讨适配器模式类型类之前 ,让我们退后一步讨论更大的问题。 库范围问题。 意思是说,让我们来谈谈与适配器模式有关的概念, Dependency Inversion Principle或简称DIP。 使用此技术编写的代码的标志是通过强制较低级别的模块符合在较高级别定义的接口,可以将较高级别的模块与较低级别的模块分离 。 因此,反演是更高级别的模块,它定义了要在其上构建的构造块,而不是相反。

DIP产生了更干净,更可扩展的代码,但这样做很大程度上依赖于结构设计模式的使用,而适配器是其中之一。 Scala允许在类型之间进行隐式转换 ,因此,DIP可以按照非常OO风格的编码在隐式转换方面实现 。 这将解决我们的代码中接口适配模板过多的问题,但是,副作用是,这将导致代码异常自然地难以调试,甚至导致错误更难以追踪。 有一个原因是默认情况下在2.10中将它们关闭(如果您经历了可变的隐式状态的乐趣,那么您将痛苦地理解为什么)。

DIP在类型级别上的泛化导致了一种称为ad hoc多态的更强大的构造。 在这种情况下,我们定义一个充当适配器的类型类 ,从而允许该类型的对象表达一组有限的操作。 这组操作定义了一个接口,可以将代码写入其中,而与对象的类型无关。 该对象及其类型不是将类包装在接口中,而是成为该接口的参数。 依靠隐式作用域解析可以基于函数调用站点上的参数类型注入正确的类型类

在进一步介绍之前,让我指出,DIP在我们刚刚描述的隐式上下文中使用时,听起来可能类似于另一种称为依赖注入的代码组织技术。 DIP不是依赖注入 ,我们刚才也没有描述。 DIP在编译时解决,而DI则围绕运行时解析。 一个可以利用另一个,但是它们彼此不同。

让我们看一下Java中日期和时间的两个示例实现: java.Datejoda.DateTime 。 两者均表示日期和时间以及修改方法。 但是,一个是可变的构造,其方法具有副作用,而另一个是不可变的,其方法返回一个新实例。 如果我们希望使用日期/时间类型,但仍与该类型的具体实现保持分离,则可以将行为的接口编码为可重用的类型类

trait DateFoo[DateType]{
  def addHours(date: DateType)(hours: Int): DateType
  def addDays(date: DateType)(days: Int): DateType
  def addMonths(date: DateType)(months: Int): DateType
  def addYears(date: DateType)(years: Int): DateType
  def now(): DateType
}

然后,在我们应用程序中的任何地方,我们都可以编写如下代码:

trait TimeTrackDAO{
  def checkin[A](unit: String, key: String, time: A)(implicit date: DateFoo[A]): A
  def checkout[A](unit: String, key: String, time: A)(implicit date: DateFoo[A]): A
  def itemize(unit: String, key: String): Int
}

只要存在“ A”的隐式范围的DateFoo类型类,“ checkin”和“ checkout”方法都将起作用。 如果没有这种类型? 我们只能使用“ itemize”方法,因为其他两个将无法编译! 沉思片刻。

让我一种说法,如果我们没有在系统中定义任何DateFoo类型类,那么没有什么可以阻止我们使用TimeTrackDAO的“ itemize”方法。 只有当我们尝试对缺少DateFoo的类型使用“ checkin”或“ checkout”方法时,我们的代码才能编译失败。 这是介绍性段落中提到的我们的编译时间切换。 具有隐式解析的类型类允许基于类型参数和作用域规则在编译时启用或禁用类/特征功能。

结论

OO设计模式和OO概念已经花费了数年的时间由开发人员完善和探索,这主要是由于OO在主流语言中占主导地位。 良好的SOLID方法已经出现,因为它需要解决基于OO的设计所带来的自然复杂性。 功能概念(例如类型类)才刚刚开始滴入共享这两种范式混合的语言中。

虽然标准的OOP习惯用语和功能构造(如类型类)之间存在分歧,但两者实际上可以用于共同利益。 FP不是灵丹妙药。 它不会使许多受OO启发的想法的常识无效,但是FP概念将迫使我们重新考虑我们为解决代码复杂性而采取的方法。 具有一流公民功能的语言的有用性质并不令人惊讶,只是冰山一角。

通过将适配器模式与受FP启发的类型类一起使用 ,我们已经展示了如何减少样板,打开代码以进行扩展以及施加编译时间约束。 没有像隐式转换或DI样式库那样的问题具有“魔术性”或难以追踪的问题。 使用适配器模式的类型类是经过精心设计和明确使用的,具有定义明确的作用域解析,该解析在编译时强制执行。 它们是OO和FP原理的完美结合。

参考:来自JCG合作伙伴 Owein Reese 在Scala上的静态类型博客中的Scala中的四个带有类型类和隐式类型的模式的帮派(第2部分)

翻译自: https://www.javacodegeeks.com/2013/03/gang-of-four-patterns-with-type-classes-and-implicits-in-scala-part-2.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值