Scala中具有类和隐式的四个模式的帮派

在Scala语言中众所周知的类型类在库开发中占有重要地位。 它们使代码易于扩展,不再冗长并简化了API。 我还没有找到其他具有相同功能的语言模式。 根据您的观点,紧随其后的是Python语言中生成器装饰器的概念之一(后者只是变相的函数组合 。) 阅读后,如果您不同意或觉得我漏了一点,请在评论部分中说出来。

对于那些不知道类型类别是什么的人,互联网上到处都是聪明的 人, 描述 他们 什么 。 实际上,Scala的创建者Martin Odersky有一篇不错的论文和演示文稿,您可以在这里这里参考。
您需要史诗般的Typeclassopedia ,它描述了scalaz库对类型类的使用,足以让我头疼。 对于不耐烦的人,无需博士就可以轻松启动并运行这些东西。 您需要做的就是查看实际操作。

在接下来的几篇文章中,我将介绍三种“四种组合”设计模式,以及如何将它们与类型类一起应用:

我希望这将使将来使用另一种语言的图书馆作者对使用这些语言的方式,时间和原因有所了解,但必须指出,在所有方面,过多的使用肯定是您做错了。 适配器模式是Scala社区中类型类最广泛认可的用例。 因此,与其坚持使用某些东西,而是让社区很大程度上意识到,我将从一些鲜为人知的东西开始。 为什么? 我最近一直在我目前的雇主Novus Partners的一个小型图书馆中使用桥接模式 。 如果您已经知道桥接模式 ,则可以跳过下一部分,直接进入类型类部分。 如果没有,继续阅读。

桥型

