CFWheels(或 RoR,或任何其他框架)发送电子邮件的最佳实践

Best practice CFWheels (or RoR, or any other framework) sending email

根据我的研究,似乎普遍认为发送电子邮件是属于 Controller 的事情。例如,如果我有一个 People 的注册表单,当用户提交注册表单时,我会验证这个人,然后一旦这个人被保存,People 控制器就会做更多的事情——例如,发送一封电子邮件确认消息,发送带有附件的欢迎电子邮件,并向管理员发送电子邮件。

这很好,直到应用程序的另一部分也可以创建人员。很容易调用 Person 模型和 create(),但是所有可能(或可能不需要!)需要发生的额外事情怎么办……开发人员是否必须记住在应用程序的任何控制器中做所有这些事情?在这种情况下,您如何保持代码干燥?

我倾向于在 Person 模型中制作一个 "after create" 过滤器,并可能添加一个可选参数,在创建 Person 时禁止发送电子邮件,但测试变成了一场噩梦,等等。

您如何避免让应用程序的所有其他部分必须知道关于创建新 Person 的这么多规则?我想重构,但不知道该往哪个方向走

这可能更适合 StackExchange's Software Engineering

您必须考虑发送后续电子邮件(如欢迎消息)不得与创建新人相关联。一个函数应该总是只做一件事,不应依赖或要求其他函数来执行。

// Pseudocode
personResult = model.createPerson(data)
if personResult.Successful {
    sendWelcomeMessage(personResult.Person)
    sendAdminNotification(personResult.Person)
} else {
    sendErrorNotification(personResult.Debug)
}

您的目标是解耦流程中的每个步骤,这样您就可以轻松更改流程详细信息,而无需更改功能。

如果您的流程流发生在应用程序的不同位置,您应该将流程包装在一个函数中并调用它。想象一下上面的代码在一个名为 createPersonWithNotifictions(data) 的函数中。现在您很灵活,可以轻松地将替代人员创建流程包装在另一个函数中。

只需将邮件实体添加到模型中。它将负责向 Person、Admin 或 Debugger 发送特定类型的消息(欢迎、通知、错误)。然后,Controller 将负责与 Person、Mail 和 Message 等实体协作,以发送相应类型的消息。

所以,您在控制器中创建用户,然后在其他地方创建它们,并且您想保持 DRY?这需要建造者!

class UserBuilder
  attr_reader :user_params, :user, :send_welcome_email

  def initialize(user_params, send_welcome_email: true)
    @user_params = user_params
    @send_welcome_email = send_welcome_email
  end

  def build
    instantiate_user
  end

  def create
    instantiate_user

    before_create(user)
    return false unless user.save
    after_create(user)
  end

  private

  def instantiate_user
    @user ||= User.new(user_params)
  end

  def before_create(user)

  end

  def after_create(user)
    # or do whatever other check you can imagine
    UserMailer.welcome_email(user) if send_welcome_email 
  end
end

用法:

# in controller
UserBuilder.new(params[:user]).create

# somewhere else
user_params = { email: 'blah@example.com' }
UserBuilder.new(user_params, send_welcome_email: false)

RE 附加信息

Also, CFWheels only provides sendEmail() for controllers, not models

这是 ruby,它具有内置的电子邮件功能。不过好吧,我会一起玩的。在这种情况下,我会在顶部添加一些 event/listener sauce

class UserBuilder
  include Wisper::Publisher

  ... 

  def after_create(user)
    # do whatever you always want to be doing when user is created

    # then notify other potentially interested parties
    broadcast(:user_created, user)
  end
end

# in controller
builder = UserBuilder.new(params[:user])
builder.on(:user_created) do |user|
  sendEmail(user) # or whatever
end
builder.create

我在 CFWheels 中的最终解决方案 是创建一个名为 "PeopleManager" 的模型对象。起初我讨厌在名称中使用 "manager",但现在对我来说很有意义。但是,如果这适合特定设计 pattern/name,我洗耳恭听。

基本上,我的应用程序中的约定是所有需要 "new person" 的各种模块都需要通过管理器来获取它。通过这种方式,可以轻松控制创建新人时发生的情况以及应用程序的哪些区域。例如,如果用户创建了一个新的 Comment,而他们的电子邮件地址还没有记录在 People table 中,则 Comment 控制器将请求一个新的人。当它向 PeopleManager 发出该请求时,业务逻辑 "When a new person is created from a Comment, send out a welcome message" 将存在于该对象中。虽然我还不确定方法名称将如何出现,但到目前为止,我正在考虑采用 "getNewPersonForComment"... 的路线,并且每个模块都有自己的调用类型。 PeopleManager 中的重复代码(即这些不同功能中的几个可能都使用相同的步骤)将被抽象为私有方法。

这在模块和数据访问层之间提供了一个层,并且还防止 DAO 类型的轮子对象变得太"smart"和偏离单一职责原则。

我还没有弄清楚所有的细节。特别是将要使用管理器的控制器是否应该显式 'handed' 该管理器,或者是否简单地将管理器视为像任何其他对象一样的对象(在 cfwheels 中,模型("PeopleManager").doSomething()足够了。

关于 RoR 和 CFWheels 在电子邮件方面的区别,CFW 没有像 RoR 那样的 "Mailer" 的概念,并且 sendMail() 函数是一个控制器专用函数。因此,我基本上开发了一个邮件队列功能,该功能改为异步处理,并且(希望)会像 RoR 模拟一样工作。这可能会成为一个 CFWheels 插件。我有一种感觉,需要这种解决方法是因为 CFW 中的控制器无法轻易调用其他控制器,并且调试变得噩梦般。

它仍在不断发展,欢迎对我的解决方案发表评论。