写点什么

使用 Ruby DSL 实现敏捷素材管理

  • 2007-09-24
  • 本文字数:4048 字

    阅读完需:约 13 分钟

领域特定语言(Domain Specific Language,简称 DSL)是一个面向语言的工具,用于解决某个特定领域的编程任务。DSL 的一般语言特征和它所被用到的问题领域,关系是非常密切的,并且在一个非常高的抽象层面上起作用。Martin Fowler 在他介绍 DSL 的文章中,将 DSL 划分为 _ 外部 DSL_ 和 _ 内部 DS_L 两类(参见原文链接)。外部DSL 是一门需要编译或者解释运行的编程语言,而内部DSL 则构建于一门通用编程语言(general-purpose programming language)之内。实际上,内部DSL 对于其宿主通用编程语言来说,就是它的一套层次非常高的API。本文讲述了了在PLANET ARGON 公司的一个开发项目中,用Ruby 实现的一个内部DSL 如何给项目带来巨大贡献。

问题的提出

我在PLANET ARGON 公司近期的一个开发项目的目标是,使用Ruby on Rails 构建一个一次性的内容管理系统,这个系统要支持18 种语言,并且管理大约1000 个的图像文件。这些文件中有许多都是专业摄影图片,每个文件大小都超过1MB。而另外的图片文件则是一些细碎的线条艺术,图片里面显示的就是许多不同的国旗。这些图片所具备的相同特点,就是它们都还不能直接投入产品使用。

我们的应用程序中包含一个 <strong>Image</strong>模型,该模型使用我们内部编写的一个文件上传插件来持久化图片数据。我们已经使用了<strong>after_save</strong>钩子来管理图形转换操作,而这些钩子在我们的模型内部使用了<strong>RMagic</strong>图形操作 API。这样看来应该就够了,但实际上我们的需求一直在不断被调整。有些图片需要进行四次变换才能投入使用;而其它图片只需要两次;其中一些图片必须被上传到一个第三方的贷款提供商那里;而又有些时候,我们把图片的尺寸弄错了。我们曾经一度编写了一个批处理脚本来上传这些原始图像,接着<strong>after_save</strong>钩子就可以重新处理这些图像了。便捷情况很快就凸现了出来,于是我们回调代码中就七荤八素地塞满条件判断语句。你可能已经猜到了,这个麻烦很快就成了往事。我们需要一个可以帮助我们以一种可持续发展的方式达到客户需求的工具。

解决方案

有一天,我们的客户又向我们的 Basecamp 发过来一个参考图片,以及他们如何在 PhotoShop 中创建这个图片的一系列指示。我抓狂了。我开始考虑使用一个更加优雅的方案来解决这个问题,我想起了 Jason Watkins 曾经跟我说起过,当他在电子游戏这一行工作的时候,他用过一类称为“素材编译器(asset compiler)”的工具。素材编译器其实是一个构建系统,将一系列媒体文件转换成用于电子游戏最终构建版用到的媒体文件。最近我用 Rake 自动化了不少重复性的任务(Rake 是一个用纯 Ruby 来实现的构建系统),所以用 Rake 搞出一套素材编译器,对我来说很有吸引力。如果创建产品图像的职责可以委派给一个外部的工具,那么不管我们客户的需求在什么时候发生改变,我们的应用程序都无须进行变更。更有意思的是,这个工具可以使得我们在需求发生变化的时候,使用一条命令就 _ 重新构建 _ 出所有的产品级图像。

拿着 Jim Weirich(Rake 的作者)的 RDoc 任务库(task library),我开始上手干活了。RDoc 任务库以模板的形式随 Rake 一起被分发。Jim 为 Rake 创建的用法模式之一就是一个实例化的类,里面带着描述了需用于自动化构建的 Rake 任务集合的合适选项。比如说,这里就给出了一个范例,展示如何在<strong>Rakefile</strong>使用 RDoc 任务库为 Rails 应用及其使用的插件,还有整个 Rails 框架构建文档。

Rake::RDocTask.new('my_docs') do |rdoc|<br></br> # configuration<br></br> rdoc.rdoc_dir = 'doc/my_docs'<br></br> rdoc.main = "doc/README_FOR_APP"<br></br> rdoc.title = 'Comprehensive Documentation'<p> # app documentation</p><br></br> rdoc.rdoc_files.include('doc/README_FOR_APP')<br></br> rdoc.rdoc_files.include('app/**/*.rb')<br></br> rdoc.rdoc_files.include('lib/**/*.rb')<p> # plugins documentation</p><br></br> rdoc.rdoc_files.include('vendor/plugins/*/lib/**/*.rb')<p> # framework documentation</p><br></br> # ...snip...<br></br> end按照这个范例在你的Rakefile中实例化一个<strong>Rake::RDocTask</strong>对象将会建立一系列 Rake 任务,用于创建、删除及重新构建你的文档。被<strong>Rake::RDocTask.new</strong>使用<strong>rdoc</strong>这个 block 参数,实际上就是正被实例化的<strong>Rake::RDocTask</strong>对象,而正被初始化的选项以及<strong>rdoc</strong>的属性则是用<strong>attr_accessors</strong>定义在<strong>Rake::RDocTask</strong>中的。

