本文翻译自: http://codesthq.com/blog/2015/hi-im-poro.html
翻译这篇简单的文章的原因主要有两点:
轻controller,重model
误导,也是当时自己理解辨识能力不足。当项目变大,业务逻辑越来越复杂时,你懂的...,所以有必要介绍一下 PORO。译文:
很多人学习 Ruby 是通过学习 Rails 框架开始的,不幸的是,这可能是学习这门语言最坏的方式。不要误解我的意思,Rails 本身是非常好的,它可以帮助你快速有效地构建 Web 应用,避免让你陷入很多技术细节之中。他们(译者:Rails 团队)提供了非常多的“Rails 魔法”来让事情变得简单。这对于新手来说是非常棒的,因为在这个过程中最令人愉快的时刻是当你说:”程序成功地跑起来了!“,同时看到所有的组件良好地整合到一起,还有很多人在使用你的应用。我们都喜欢做一个“创造者”。
但是,有一个方法,它可以在众多的程序员中判断出谁是好的程序员:好的程序员会明白他们所使用的工具是如何工作的。掌握工具的工作原理并不是说要知道它所提供的所有的方法和模块,但是要明白它是如何工作的,理解“Rails 魔法”是如何发生的。只有这样你在 Rails 中使用对象和编程时才会有舒服的感觉。
OOP(面向对象)的根基就是让复杂的 Rails 变得简单的秘密武器,也已经在 PORO 中提到过。PORO 是 Plain Old Ruby Object 的缩写。这个名字究竟有什么含义?为什么是极好的秘密武器?我是一个简单的没有任何继承关系的 Ruby 对象。是的,就是这样的。
class AwesomePoro
end
假设你在持续开发你的应用,在你的用户数量和他们的需求不断增加时,你会在项目里面增加各种功能。然后你会遇到越来越多的极端扭曲的逻辑盲点,那些连最勇敢的开发者也极力像躲开瘟疫一样躲开的地方。当这种地方变得越来越多时,管理应用就会变得愈发艰难。一个标准的例子就是:用某一个 action 处理新注册的用户,同时触发整个有关联的 actions:
简单的代码实现:
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 的世界里,我们习惯于每一个类都是一个 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.