Introducing Lotus - A complete web framework for Ruby
介绍:http://lucaguidi.com/2014/06/23/introducing-lotus.html
网站做的很漂亮。代码按不同组件分成了单独的 github repo,比较适合阅读学习。
暂时发现几个特点:
1, 颠覆 Rails 命名系统,直接使用 Ruby namespace 来管理。我觉得这个好,又简单又灵活。
不明白的地方:
#1 楼 @fredwu 是的,很适合阅读学习:-) #5 楼 @billy Lotus::Model 不支持 migration。他应该是把这件事交给实现了。如这个例子里用了 Sequel: https://github.com/lotus/model/blob/master/EXAMPLE.md#setup
一看 model 的各种 EJB 名词就关了...
正常脑子都会想到 instance 和 class 而不是 entity 和 repository, 语言提供的设施不用,自己造一堆是为何...
刚才在 Skype 上和作者 Luca 聊了一个多小时关于 Lotus::Model 的开发计划,受益不浅。相比较 Datamappify 的 Entity + Validation 和 Repository + Mapper 的设计,Luca 的 Lotus::Model 把所有东西都细分化了,架构更明显。接下去我会尽量抽点时间来为 Lotus::Model 做点微薄的贡献。
看到@ashchan 力推 Lotus 这个新的 web framework,大感兴趣研究一番。 但在看了 2 个 demo(https://gist.github.com/jodosha/9830002 和 https://github.com/sidonath/room-reservation)除了 在https://github.com/lotus/lotus 中介绍的 Microservices architecture(相当于 rails 的 engine 功能或者 Padrino 的 Mountable app)这个有意思外,其他的部分感觉是为了在做一个 framework 而创造 framework。
以下是吐槽(代码摘自和修改自 2 个 demo 以及 lotus 的 example)。
然后在每个 action class 里自己来实现一个 call(params) 方法里写处理逻辑(有种 rack 风格的味道),如:
class RoomsController
class Index
include RoomReservation::Action
expose :rooms
def initialize(repository: RoomRepository)
@repository = repository
end
def call(params)
@rooms = @repository.sorted_by_name
end
end
class Show
include RoomReservation::Action
def initialize(repository: RoomRepository)
@repository = repository
end
def call(params)
@model = @repository.find(params.fetch(:id))
end
end
虽然如设计理念上是容易测试了,如:
action = Show.new
response = action.call({ id: 23, key: 'value' })
但感觉是在把东西做复杂了,而且单独测试一个 contoller 的场景多吗?更多的恐怕是集成测试。
虽然有简单的 DSL 语法如:
class HomeController
include Lotus::Controller
action 'Index' do
expose :planet
def call(params)
@planet = 'World'
end
end
end
但看到一个 controller 文件里一大片的def call(params)
出现,DRY 的冲动就忍不住涌现。
一个折衷的方法是再定义个 dsl 语法来 DRY,例如:
class HomeController
include Lotus::Controller
def_action 'Index' do (params)
@planet = 'World'
end
end
例子:
module Rooms
class Index
include Lotus::View
layout :application
end
class Show
include Lotus::View
layout :application
end
class New
include Lotus::View
layout :application
def form
FormPresenter.new(locals[:form])
end
end
Create = New
class Edit
include Lotus::View
layout :application
def form
FormPresenter.new(locals[:form])
end
end
Update = Edit
end
然后每个 view 就需要对应的 template(如 rails 中的 view,也就是一些 erb)。 这个感觉有点像 cells(https://github.com/apotonick/cells)的概念。 但做 web 应用有必要这么复杂吗,除了某些场景需要复杂处理的 form 等(这时可以用 simple_form/formtastic/reform(room-reservation 的 demo 中用到)) 等 form helper,其余 90% 的就是个 erb。
对应 rails 的 view helpr 的是 Lotus::Presenter。 这个名字改得好,rails 中的"helper"这个词个人感觉有点太空泛了,让人不容易直接能联想到是跟 view 相关。但只是有必要每个 presneter 都也要是个类吗,我觉得在 rails 中比较好的解决方案是 Draper 或者 ActiveDecorator。
用 model 负责业务逻辑,持久化用 repository,详细见http://msdn.microsoft.com/en-us/library/ff649690.aspx
例子:
author = Author.new(name: 'Luca')
AuthorRepository.create(author)
author.name = "Luca"
AuthorRepository.update(author)
AuthorRepository.delete(author)
author = AuthorRepository.find(12)
要不要这么 raw,至少可以加个委托对象,如
author = Author.new(name: 'Luca')
author.repository.save
author.repository.delete
非常同意@luikore的看法:“正常脑子都会想到 instance 和 class 而不是 entity 和 repository, 语言提供的设施不用,自己造一堆是为何...”。
lotus 中 MVC 运作的概览,看一下从如何响应 new action 显示一个 form 到保存一个 room 到数据库所需要涉及的代码:
# controller
class RoomsController
class Index
include RoomReservation::Action
expose :rooms
def initialize(repository: RoomRepository)
@repository = repository
end
def call(params)
@rooms = @repository.sorted_by_name
end
end
class New
include RoomReservation::Action
expose :form
def call(params)
@form = RoomsFormFactory.create
end
end
class Create
include RoomReservation::Action
expose :form
def initialize(repository: RoomRepository)
@repository = repository
end
def call(params)
@form = RoomsFormFactory.create
room = @form.populate(params.fetch(:room), self)
@repository.persist(room)
redirect_to @router.path(:rooms)
end
end
# view
module Rooms
class Index
include Lotus::View
layout :application
end
class New
include Lotus::View
layout :application
def form
FormPresenter.new(locals[:form])
end
end
Create = New
end
# tempalte
<form action="<%= router.path(:rooms) %>" method="POST">
<div class="row">
<div class="small-12 columns <%= form.error_class(:name) %>">
<label>Name:
<input type="text" name="room[name]" value="<%= form.name %>">
</label>
<% form.errors_for(:name) do |message| %>
<small class="error"><%= message %></small>
<% end %>
</div>
</div>
<div class="row">
<div class="small-12 columns">
<a href="<%= router.path(:rooms) %>">Cancel</a>
<button type="submit">Create new room</button>
</div>
</div>
</form>
# presenter
class FormPresenter
include Lotus::Presenter
def error_class(key)
return nil if errors[key.to_sym].empty?
'error'
end
def errors_for(key, &block)
errors = self.errors[key.to_sym]
return if errors.empty?
yield errors.join(', ')
end
end
# form factory
module RoomsFormFactory
def self.create
RoomForm.new(Room.new)
end
end
# form
class RoomForm < Form
property :name
property :description
validates :name, presence: true
validates :description, presence: true
end
# model
class Room
include Lotus::Entity
end
# repository
class RoomRepository
include Lotus::Repository
def self.sorted_by_name
query { order(:name) }
end
end
对比 rails 以及 padrino 的实现,会感觉比较“啰嗦” 。 总体给我一种感觉是要把所用东西都要“类”化(作者是不是从 c#/java 转型到 ruby 的?),对于 web 开发却是有种化简为繁的感觉,如果不提供多些 DSL 或者 generator 来简化一些常见的类定义,将会是个 java 形态的 web framework。
#20 楼 @rainchen #21 楼 @ashchan #22 楼 @hooopo
Lotus 的设计理念是 decouple 至上 —— 这是从 Luca(作者)他们团队项目的高复杂度中衍生出来的。这点我非常能够体会到,因为我们团队现在开发的应用也是高复杂度,大部分逻辑都是在 Rails 以外实现的。
@rainchen 吐槽的绝对合理,但是其实只是因为框架还未成熟。从 decouple 化的架构增添 DSL 或 abstraction layer,要比 decouple 像 rails 这样庞大的框架要容易的多了。
Lotus 目前为止毕竟是 Luca 一个人的产物。很多 API 还不人性化是肯定的。作为开源的一个项目,还是需要社区的支持的。这几天 Luca 开始添加 issues 了,比如 Lotus::Model 的:https://github.com/lotus/model/issues 我会尽量帮助开发 Lotus::Model,希望我们社区有兴趣的人也能尽量添一笔力量。
对于这点——
非常同意@luikore的看法:“正常脑子都会想到 instance 和 class 而不是 entity 和 repository, 语言提供的设施不用,自己造一堆是为何...”。
我完全不赞同。instance / class
与 entity / repository
完全是两码事,在同一个句子里出现有些古怪。如果你们看过 ROM 或是我的 Datamappify 的话,就会了解为什么要 entity / repository
了。对于简单的应用(比如一个 blog)而言,ActiveRecord 很方便,但对于搞复杂度的大型应用而言,data mapper 这个 pattern 的优点远大于缺点。
Data mapper pattern 最重要的一个优势,是在 Datamappify 的 README 里提到的:
The coupling between domain logic and data persistence.
DSL 可以不断的优化,但是基本的设计理念是非常难修改的。Rails 有几千个人提交代码,但 ActiveRecord 始终是一个难题。
DSL 的优化当然也要看是否与设计理念产生冲突,比如说 @rainchen 提议的这个委托 DSL:
author = Author.new(name: 'Luca')
author.repository.save
author.repository.delete
这个就与设计理念有冲突。因为作为 entity 而言,是不允许有任何的 repository 的 knowledge 的。这个不仅仅是设计 DSL 的问题,而是代码的 semantic 和对象的 responsibility 的问题。
当越来越多的 ruby/rails 程序员跑去其他社区(go 啊,elixir 啊,closure 啊之类的),ruby 社区需要像 Lotus 这样把设计放第一位的框架。
我为 Rails 做过贡献,但是每次打开 Rails 的源代码,都是这个表情——
而我初次看 Lotus 的代码时,表情是这样的——
repository 和 entity 的逻辑不是说分就能分的. 如果一个方法既包含业务逻辑,又包含持久化动作,应该放到 repository 还是 entity? 如果放到 repository, 它就必须有 entity 的知识,如果放到 entity, 它就必须有 repository 的知识。如果将两者完全隔离,那么这种方法就要放到更高一层,放到 controller 中,当 controller 膨胀了,就会想到加个 service 层... 层数越来越多,架构越来越 fancy, 做实际事情的代码却越来越难找...
AR 中拆分逻辑也很容易啊,可以 extend, 可以加 scope, 方法多种,又不需要定死在一个 repository/entity 的划分方式中,一开始就强制拆分开来把很多本来简单的 model 都弄复杂了。
#23 楼 @ShiningRay #25 楼 @fredwu
我只说一点个人感觉 model desgin 这快“怪味道”最浓烈的地方:用 repository 和 entity 分离逻辑和持久化后,但在 controller 里又用到 repository 来 select 数据,难道还能说 repository 就跟业务逻辑“分离”了?
我觉得 Rails 的 MVC 结构足够好,需求复杂的可以再加自己需要的层。Lotus 起点就引入这么多层次和模式,我不理解喜欢这样代码的人,不过也好,喜欢的人可以投入 Lotus,不用要求 Rails 迎合他们的趣味了。
#32 楼 @fsword 譬如想在 lotus 上写个分页插件或者上传插件,会发现每个层都要修改,这分页插件的 api 怎么设计头大得很... 当然 rails 也要,不过层数少点更容易做
#33 楼 @benzhang 这样划分也可以,entity 专注无 IO 的内容,带 IO 的方法提升到 repository 中去,单元测 entity 就容易很多,repository 的方法可以放到集成测。不过和很多 java 项目类似,结果往往是绝大部分代码都会落到 repository 中去,而 entity 只剩下一个只带属性的空壳子...
至于不连接数据库,只用 memory adapter 模拟持久化的做法,也不是 repository pattern 专用的,ActiveRecord 也能做到,但做多就变成 OODB 了还不如不用数据库和 ORM 了直接上 maglev gemstone
If anything, controller actions are the service layer. Which is the point. Don't put another service layer on top of it. - DHH