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操作。 完全不需要关心或担心另一个的实现。
类型类中的桥接模式
我想认为在类型类中实际使用桥接模式有两个要求:
- 存在两个互补的概念,需要独立变化
- 显式依赖某些事物,这些事物被隐式理解,但可以通过类型签名来表达
举例来说,我使用的每个数据库(PostGRES,HSQLDB,MS SQL等)不仅实现了不同版本的ANSI标准SQL (有时仅实现标准的90%),而且通常还包含其自己的特定扩展,这些扩展分别是不可移植到其他数据库。 在编写查询时,我很少看到有人在他们的类或函数定义中将这种显式关系编成代码,即使它存在于查询字符串中。 团队通常会隐式地理解到绑定到一种数据库类型,并且此知识可以通过口口相传,代码中的注释或通过使用的技术堆栈来传递。 更进一步,在数据库类型的上下文中还强烈依赖于如何使用JDBC API。 JDBC文档明确警告了诸如创建PreparedStatement或获取生成键之类的警告。 并非所有的JDBC驱动程序都支持所有操作 ,也不是所有的数据库都支持相同的API挂钩 。
总而言之,我们已经满足了第二个条件。 为了满足第一个要求,我们需要讨论连接池 。 JVM有几个很棒的连接池( BoneCP , C3P0 , JDBC-Pool和DBCP仅举几例,尽管我很想听听有关活跃开发的更多知识。)这些库中的每一个都有很大的不同,以保证有自己的库连接处理程序实例,行为/性能日志记录和初始化机制。 为了在同一个库中支持多个池和数据库,两个不同的概念(池和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 隐式类