桥接模式的开发是为了解决多个独立概念需要共存而又不会引起类型组合爆炸的问题。 在面向对象的语言(Java,C#,C ++)中,人们倾向于通过继承而不是对象组合来耦合思想,这种模式经常出现在重构 游览中 。 本质上,对象在运行时配对在一起,并在编译时通过公共接口松散耦合,通常一个概念的接口传递给另一个概念的具体实现的构造函数。

如果您从未听说过关于继承组合 ,那么就是这种模式。 OO语言的实践者和支持者并不感到惊讶,认为它不仅是OO的扩展,而且体现了良好设计最佳原则 。 我本人不会轻视它的美德。 像任何设计模式一样,它以简单,优雅的方式解决了实际问题。 对于FP语言或Scala之类的FP-OO混合语言,倾向于使用高阶函数来代替一次性抽象接口(因为函数本身就提供了一个合理且易于理解的接口)。

那是什么样子? 考虑以下类别:

class EncryptedFileWriter(cypher: String, key: String) extends FileWriter{
  def write(file: File, content: String) = open(file){ encrypt(content) }
  //more
}
class CompactJsonFileWriter extends FileWriter{
  def write(file: File, content: String) = open(file){ 
    validate(content)
    compact(content) 
  }
  //more
}

显然,每个类都在对文件执行两种不同类型的写入。 一种是将其转换为加密格式,而另一种是将JSON转换为紧凑表示形式。 但是,如果我想写数据库怎么办? 为了能够写加密或JSON格式,我必须再创建2个特定于我正在使用的数据库的类。 看到这是怎么回事? 如果我有M种格式和N个内容目标,则必须创建MN类。

桥接模式表示,为了最大程度地减少类的数量,我应该将写入目标和格式的概念分离为独立的层次结构,这些层次结构是独立变化的。 因此,本着良好的OO设计精神,将通过构造函数或使用高阶函数作为另一个参数来传递目标概念。 在这个人为的例子中,我们可以期望重构:

class FileWriter(file: File) extends Writer{
  def write(content: String, convert: String => String = identity) = open{ convert(content) }
  //more
}
class JsonConvert extends (String => String){
  def apply(input: String): String = //more
}

因此,我们采取了将格式设置为可选的附加自由。 现在Writer只是表示要写入的内容和格式化的内容是String => String操作。 完全不需要关心或担心另一个的实现。

类型类中的桥模式

我想认为在类型类中实际使用桥接模式有两个要求:

  1. 存在两个互补的概念,需要独立变化
  2. 隐式理解某些事物的显式依赖关系,但可以通过类型签名来表达

举例来说,我使用的每个数据库(PostGRES,HSQLDB,MS SQL等)不仅实现了不同版本的ANSI标准SQL (有时仅实现标准的90%),而且通常还包含其自己的特定扩展,这些扩展分别是不可移植到其他数据库。 在编写查询时,我很少看到有人在他们的类或函数定义中将这种显式关系编成代码,即使它存在于查询字符串中。 团队通常会隐式地理解到绑定到一种数据库类型,并且此知识可以通过口口相传,代码中的注释或通过使用的技术堆栈来传递。 更进一步,在数据库类型的上下文中还强烈依赖于如何使用JDBC API。 JDBC文档明确警告了诸如创建PreparedStatement获取生成键之类的警告。 并非所有的JDBC驱动程序都支持所有操作 ,也不是所有的数据库都支持相同的API挂钩

总而言之,我们满足了第二个条件。 为了满足第一个要求,我们需要讨论连接池 。 JVM有几个很棒的连接池( BoneCPC3P0JDBC-PoolDBCP仅举几例,尽管我很想听听有关活跃开发的更多信息。)这些库中的每一个都有很大的不同,以保证它自己的连接处理程序实例,行为/性能日志记录和初始化机制。 为了支持同一个库中的多个池和数据库,需要将两种不同的概念(池和JDBC API调用)和谐共存。

用例范例

假设我们想要一个非常基本的接口来执行您的四种DML查询(也称为CRUD操作)。让我们来看一下类似的东西:

trait Query{
  def insert(query: String, args: AnyRef*): List[Int]
  def delete(query: String, args: AnyRef*): Int
  def update(query: String, args: AnyRef*): Int
  def select(query: String, args: AnyRef*): ResultSet
}

这种类型的接口公开了直接,统一且易于理解的API。 即使没有上下文,也几乎不会发生什么混乱。 就是说,如果我们不加修改地针对它进行开发,则Query最好表示为一个抽象类,其中有两个构造函数参数采用一个用于连接池的接口,一个用于数据库特定逻辑的接口。 简而言之,它看起来很像Java。 这是我们转向类的地方。

正如我在上面概述的那样,我们所做的任何查询都会隐式地假设使用哪个数据库。 为什么不将其显式编码为类型签名? 而且,如果我们想使用类型类来简化API,我声明类型类应遵循以下规则:类型类应具有参照 透明性 ,仅描述方式,仅此而已。 因此,在选择使池成为类型类还是使语句执行器成为类型类之间进行选择时,我认为选择本身就可以了。 都不行 两者都是副作用操作。 但是,如果我们愿意,可以将后者改写为返回IO Monad,从而保持“纯净”。

首先,我们将所有连接池处理放入Query接口本身:

trait Query[DB]{
  def insert(query: String, args: AnyRef*)(implicit con: StatementConstructor[DB]) = pool{ con.insert(query, args: _*) }
  def delete(query: String, args: AnyRef*)(implicit con: StatementConstructor[DB]) = pool{ con.delete(query, args: _*) }
  def update(query: String, args: AnyRef*)(implicit con: StatementConstructor[DB]) = pool{ con.update(query, args: _*) }
  def select(query: String, args: AnyRef*)(implicit con: StatementConstructor[DB]) = pool{ con.select(query, args: _*) }
  protected def pool[A](f: Connection => A): A
}

利用Scala的特性来定义所有内容,但不包括每个池库特定的实现。 然后,我们将在通过隐式范围解析传递的类型类中定义数据库语句构造:

trait StatementConstructor[DB]{
  def insert(query: String, args: AnyRef*)(connection: Connection): List[Int]
  def delete(query: String, args: AnyRef*)(connection: Connection): Int
  def update(query: String, args: AnyRef*)(connection: Connection): Int
  def select(query: String, args: AnyRef*)(connection: Connection): ResultSet
}

允许两者彼此独立变化,同时仍产生一种与我们最初描述的消费者完全相同的API。

注意,这两个特征签名的互补性质描述了一个系统,该系统是免费的,任何人都可以在给定的操作上进行扩展。 处理PreparedStatement的特定于数据库的逻辑对使用者完全透明。 使用者只需要知道要针对哪个数据库进行编码(通过类型签名表示),以及如何初始化Query的具体实现。

从理论上讲,使用该系统的代码如下所示:

val queryPool = new DBCPBackedQuery[MySQL]("myConfigFile")

def activeUsers(query: Query[MySQL]) = {
  val resultSet = query.select("SELECT * FROM active_users")
  //more

我认为任何未来的维护者都将非常容易理解。 同样,任何代码审阅者都可以通过简单地检查类型签名来获得有关如何编写查询的指南。 一场胜利。

结论

桥接模式解决了OO和OO-FP混合语言中的基本软件设计问题。 在使用类型类的Scala中进行开发时,我们可以通过将隐式关系推迟到函数调用站点的关系来避免显式地构造函数的定义。 这不仅简化了API,而且打开了要扩展的库,而又不会使贡献者负担成倍的扩展成本。

我希望该示例足够说明性,以突出Scala中这种设计模式的优点以及总体上类型类的灵活性。 在下一篇文章中,我将使用适配器模式介绍隐式类型类; 展示了如何将其用于即席多态性,并通过类似于C ++的enable_if模板构造的特定类型来限制类功能。 不用担心,不会有任何C ++,只有Scala。

参考:来自JCG合作伙伴 Owein Reese 在Scala上的“ Scala中带有类型类和隐式的四个模式的帮派”在“ 静态类型”博客上。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值