新手问题 Repository 模式如何处理多层关联?

mizuhashi · 发布于 2016年12月07日 · 最后由 mizuhashi 回复于 2016年12月09日 · 455 次阅读
23529

场景1:我们有一个两层关联,在ActiveRecord里面这样写:

orders = Order.where(id: [1,2]).includes(order_items: :product)

在Lotus里应该怎么写呢?按照官方的教学,查询应该直接写成一个方法:

order_repo = OrderRepository.new
orders = order_repo.find_with_order_items_with_product_by_id([1,2]) # => Array

但是问题来了,order_items和product的关联是写在OrderItemRepository里的,OrderRepository怎么知道存在这个关联呢?

为了保持OrderRepository的纯洁,我们只能把脏活交给Controller干:

order_repo = OrderRepository.new
orders = order_repo.find_by_id([1,2]) # => Array
order_items = OrderItemRepository.new.find_with_product_by_orders(orders) # => Array
order_repo.bind_order_items(orders, order_items)

数据总算是取出来了,可是order_items和order的关联弄丢了,没关系,我们再用一个方法补上。

取数据的部分完成了,看上去干了不少(?),但是总觉得怪怪的,Repository的读逻辑往外层移动了,而且很难界定各部分逻辑应该由哪个Repository完成。

=======

场景2:我们要创建一个聊天,并加上一条回复,其中涉及到三个表的关联,在Rails里这样写:

conversation = Conversation.new
reply = conversation.replies.new(message: 'xxx')
params[:attachments].each{|x| reply.attachments.new(file: x)}
conversation.save

在Lotus里,必须写成:

reply = Reply.new(message: 'xxx')
attachments = params[:attachments].map{|x| Attachment.new(file: x)}
ConversationRepository.new.create_with_reply_with_attachments(conversation, reply, attachments)

class ConversationRepository < Hanami::Repository
  def create_with_reply_with_attachments(conversation, reply, attachments)
    conversation = create
    reply.conversation_id = conversation.id
    saved_reply = ReplyRepository.new.save(reply)
    attachments.each do |att|
      att.reply_id = saved_reply.id
      AttachmentRepository.new.save(att)
    end
  end
end
#其实真正lotus的repository是没有save的,我这里假装可以save entity,避免了传hash参数,如果是hash参数的话repository会承担更多的逻辑

为什么要把reply和attachment的数据都传到create_with_reply_with_attachments里呢?因为在create reply之前,attachment不知道reply的id,是不能保存的。所以你必须自己在create_with_reply_with_attachments里按顺序保存数据。

但是其实我们的ConversationRepository并不应该有Attachment甚至Reply的知识。这样的后果是,要么Repository变得越来越肥,要么是你还得像Rails一样,把create的逻辑写到controller里,这样Repository又再一次被架空了。

=======

以上两种场景都会有些逻辑的异位,不知道有什么好方法避免?

按我的理解,Repository的正确用法应该是:

data = repository.query # complex sql
service.process(data)
repository.commit(data) # complex sql

其中process不操作数据库,只在内存里处理entities,但是query和commit的sql其实都得自己写,而且这堆sql不知道如何SRP。大概最后也会变成薄service,厚repository。

一种想法是把data做成一个数据表的diff patch,在commit的时候自动apply,但是这其实就是AR的做法。。只需要在内存里构建好数据和关联,然后save就够了。

共收到 6 条回复
4215
chenge · #1 · 2016年12月07日

的确值得思考

96
xnnyygn · #2 · 2016年12月07日

讲一下个人理解Repository的用法,不一定是Lotus最好的用法。

逻辑上以Repository和模型一对一(不一定是一张表),多个Repository支持一个Service操作的方式区分代码职责。比如

Order.where(id: [1,2]).includes(order_items: :product)

拆分为

class OrderService
  attr_reader :order_repository
  attr_reader :order_item_repository
  attr_reader :product_repository

  def initialize
    @order_repository = nil
    @order_item_repository = nil
    @product_repository = nil
  end

  def list_with_item_and_product(id_list)
    orders = @order_repository.list(id_list)
    # fill orders with items and products
    # from order_item_repository and product repository
  end
end

controllers不处理类似代码,因为不利于代码复用。 第二个场景也是类似的方式。

class ConversationService
  attr_reader :reply_repository
  attr_reader :attachment_repository

  def initialize
    @reply_repository = nil
    @attachment_repository = nil
  end

  def save_reply(message, attachments)
    # validate parameters

    # begin transaction

    reply = Reply.new
    reply.message = message
    @reply_repository.save(reply)

    # create and save attachments

    # end transaction

    reply
  end
end

遇到类似的代码职责问题可以通过建立一个中间层来解决(但是记住不能建太多),这里实际上是一个业务逻辑层。

Rails自身在设计的时候有意忽视了这点(因为目标客户的原因),把Repository和模型合并(后果就是在没有数据库的时候你无法测试),预期Controller中不会有太复杂逻辑,采用了一些不是最佳的代码复用手段。理解这点后,你就可以不拘泥于Rails的设计而按照常规的软件架构(不仅仅是MVC)进行设计和编码。

以上仅作参考。

4215
chenge · #3 · 2016年12月07日

#2楼 @xnnyygn 比较赞同你的做法。补充一点就是,repo是否应该用参数初始化,这样便于测试的时候替换repo。

23529
mizuhashi · #4 · 2016年12月08日

#2楼 @xnnyygn 你这个就是普通的service嘛,我说放controller换成放service也一样,两者的抽象层次是等同的,问题是在这种情况下看不出Repository有什么意义,而且事实上也不能和别的Repository/Service解耦。

测试的话,其实厂妹也可以没有数据库的时候测试啊,或者用sqlite::memory也行,而且repository现实中不见得就能多数据源,因为join这些操作也不是所有数据源都有实现的。

9096
emanon · #5 · 2016年12月09日

先粘贴两个链接,不知道 lz 是不是需要:

领域模型的价值与困境

再论领域模型的困境


然后说点题外话。实际上我觉得写代码就是 3 件事:实现功能,去除重复,最后使表达清晰——主要是命名。

但是在完成后面两个目标的时候,特别是是“去除重复”这个目标的时候,我们会不断的遇到困难。这时候就不得不动用你所在的那个世界(那门语言)所能提供的所有方法资源:封装、组合、继承、mixin、元编程等等。

极端地去除重复必然导致模块粒度细化,各种最细粒度的模块互相结合又成为更粗粒度一些的模块,最后软件成为各种不同粒度(不同抽象级别)组合而成的一个组合体。它的内部因为没有重复而条理清晰。似乎很多写不干净代码的人会嘲笑这种情况只是个梦。

所以,我觉得没有必要去纠结太多概念,也没有必要迷信各种框架的写法。有了原则,什么都好办,没有太多固定的模式。

其实以我自己的一点经验,多写写脱离现有框架的东西。比如公司项目里看似比较边缘的,跟整体框架关系不是很紧密的,又没有现成框架可用的东西。把那些东西写干净的话,对自己的提升其实比在一个框架(比如 Rails)里写代码来得大得多。

希望以上这些能有点帮助。

23529
mizuhashi · #6 · 2016年12月09日

#5楼 @emanon 并不是纠结概念,因为要做side project,只是为了确认repository是无意义的,便可以从选型中去掉了,而且lotus这么多人捧不可能这些基础的都没考虑过啊…

目前的状况看lotus划的抽象比较蠢,写法也十分繁琐,肯定不会用他了,虽然我觉得他的container比rails engine简洁很多…

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