1

From my research, it seems there's a general consensus that sending email is something that belongs in the Controller. So for example, if I have a signup form for People, when a user submits a signup form, I would validate the Person, and then once the Person is saved, the People controller would do more stuff - for example, send an email confirmation message, send a welcome email with attachments, and send an email to an admin.

That's fine until there's another part of the application that ALSO creates people. Easy enough to call the Person model and create(), but what about all that extra stuff that might (or might not!) need to happen... should the developer have to remember to do all that stuff in any controller of the application? How do you keep your code DRY in this case?

My inclination was to make an "after create" filter in the Person model, and perhaps add an optional parameter that would disable sending of email when a Person is created, but testing becomes a nightmare, etc.

How do you keep from having all the other parts of the application have to know so many rules about creating a new Person? I want to refactor, but not sure which direction to go.

tereško
  • 58,060
  • 25
  • 98
  • 150
Franc Amour
  • 301
  • 1
  • 11
  • One other thing that i toyed with, and would like comment on: At one point, i had set up a sort of "observer" pattern, where various controllers could simply "announce" that something has been done, and then one or more listeners could act upon that event. In this example, the "Comments" controller could create the User it needs, and not care whether there's a business rule that indicates the emails that shoudl go out - instead, it just announces the new user and includes the user in the announcement. Then any number of listeners could act upon that, in the appropriate other modules...? – Franc Amour Nov 20 '16 at 14:37
  • Third option - would the Mediator pattern apply here in some way? – Franc Amour Nov 20 '16 at 14:40
  • Fourth option: Should all modules that need "new people" talk to a PeopleService? What should the methods look like - module specific (createNewPersonForComment(personData)) - or should it be more functionality specific as suggested below (createNewPersonWithNotifications()) ? I would lean towards the former as it's more clear what events should take place when a specific thing happens in the app...? – Franc Amour Nov 20 '16 at 14:43

4 Answers4

1

This is probably better suited for StackExchange's Software Engineering.

You have to consider that sending follow-up emails (like a welcome message) must not be linked to the creation of a new person. A function should always do just one thing and shall not depend on or require other functions to execute.

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

Your goal is to decouple every step in a process flow, so you can easily change process flow details without needing to change functions.

If your process flow occurs in different locations in your application, you should wrap the process in a function and call it. Imagine the above code in a function named createPersonWithNotifictions(data). Now you are flexible and can easily wrap an alternative person creation flow in another function.

Community
  • 1
  • 1
Alex
  • 7,743
  • 1
  • 18
  • 38
  • okay, so create another function very specific to that task... where would you put such a function? And if i need to send only one of those notifications instead of all of them... just add parameters, or a whole new function? I like the decoupling, but is this the only alternative, or is there an even better tried-and-true solution? I guess i'm thinking more about (and have in the past built) how a module could simply "announce" that something has happened, which could then be handled by the People module and any others i choose to add or remove later...? IoC is hard... maybe i'm too old! LOL – Franc Amour Nov 19 '16 at 13:48
  • Also, thank you SO much for your answer. I appreciate you!! Do you think I should move this question over to the SE Software engineering area? I'm still pretty green when it comes to SE and hate the thought of violating any rules, etc. – Franc Amour Nov 19 '16 at 13:52
  • Using my arcane example, is perhaps what i'm looking for some sort of "service" that the Comments controller is explicitly told about, against which is calls for creation of a new person and gets that person back, instead of talking directly to the Person model? – Franc Amour Nov 19 '16 at 15:00
  • 1
    I think you should move your question. By now you have probably received all the help you are going to get here. Another forum means another audience. – Dan Bracuk Nov 19 '16 at 15:28
  • Thanks Dan. Best way to move a question? (or do I just create a new one?) – Franc Amour Nov 20 '16 at 16:00
1

So, you create users in controllers and you create them somewhere else, and you want to keep DRY? This calls for a builder!

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

Usage:

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

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

RE additional info

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

