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