翻译 你好,我是 PORO

early · 2017年11月11日 · 最后由 mengqing 回复于 2017年11月12日 · 10542 次阅读

本文翻译自: http://codesthq.com/blog/2015/hi-im-poro.html

翻译这篇简单的文章的原因主要有两点:

  • 1. 刚入行时被 轻controller,重model误导,也是当时自己理解辨识能力不足。当项目变大,业务逻辑越来越复杂时,你懂的...,所以有必要介绍一下 PORO。
  • 2. 社区里面 PORO 的介绍太少了。

译文:

很高心见到你

很多人学习 Ruby 是通过学习 Rails 框架开始的,不幸的是,这可能是学习这门语言最坏的方式。不要误解我的意思,Rails 本身是非常好的,它可以帮助你快速有效地构建 Web 应用,避免让你陷入很多技术细节之中。他们(译者:Rails 团队)提供了非常多的“Rails 魔法”来让事情变得简单。这对于新手来说是非常棒的,因为在这个过程中最令人愉快的时刻是当你说:”程序成功地跑起来了!“,同时看到所有的组件良好地整合到一起,还有很多人在使用你的应用。我们都喜欢做一个“创造者”。

但是,有一个方法,它可以在众多的程序员中判断出谁是好的程序员:好的程序员会明白他们所使用的工具是如何工作的。掌握工具的工作原理并不是说要知道它所提供的所有的方法和模块,但是要明白它是如何工作的,理解“Rails 魔法”是如何发生的。只有这样你在 Rails 中使用对象和编程时才会有舒服的感觉。

OOP(面向对象)的根基就是让复杂的 Rails 变得简单的秘密武器,也已经在 PORO 中提到过。PORO 是 Plain Old Ruby Object 的缩写。这个名字究竟有什么含义?为什么是极好的秘密武器?我是一个简单的没有任何继承关系的 Ruby 对象。是的,就是这样的。

class AwesomePoro
end

我可以在哪里帮助到你?

假设你在持续开发你的应用,在你的用户数量和他们的需求不断增加时,你会在项目里面增加各种功能。然后你会遇到越来越多的极端扭曲的逻辑盲点,那些连最勇敢的开发者也极力像躲开瘟疫一样躲开的地方。当这种地方变得越来越多时,管理应用就会变得愈发艰难。一个标准的例子就是:用某一个 action 处理新注册的用户,同时触发整个有关联的 actions:

  • 检查 ip 地址是否在黑名单里
  • 给新用户发送一封邮件
  • 给推荐者奖励
  • 给新用户创建相关的服务账户
  • 其他

简单的代码实现:

class RegistrationController < ApplicationController
  def create
    user = User.new(registration_params)
    if user.valid? && ip_valid?(registration_ip)
      user.save!
      user.add_bonuses
      user.synchronize_related_accounts
      user.send_email
    end
  end
end

你已经写好了代码,一切都工作的很正常,但是,这些代码真的好吗?或许我们可以写得更好写?首先,它打破了单一职责的编程原则,所以我们确信可以有更好的方式。但是,怎么写呢?

这就需要用上面提到的 PORO 来帮助你了。上面的业务逻辑可以分离到一个 RegistrationService 类中,它只负责一件事情:通知所有的相关服务。通过相关服务,把上面的 action 中的行为分拆到服务中去。在上面的 controller 中你只需要去创建一个 RegistrationService 类的实例,同时调用一个方法 fire! 这样代码会变得更加简洁,我们的 Controller 占用了很少的空间,同时,每一个新创建的类实例只负责一个单一的任务,所以我们可以轻易的用下面的代码来代替:

class RegistrationService
  def fire!(params)
    user = User.new(params)
    if user.valid? && ip_validator.valid?(registration_ip)
      user.save!
      after_registered_events(user)
    end
    user
  end

  private

  def after_registered_events(user)
    BonusesCreator.new.fire!(user)
    AccountsSynchronizator.fire!(user)
    EmailSender.fire!(user)
  end

  def ip_validator
    @ip_validator ||= IpValidator.new
  end
end