This is ruby, it has built-in email capabilities. But fine, I'll play along. In this case, I would add some event/listener sauce on top.

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
Sergio Tulentsev
  • 226,338
  • 43
  • 373
  • 367
  • thank you very much for your detailed response. Not just DRY, but following best practices with regards to dependencies and IoC, which this definitely seems to do. Where I'm stuck with this is that the "builder" pattern seems most suited to simply constructing and returning objects with various complexities in their constructor ("telescoping constructor anti-pattern"), but not necessarily dealing with multiple steps that a controller would have, such as sending email. Your solution IS very close to what I had been considering. – Franc Amour Nov 20 '16 at 14:17
  • @FrancAmour: hm, you could put all those additional steps into `after_create`. This what it's for. – Sergio Tulentsev Nov 20 '16 at 14:49
  • That was rejected as it would make testing harder and you wouldn't always want email notification(s) to go out every time a person is created... only in specific cases. Plus it requires that the Person object be knowledgeable about email, which seems to break the single responsibilities principle. Also, CFWheels only provides sendEmail() for controllers, not models. – Franc Amour Nov 20 '16 at 16:10
  • @FrancAmour: nah, `after_create` in the builder, not user model. – Sergio Tulentsev Nov 20 '16 at 16:19
  • I think i'm getting closer to the epiphany i need, lol. Before i think too much harder, how does your solution compare with this: http://naildrivin5.com/blog/2012/06/10/single-responsibility-principle-and-rails.html (which i understand may be complete overkill unless really needed - others have rightfully advocated for duplicating code in the controllers for clarity instead of all this, of course). – Franc Amour Nov 20 '16 at 17:38
  • @FrancAmour: that one is fine too, but version with listeners is a bit more flexible, I think. That is, you can attach multiple listeners to the same event, whereas that version only allows you to send emails. – Sergio Tulentsev Nov 20 '16 at 18:43
  • I built something like this for CFWheels, but I did it wrong because it was impossibly hard to debug. Probably a limitation of CFWheels. To clarify the sendEmail() part of the discussion, sendEmail() is a cfwheels wrapper for coldfusion's cfmail tag, and behaves like Ruby's sendEmail() function, but i assume RoR makes sendEmail() available everywhere(?). Let me wrap my head around this, and spit back what i think I need to do... – Franc Amour Nov 20 '16 at 18:57
  • Would it be at all safe to say that one could simply create a convention (perhaps at the base Model.cfc that all model objects extend) where all the various filters simply broadcast that they have executed, and then register the appropriate Mailers as listeners? AfterCreate() would broadcast new_user_created or something? how would you pass on to the listeners the idea that we DON'T want them to listen at certain times (create a user, but do NOT send a message)? – Franc Amour Nov 20 '16 at 19:19
  • @FrancAmour: when you don't want listener to handle event, you don't subscribe it. Easy! :) – Sergio Tulentsev Nov 20 '16 at 19:20
  • I think what i meant was, i probably moved the broadcast up too far - putting it in afterCreate() in the Person model would mean the listener would always be fired, even if I didn't want a message to go out in a certain circumstance. So instead, the broadcast would (for example) stay down in the Comments controller that's creating the new user, after which it woudl broadcast "hey, a new comment created a new user" and then a mailer would listen for that. I guess. Still hard to troubleshoot (probably a wheels/cf limitation) and there's no wonderful pub/sub plugin for wheels. – Franc Amour Nov 20 '16 at 19:27
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/128585/discussion-between-sergio-tulentsev-and-franc-amour). – Sergio Tulentsev Nov 20 '16 at 19:40
0

Just add a Mail entity to the model. It will have the responsibility of sending a message of a particular type (Welcome, Notification, Error) to Person, Admin or Debugger. The Controller will then have the responsibility of collaborating with entities such as Person, Mail and Message to send the respective types of messages.

BKBK
  • 484
  • 2
  • 9
  • How and where would you actually add the mail entity to the Person model? each time a Person is requested? I guess i need a concrete example on this suggestion. – Franc Amour Nov 20 '16 at 14:24
  • A Person cannot own a Mail. The _model_ contains the _entities_ Person, Mail and Message. The responsibility of the controller is to coordinate requests. Thus, it may request Person to create a person, Message to compose a particular type of message, depending on which action the controller has just completed, and Mail to send the message to a person. – BKBK Nov 20 '16 at 14:36
  • Ah, I misunderstood what you meant by adding to the model. Unfortunately, in the context of CFWheels, the sendEmail() function is considered a controller-only function - they consider it almost like calling a view. Also, in the context of what a controller does vs. a model, I can see why they do it that way. But this still doesn't answer the actual question - how to keep any knowledge of "email" from the Comments controller's creating of a new person when creating a comment. Comments just shouldn't need to be responsible for that. It's looking more and more like I need a PeopleService. – Franc Amour Nov 20 '16 at 16:07
0

My final solution in CFWheels was to create a model object called "PeopleManager". I hated using "manager" in the name at first, but now it makes sense to me. However, if this fits a specific design pattern/name, i'm all ears.

Basically, the convention in my application will be that all the various modules that want a "new person" will need to go through the manager to get it. In this way, it is easy to control what happens when a new person is created, and for which areas of the application. For example, if a user creates a new Comment and their email address is not already a record in the People table, the Comment controller will be requesting a new person. When it makes that request of the PeopleManager, it is in that object that the business logic "When a new person is created from a Comment, send out a welcome message" will exist. While I'm not sure yet how the method names will pan out, so far I am considering going the route of "getNewPersonForComment"... and each module will have it's own types of calls. Repeated code in the PeopleManager (i.e. several of these distinct functions may all use the same steps) will be abstracted into private methods.

This provides a layer between modules and the data access layer, and also keeps the DAO type wheels objects from getting too "smart" and straying from the single responsibility principle.

I haven't worked out all the details yet. Especially whether or not a controller that will be using a Manager should be explicitly 'handed' that manager or whether simply treating the Manager as an object like any other (in cfwheels, model("PeopleManager").doSomething() is sufficient.

With regards to the differences between RoR and CFWheels when it comes to emailing, CFW does not have the concepts of a "Mailer" like RoR does, and the sendMail() function is a controller-only function. So, I have basically developed a mail queue feature that gets processed asynchronously instead, and will (hopefully) act much like the RoR analogue. This may become a CFWheels plugin. I have a feeling the need for this type of workaround revolves around the fact that controllers in CFW cannot easily call other controllers, and the debugging becomes nightmare-ish.

It's still evolving, and I welcome comments on my solution.

Franc Amour
  • 301
  • 1
  • 11
  • Since this solution was most closely inspired by the Builder pattern that Sergio provided, I will mark that as part of the answer as well. – Franc Amour Nov 21 '16 at 13:44
  • My solution was also inspired by the prior mentioned article: http://naildrivin5.com/blog/2012/06/10/single-responsibility-principle-and-rails.html – Franc Amour Nov 21 '16 at 13:46