不管我们的程序写了多少漂亮的模块,最终它总是要集成的,将不同来源的输入,如用户输入、网络服务的结果、数据库保存的数据,经过各种各样的业务处理,输出成观众喜闻乐见的输出格式。

这个过程中,往往充满了没有特别技术原因的决定:例如输入的格式可能需要兼容历史数据、可能需要格式转换,输出的格式往往是为了当前的方便等。仅仅从上面的表述我们就可以知道,这个集成的过程往往是低内聚性的、逻辑关系不明确的操作的组合。

处于某种原因,很多程序员喜欢将集成的复杂度隐藏在简洁的外壳下面,如下面这个工具程序的 -main 函数:

clojure(defn -main
  []
  (let [cfg (conver-param (util/load-edn-config "config.edn"))
        msg (validate cfg)]
    (if msg
      (exit -1 msg)
      (time (search-users-data (create-system cfg))))))

看起来很美。但它的输入是什么?我们从上面代码中难以判断,输出又是什么?也无从看起。实际上,它的输入和输出的集成在 search-users-data 这个函数中:

clojure(defn search-users-data
  "查所有用户数据,并输出到excel文件"
  [system]
  (let [{:keys [user-names-file stat-time retain-days
                retain-def newer-time] :as cfg} system
        usernames (util/load-excel-users user-names-file)]
    (out-excel
      (core/stat usernames stat-time retain-days retain-def newer-time cfg)
      cfg)))

在这个函数中,我们终于可以看到 load-excel-users 这个输入函数了,我们也能够注意到这个函数的名字已经包含了实现的细节:excel,显然,如果变化输入格式,本函数将需要改写。我们也能看到输出函数 out-excel,它当然也有同样的问题。

实际上,这种有意将集成延迟在第二、第三层的做法,不过是引入了不必要的包装,search-users-data函数本身并没有重用的价值,它反而增加了维护者、读者的思考负担。该程序的问题还没有到此为止,我们注意到,在 search-users-data 函数中有大批的过路参数(请见我的另一篇博客),让函数的耦合负担进一步加重。

更好的做法是一层集成,将输入、处理和输出能更明确地表达出来:

clojure(defn -main
  [user-names-file output-filename]
  (let [cfg (convert-param (util/load-edn-config "config.edn"))
        error-msg (validate cfg)]
    (if error-msg
      (exit -1 error-msg)
      (let [usernames (load-excel-users user-names-file)    ;从文件中读取所有用户名
            conn (esr/connect (:elk-url cfg))
            cfg (assoc cfg :conn conn)                      ;连接查询服务器
            data (user-data usernames cfg)                  ;查询出所有数据
            ]
        (to-excel-file! data (date-out-file output-filename (:stat-time cfg)))       ;输出到 excel 文件
        ))))

偶然耦合的关系到此为止,具体的输入处理函数如 load-excel-users,具体的输出函数如 to-excel-files 和具体的处理函数 user-data 之间如何作用,一目了然,在恰当的注释帮助下读者非常容易定位。而中间的集成函数则可以简单删掉。

对于 web 服务器这样的服务器程序,我们的集成点则不象上述例子中只有一个-main函数,而很可能是多个:每种请求都可能是一个出发点,但集成的道理是一致的:

应减少集成的中间层次。


robertluo
738 声望21 粉丝

引用和评论

0 条评论