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

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

http://www.infoq.com/cn/articles/Agile-Asset-Management

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

相关厂商内容

Scrum、看板、XP看敏捷现状与未来

Hadoop、HBase、MongoDB和Cassandra等技术在当前的企业中的应用

云计算平台面面观,从架构到实践

深度剖析顶级互联网公司开放平台布局

新浪首席DBA杨海朝讲述MySQL性能优化

相关赞助商

QCon全球企业开发大会(杭州站),报名启动9月30日前9折优惠!。

问题的提出

我在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|
    # configuration
    rdoc.rdoc_dir = 'doc/my_docs'
    rdoc.main = "doc/README_FOR_APP"
    rdoc.title = 'Comprehensive Documentation'

    # app documentation
    rdoc.rdoc_files.include('doc/README_FOR_APP')
    rdoc.rdoc_files.include('app/**/*.rb')
    rdoc.rdoc_files.include('lib/**/*.rb')

    # plugins documentation
    rdoc.rdoc_files.include('vendor/plugins/*/lib/**/*.rb')

    # framework documentation
    # ...snip...
  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|
    t.src_files   =  image_src 'images/*.jpg'
    t.build_path  =  build     'greyscale_thumbnail'
    t.remote_dirs << REMOTE_DIR[:greyscale_thumbnail]
    t.transformation do |img|
      # apply bronze image effect
      img = img.quantize 256, Magick::GRAYColorspace
      img = img.colorize 0.25, 0.25, 0.25, '#706000'
      # crop to thumbnail size
      ...snip...
    end
  end

上面的代码定义了以下Rake任务,用<strong>rake -T</strong>可以得到以下描述:

rake assets:bronze_thumbnail:build                    # Build the bronze_thumbnail files
  rake assets:bronze_thumbnail:clobber                  # Remove bronze_thumbnail files
  rake assets:bronze_thumbnail:rebuild                  # Force a rebuild of the bronze_thumbnail files
  rake assets:build                                     # Build all assets
  rake assets:clobber                                   # Clobber all assets
  rake assets:rebuild                                   # Rebuild all assets

我意识到,如果描述这些图像变换的代码与客户给我们的PhotoShop指令非常相似的话,这个工具将变得极具灵活性——代码就成了为图形变化定义Rake任务的内部DSL了。

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

define_image_transformation 'thumbnailize' do
    crop_to '62x62', :north
  end

  define_image_transformation 'bronze' do
    greyscale
    lighten
    #    r     g     b     tint
    tint 0.25, 0.25, 0.25, '#706000'
  end

  image_task 'bronze_thumbnail' do
    from images 'images/*.jpg'
    to   build 'greyscale_thumbnail'
    remote_dirs << REMOTE_DIR[:greyscale_thumbnail]
    transformation do
      bronze
      thumbnailize
    end
  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

<script type="text/javascript"></script><script src="/scripts/forum.js?20110705" type="text/javascript"></script><script src="/dwr/interface/ForumNotifications.js" type="text/javascript"></script><script src="/scripts/jquery.timeago.js" type="text/javascript"></script><script src="/scripts/date.js" type="text/javascript"></script>

相关推荐