翻译 你好,我是 PORO

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

本文翻译自: 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.

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