【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置构建工具的小工具为例 (一)

【如何进行 Scala 代码设计 -- 类型划分定基调】 -- 以自动配置小工具为例 (一)

前言

scala 是一种多范式的语言, 可以使用面向对象的方式 或者是使用函数式的方式进行编程。

本文以 5分钟搞定构建工具仓库配置 一文中的工具为例, 尝试探讨一种 scala 的代码组织方式, 抛砖引玉。

首先介绍一下这个工具的功能, 自动配置 SBT 和 Maven 工具的本地仓库, 和国内镜像。目标非常简单, 但是要求代码的设计要优雅,可扩展,而不是像写脚本文件一样一个文件写完。

源码地址

代码保存在 码云,直接看 master 分支提交记录即可.

第一次尝试

首先我们分解一下程序的步骤,

  1. 首先是根据构建工具的不同,到不同的位置读取其当前的配置文件
    比如 sbt 就是在 SBT_HOME/bin/sbt-launch.jar 内的 sbt/sbt.boot.properties, maven就是在 MAVEN_HOME/conf/settings.xml.
  2. 然后将对应的配置更新进去。

这里有三种类型的配置,

  1. 镜像配置,
  2. 本地仓库目录配置,
  3. maven仓库的配置(对于非Maven的工具,比如 sbt 自己用的是 ivy 仓库,配置mvn仓库之后可以重用其中的依赖)

然后工具目前有2种类型: MavenSBT

初步的程序构想是,利用 typeclass 模式将各个服务组装起来,最后提供一个隐式装换封装一个 api 出来,方便调用。

typeclass模式最方便的一点就是编译器自动利用类型帮你注入依赖

最终的api构想

最后的代码应该类似于这种:

XXXX.config[SBT,Mirror](param)
XXXX.config[Maven,LocalRepo](param)

可以通过类型参数选择我要执行那些配置,当然这些类型参数如果能省略(利用编译器自动推断)就更好了.

接口定义

为什么上来就要接口定义? 因为好的设计是面向抽象编程而不是面向具体编程, 当然你也可以先写完普通代码然后再提取出接口, 但是我希望做到的是从接口出发, 逐步实现。所以我这里上来就直接定义接口。

定义类型

正如上面所说,配置类型有三种,我们需要单独提取一些类型出来
在这里插入图片描述
这里的类型并没有什么方法或者属性定义, 因为这个类型我想将其作为 虚类型(phantom type), 这些类型是用来标记代码的, 是为了方便 typeclass 需要对应的实现.

然后还需要定义一个构建工具的类型, 这里我把 Gradle 也定义了, 虽然暂时还用不到.
在这里插入图片描述

定义行为接口

根据上面的步骤分解, 我们首先需要一个查找从 home 目录查找配置文件的步骤, 所以我定义了一个接口 ConfigFinder, 这里的 F[_] 表示代码执行的上下文, 用来进行 Monad 变换的, BuildToolConfigType 表示 构建工具的类型 和 配置的类型, 也就是我们上面的类型定义, info表示参数. 返回值直接是 File了, 这里是为了简化接口参数… 要不然这个接口的类型参数也太多了.
在这里插入图片描述
然后我们需要进行配置转换, 所以再定义一个 ConfigTransformer 接口
在这里插入图片描述
然后在编码过程中, 发现还需要检查当前是否已经配置过了,如果已经配置过的话, 就不需要再配置了, 所以需要一个检查配置是否已经配好的接口
在这里插入图片描述
最后来一个汇总的接口, 用来组合上面的那些接口
在这里插入图片描述
粗略分下来大概有这么一些接口,当然还有其他的接口 ConfigReader,ConfigWriter 大致类似。

细化,分包,重命名

这些接口和类总不能乱糟糟的放在同一包里面, 所以进行了一下分包和重命名, 让代码看起来更加功能. 后面我从 ConfigFinder 中再分一个 ConfigReader 接口出来, 这是因为 sbt 的读取配置 和 maven 的读取配置是不一样的逻辑, sbt 还要解析 jar 包中的配置文件.
在这里插入图片描述
但是这个 ConfigReader 的类型参数也太多了吧