我开始将构建产品级图像的代码从我们的Image模型中抽取出来,放进一个 Rake 任务库中。在建立了一个操作起来类似于 RDoc 任务库的可工作任务库之后,我捣鼓出来的代码就像下面这样:

ImageTask.new :bronze_thumbnail do |t|<br></br> t.src_files = image_src 'images/*.jpg'<br></br> t.build_path = build 'greyscale_thumbnail'<br></br> t.remote_dirs << REMOTE_DIR[:greyscale_thumbnail]<br></br> t.transformation do |img|<br></br> # apply bronze image effect<br></br> img = img.quantize 256, Magick::GRAYColorspace<br></br> img = img.colorize 0.25, 0.25, 0.25, '#706000'<br></br> # crop to thumbnail size<br></br> ...snip...<br></br> end<br></br> end上面的代码定义了以下 Rake 任务,用<strong>rake -T</strong>可以得到以下描述:

rake assets:bronze_thumbnail:build # Build the bronze_thumbnail files<br></br> rake assets:bronze_thumbnail:clobber # Remove bronze_thumbnail files<br></br> rake assets:bronze_thumbnail:rebuild # Force a rebuild of the bronze_thumbnail files<br></br> rake assets:build # Build all assets<br></br> rake assets:clobber # Clobber all assets<br></br> rake assets:rebuild # Rebuild all assets我意识到,如果描述这些图像变换的代码与客户给我们的 PhotoShop 指令非常相似的话,这个工具将变得极具灵活性——代码就成了为图形变化定义 Rake 任务的内部 DSL 了。

我为我的任务库创建了一些单元测试,以保证任务可以被正确地创建出来,并且以测试驱动重构迈开第一步。由于我创建的是一个内部 DSL,它在我这个例子里是一个 Ruby API,这套 DSL 的创建完全可以遵循测试驱动的实践。我开始将我单元测试中初始化任务库各个类的范例代码替换成我尚未实现的 DSL 的范例代码,并且修改了数次,直到我满意为止。

define_image_transformation 'thumbnailize' do<br></br> crop_to '62x62', :north<br></br> end<p> define_image_transformation 'bronze' do</p><br></br> greyscale<br></br> lighten<br></br> # r g b tint<br></br> tint 0.25, 0.25, 0.25, '#706000'<br></br> end<p> image_task 'bronze_thumbnail' do</p><br></br> from images 'images/*.jpg'<br></br> to build 'greyscale_thumbnail'<br></br> remote_dirs << REMOTE_DIR[:greyscale_thumbnail]<br></br> transformation do<br></br> bronze<br></br> thumbnailize<br></br> end<br></br> end我开始运行我的单元测试,然后看见一连串错误消息跳将出来。这是件好事——现在我知道了我要的是什么,还有我需要哪些步骤才能实现我的目标。接着,我重命名了几个方法(比如说,把<strong>src</strong>改成了<strong>from</strong>,然后创建了一些辅助方法(helper methods)来辅助初始化任务库的类。此外,我还将一个低层的 RMagick 代码包装进一个类库之中,类库使用了对设计师友好的方法命名。随后我创建了一个<strong>define_image_transformation</strong>,该方法通过聚合那些 RMagick 的包装方法,定义了针对应用程序的高级图形变换。通过把类似于 Photoshop 的图形工具可以做到的事情和我们的客户需要我们做到的事情区别对待,我就可以保证我的工具能被抽取出来,并且重用在今后的项目之中——高层的图形变换代码是存放在我应用程序中<strong>lib/tasks</strong>目录的,而低层的包装器代码则存放在<strong>vendor/asset_compiler</strong>目录之下,和任务库分开存放。

成果

目前,我们的<strong>lib/tasks/assets.rake</strong>文件包含对<strong>image_task</strong>的 12 个调用,包括上面展示的实例。尽管对于外行程序员(lay programmers)来说,这门语言不一定合适(Fowler 在文章中说,外行程序员就是非专业的程序员,但他们可以使用简单的编程工具来自动化业务任务),但是它和我们客户的语言是同步的,而且优雅地将他们的想法和目标建模出来。