class RegistrationController < ApplicationController
  def create
    user = RegistrationService.new.fire!(registration_params)
  end
end

然而,PORO 不仅仅是只用在 controller 中。想象一下你创建的应用在使用一个按月计费系统。它不关心产生计费的准确时间,我们仅仅需要知道它只关心特定的月份和年份。当然你可以设定每个月的第一天是哪天,并把信息存储在一个“Date”类对象(译者:这里的 Date 应该指的是一个 rails model)中,但是它既不是一个真实的信息,你的应用也不需要它。通过使用 PORO 你可以创建一个“MonthOfYear”类,这个类的实例对象可以储存你想要的准确数据信息。此外,当你引入模块“Comparable”时就可以迭代和对比这些对象,就像你在使用“Date”类一样。(译者:意思是用一个 PORO 类也可以实现类似 model 的功能,有些数据不需要储存到数据库,所以用 PORO 代替也行)

class MonthOfYear
  include Comparable

  attr_reader :year, :month

  def initialize(month, year)
    raise ArgumentError unless month.between?(1, 12)
    @year, @month = year, month
  end

  def <=>(other)
    [year, month] <=> [other.year, other.month]
  end
end

把我介绍给 Rails

在 rails 的世界里,我们习惯于每一个类都是一个 model,一个 view 或 controller。他们都有各自精确的位置,所以,我们可以在哪里存放 PORO 大军呢?考虑一下,第一个想法是,既然 PORO 不是 model,view 或者 controller,那我们应该把它放到“/lib”目录中去。从理论上讲,这是一个不错的方案,然而,如果所有的 PORO 文件将会放到同一个目录中,当应用变得很大时,这个目录将会很快变成一个你害怕打开的‘dark place’。因此,毫无疑问,这不是一个好的方案。

AwesomeProject
├──app
  ├─controllers
  ├─models
  └─views

└─lib
  └─services
      #all poro here

你也可以把部分非 ActiveRecord 类放到“app/models”目录中去,并把处理其他服务的类放到“app/services”目录中去。这是一个非常好的方案,但是有一个缺点:每次当你要创建一个新的 PORO 时,你都要决定它更多的是'model'还是'service'。这样的话,你可能会在应用中积累两个 dark places。

还有第三个方案,称作:using namespaced classes and modules,你需要做的就是,创建一个和 model 或 controller 有同样名字的目录,把所有 PORO 文件放到对应的目录中去。

AwesomeProject
├──app
  ├─controllers
   ├─registration_controller    #译者: 这就是创建的目录,一个目录对应一个Controller
    └─registration_service.rb
   └─registration_controller.rb
  ├─models
   ├─settlement
    └─month_of_year.rb
   └─settlement.rb
  └─views

└─lib

感谢这样的安排,当我们使用时,不需要在类的前面写命名空间。你有更简短的代码和更有组织逻辑的目录结构。

检查看看我写的!

describe MonthOfYear do
  subject { MonthOfYear.new(11, 2015) }

  it { should be_kind_of Comparable }

  describe "creating new instance" do
    it "initializes with correct year and month" do
      expect { described_class.new(10, 2015) }.to_not raise_error
    end

    it "raises error when given month is incorrect" do
      expect { described_class.new(0, 2015)  }.to raise_error(ArgumentError)
      expect { described_class.new(13, 2015) }.to raise_error(ArgumentError)
    end
  end
end

我希望能再次见到你!

我们这些例子清晰地展现了使用 PORO 改善了程序的可读性,让它们更加模块化,同时,也更容易管理和扩展。如果需要的话,拥抱 PORO,它促进了特定类之间的交流,也不会给其他模块带来干扰。它也使得测试程序变得更简单更快。此外,这种方式可以让 Rails controller 和 model 更容易保持简短,我们都知道在不断的开发过程中它会不断变大。

关于作者

KASIA — JAPAN ASTRONAUT

Konnichiwa! Although she is native Polish, she is in love with Japan and obviously speaks Japanese fluently! Katarzyna has excellent relations with clients and other Astronauts.

需要 登录 后方可回复, 如果你还没有账号请 注册新账号