至此,大概的目录结构如下:
在这里插入图片描述
其中 sparrowxin.service.interpret 包用来放接口的实现. 这个目录结构是参考《函数响应式领域建模》一书中提到的结构。

model包里面的 configInfo 是干嘛用的呢?这里我是用其来当做常量类使用了。通过隐式依赖可以简化代码,提取共同的逻辑。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
另一方面是当我能找到一个隐式的 ConfigInfo[BuildTool,ConfigType] 的时候, 说明这种配置方式是这种工具下允许配置的. 因为我们上面说过 SBT 相比 Maven 多了一个 SBT配置 Maven 的地址这种配置类型.

第一版实现

在这里插入图片描述
为了细化类型, 我又定义了 SBTConfigReader, SBTConfigTranformer 等接口, 这样就可以减少 类型参数中的 BuildTool 和 不同构建工具 读取出来的配置的类型, 比如 sbt 读取出来的配置作为 List[String], 而 maven 的配置读取出来就是 scala-xml 库中的 Elem 类型
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这些细化的接口中, 我们可以把其特定的代码增加到接口中去, 这样可以提取更多的公共代码,减少代码量 和 重复代码.

最后把代码接口都实现了之后, 我们再声明一些隐式变量到 Interpreter中, 方便导入和依赖查找
在这里插入图片描述

在这里我们为不同的 BuildTool 和 不同的 ConfigType 都提供了实现, 这样后面组装的时候,他们就可以自动搜索到这些隐式参数
在这里插入图片描述
最后在 ConfigBuildTool 类中将所有的方法集中起来, 这样我们使用的时候 import ConfigBuildTool._ 就能调用所有的方法了.
在这里插入图片描述
然后为了方便调用, 在 ConfigBuildTool 中声明了一些隐式类, 封装 api
在这里插入图片描述

最终目录结构

在这里插入图片描述
看着还挺工整的, 但不知道以为是写了个火箭的起飞代码 – 这类分得也太多了.

第一版缺点

  • 接口太细, 导致类爆炸. 明明一个小功能, 却搞了这么多类, 增加许多不必要的灵活性. 每多增一个接口, 组合最后的接口就越复杂. 比如为了组合成最后的 DoConfig,我得增加这么多隐式参数的声明. 接口拆分的越多, 通信的开销就越大. 还有内部的配置的表示类型(比如 SBT 读取出来是 List[String], maven读取出来是 Node) 也需要在接口间传递, 导致导出都要声明一大堆类型.
    在这里插入图片描述

  • 其次类型参数不够明确, 所有的类都带上了 BuildTool 和 ConfigType 参数, 这样会导致在声明接口, 声明隐式变量, 隐式类的时候, 也都要传递隐式参数,而且太多的隐式参数不利于自动推断类型. 而且在逻辑上也没有必要这么做,比如说 配置读取类ConfigReader, 对于一种特定的工具, 读取配置的方式跟要配置什么没有关系, 无论是要配置 镜像 还是 配置 本地仓库, 其实都是都是 conf/settings.xml 文件. 所以 ConfigType 应该从 ConfigReader, ConfigWriter中去掉. 其次所有接口都带上了 上下文类型参数 F[_], 这个也是没有必要的. 按理来说, 应该只在函数调用可能参数副作用 或者 有明显的上下文类型的时候进行声明, 毕竟后面调用的时候, 由外部指定类型就好了, 所以 F[_] 也应该都去掉.

  • 没有划分参数, 有一些参数是需要外部传进来的, 而且某些逻辑也需要外部的参数, 比如说 ConfigChecker, 检查是否已配置的镜像的时候, 我需要通过 镜像的名称 来跟我当前配置里面的名称匹配, 如果匹配, 则说明配置过了, 无需再匹配. 这种情况下, ConfigChecker 就无法声明为隐式参数自动注入. 因为依赖了外部的参数. 所以这种依赖于外部参数的类型, 就应该单独分离出来, 将其作为未来 api 调用的一部分(下文会说明). 这也导致了抽象不统一, 需要一些重复代码才能声明特定隐式类作为 调用的 api.

下一篇文章, 我将说明如何对其重构以及期间的心路历程。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值