因为 Ruby DSL 就是 Ruby 的代码,所以我强烈建议要遵循我在这篇文章中描述的测试驱动过程:编写你希望使用的 Ruby 内部 DSL 的一个范例代码,然后编写为这个范例的期望结果编写测试用例,接下来还是编写代码,直到所有的测试都能通过为止。此外,在一开始时就要考虑你 DSL 的范围。由于我强调了图像转换功能和我们应用程序的需求之间的分离,在我们的 DSL 基础上构建出一个强大、可重用而又可扩展的框架就是一件自然而然的事情了。我认为这是一个正确的决定,因为我知道很快我又将把这套类库用于其它项目。如果你的需求更加特别,那么你就不需要多层的抽象了。如果你打算重用你的内部 DSL,那么请确定没有任何针对应用程序的业务逻辑掺杂其中,而是提供一个机制来俘获业务逻辑。

最后,告诉你应当如何使用哪种语言创建出一门内部 DSL 的圣经,是不存在的。在这个主题上我所读到的所有文章中,绝大多数都认为你必须边做边学。留意观察客户和领域专家如何沟通他们的需求,并且专注于使用可执行的代码来捕捉这些需求。即便你无法将你的领域专家转变成一个外行程序员,他们应当可以在一次代码评审或者一个结对编程过程中明白清楚地看到他们的需求,而你的目标则应当是,在你得到他们需求的同时就马上捕捉下来。

查看英文原文: Agile Asset Management with Ruby DSLs

2007-09-24 23:302293
用户头像

发布了 117 篇内容, 共 18.7 次阅读, 收获喜欢 0 次。

关注

评论

发布
暂无评论
发现更多内容

成为数字游民,他们为何「All in Web3」?

One Block Community

程序员 web3 数字游民

【Python 基础学习】-运算符

度假的小鱼

11月月更 Python运算符 Python运算符优先级

ONE 2.0应用场景解读 | 如何通过时序拓扑直观还原故障传导链路?

博睿数据

可观测性 应用场景 智能运维 博睿数据 ONE平台

软件架构的定义与分类

穿过生命散发芬芳

架构 11月月更

“链游版羊了个羊”Matching Game即将登陆Gate

股市老人

支持向量机-非线性SVM与核函数

烧灯续昼2002

Python 机器学习 算法 sklearn 11月月更

如何使用Git进行代码托管

我是一个茶壶

git 代码托管 11月月更

一文了解 Go 的复合数据类型(数组、切片 Slice、Map)

陈明勇

Go golang go基础 11月月更

Web3领域首个三消小游戏Matching Game,近30交易量破800万U

BlockChain先知

聊聊Mybatis的类型转换注册类TypeHandlerRegistry

急需上岸的小谢

11月月更

华为云区块链三大核心技术国际标准立项通过

科技怪授

第三章 TCP/IP ip地址概念与应用

我叫于豆豆吖.

11月月更

2022-11-24:小团在地图上放了3个定位装置,想依赖他们进行定位! 地图是一个n*n的棋盘, 有3个定位装置(x1,y1),(x2,y2),(x3,y3),每个值均在[1,n]内。 小团在(a,

福大大架构师每日一题

golang 算法 福大大

从手动测试到自动测试,企业该如何选择?

飞算JavaAI开发助手

容器服务 ACK 结合 MSE Ingress,让集群入口流量管理更丰富、更容易

阿里巴巴云原生

阿里云 云原生 容器服务

如何用JavaScripte和HTML 实现一整套的考试答题卡和成绩表

葡萄城技术团队

聊聊Mybatis的数据源之获取连接

急需上岸的小谢

11月月更

广告业务存储神器:华为云GaussDB(for Redis)

秃头也爱科技

Linux 文件基本属性

芯动大师

chmod 11月月更 Linux文件属主 charp chown

【Python 基础学习】-变量和常量、输入输出

度假的小鱼

11月月更 Python变量常量 Python变量

阿里 CTO 程立:Severless 化正加速重塑阿里应用架构和研发模式

阿里巴巴云原生

阿里云 Serverless 云原生

三分建设,七分运营|用现代化安全运营应对数据安全风险

爱科技的水月

云数据库时代,DBA将走向何方?-v4

科技之光

聊聊Mybatis的类型转换接口TypeHandler

急需上岸的小谢

11月月更

kitti数据集在3D目标检测中的入门(二)可视化详解

Studying_swz

人工智能 11月月更

感恩每一位 RockStar!

StarRocks

数据库

【Python 基础学习】-基础语法

度假的小鱼

11月月更 Python基础语法

支持向量机-探索核函数在不同数据集上的表现

烧灯续昼2002

Python 机器学习 算法 sklearn 11月月更

游戏品类加速回暖,文娱内容持续火热——2022年IAA行业品类发展洞察系列报告·第三期

易观分析

游戏 报告 文娱

折叠屏“世界杯”开哨,荣耀Magic Vs踢出关键一球

脑极体

使用Ruby DSL实现敏捷素材管理_Ruby_Jeremy Voorhis_InfoQ精选文章