Missing Guide to Service Objects in Rails - Riaz Virani

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hi everyone my name is riaz varani and this is the missing guide to service objects in rails so thank you for joining us i know it's a different circumstance than normal considering covet but i'm happy to have you so my name is riaz varani uh i'm a human male i live here i figured it's kind of redundant to put a picture of myself since you're also staring at my face but uh if you want to read more about my random musings on life you can do it at the url listed on the screen so uh i am a professional rubyist that's you would call i've been writing ruby pretty much every day for uh six years seven years something like that it's pretty much about the same time i realized as i've had this dog that's very sad very he didn't actually come in the mail we put him in there we might have been trying to mail him out but he didn't take that very well anyways uh back to real life and me not abusing my dog let's talk service objects which is what i told you we would talk about today so i've gotten sometimes the claim that you know we don't need them anymore in fact i think i saw a recent episode on uh rails with jason which is an excellent podcast where he was kind of going over maybe this is a concept that uh is a bit overused and is not actually needed in most applications and i listened to and i didn't exactly agree and if that's you and you don't agree with me that we should should uh be writing our code our business processes and service objects that's totally fine uh you're not a bad person if you you know don't use service objects you're not a bad person if you don't agree with me uh i sometimes find it a little annoying when presenters come on and tell me how i should do testing or devops or whatever and make me feel bad if i don't want to do it their way there's a lot of ways to do these things and if you know you are interested um and uh you you want to learn more about like ways you can do it then you know let's let's have a talk and let's see where we learn but before we do that let's start from the beginning and learn kind of how we got here and really we should just think about rails so rails gives you a lot out of the box and it's sort of designed to do that in fact we would call it a batteries included framework it's not a library and it gives you really the standard pieces that you need to get stuff on the page and i'm talking you know not only the standard mvc pattern but you even get things like active job to have a standardized interface for your background jobs and you get action mailer to be able to send out emails but at the most fundamental level to get something on the page you need a route in term that that defines kind of the url uh what controller it maps to which does all the work and it can include usage of models if it's having to save or create something or read something from the database and then a view to show what the result is on the page or potentially return json and this picture on the right really is just this nice elegant set of directories that you get from rails just by doing rails new and it gives you so much of the patterns and structures that you need out of the box in fact i would say that if you look at this rails example code it's pretty typical what i've seen when people are like hey here's how you write your controller it just looks so clean and so neat and so rails like you just take some params that you got from your view in the form uh whenever you're filling it out or i guess it could also just be a post from a json submission or ajax call um but then you pass it into a model and then you can sort of say hey it's saved let me redirect you somewhere else if i guess in this case it would have to be server rendered page or render another page and it just looks so elegant and i really appreciate how great that looks however in my experiences you know this looks great but then you end up with life happening and your real rails code might look more like this it's got a whole bunch of mess in there in terms of authorization and whether someone could create an order which is the example we're looking at how does the payment processor work and how does the pricing work and how does maybe notifying people via email after an order is created work and i i get it then putting this in the controller which is sort of not a thing most people do but i don't really think it matters whether you put this in the controller or if you put it in a model yeah you can break some of this out into sub methods um all in all it's just it's sort of like where rails ends in some ways where this is not just the basics of how to put something on the page but where all the business processes need to start so you know if you uh if you kind of sympathize with me and you kind of get what i'm saying that this is you know rails is great but there's just some parts and some patterns it doesn't give you for things you'll actually need in real life um and you've kind of coated yourself into a mess don't feel bad i think i've seen something like this you know the sort of the warts and the the um skeletons in the closet in most applications i've worked on even as a contractor they just always exist and you know the worst thing you can do is just sort of say no i have this perfect code and like look at my perfectly manicured lawn perfectly tested perfectly organized and so perfectly abstracted just doesn't exist in real life we don't want to be defeatist we don't want to say well it's all just going to be crappy and messy anyways no matter what i do and so i should just give in to that impulse there's got to be a better way and this is really where service objects come in and so now we're actually going to talk about service objects for real so what are service objects um and i don't actually think i've found a really great sort of succinct definition definition of what a service object is written in some blog post or whatever i've written kind of something like that i think it's you know code that represents and executes business processes specific to your application and that's pretty generic way to say it but uh it's the stuff that represents you know the process of like creating let's say an order or creating a user and what are all the steps and things that have to occur in that process it doesn't really represent like the specific order or user it doesn't represent um you know just a purely technical concept like a controller or a view might be but it really represents i think a business process so that kind of brings us to the next question in the meat of this talk whenever we kind of go down this route saying well we need to create this other thing called a service object there's actually a lot of things to think about among other things um and i find that we don't do a really great job of talking about it in the community there seems to be such an overwhelming number of talks and amazing content on say testing or devops or all sorts of other things but i've actually found that there weren't that many talks on how to do this and i think some part of it is that service objects because they represent business processes um you know they're kind of unique uh but i i found that there's definitely some patterns of how you organize that code that you can share so here's some questions we're gonna explore today so for writing our service objects you know do we use more oo or object oriented patterns within our service objects or maybe a little more of a functional approach you know inside the service object to organize what it's doing um how many public methods do we expose so if our controllers like service object do thing in our example case create order you know should it just say create order dot you know perform or should it be able to expose other methods into that service object uh how do we handle errors you know in our previous example we were doing all the stuff and then sending an email what if sending the email failed like do we roll back how do we kind of control the flow of logic when there's a sequence of things to occur and what does each service object return so if our controller or other context is is saying service object do a thing does it just get yep i did it or i didn't do it how does it kind of communicate to the outside world in a way that we can support kind of the things we need like you know displaying errors etc and also like where do we put this code because it's not a standard thing in rails right it's not one of those directories that we saw earlier uh do we just i don't know put it wherever uh do we put it next to the models i don't know um and finally uh when is lunch because i'm kind of hungry that's a pretty important question but we'll get to that by the end of this talk lunch okay no no lunch lunch is not happening so first of all let's talk about uh object oriented patterns versus functional patterns okay i'm going to show you some code examples to kind of make this more real world but just giving you a concept ahead of time object oriented patterns and service objects you know the nice thing is it'll just look like plain ruby it's you know maybe a class that says order creator and then it does a thing and it's familiar it looks a lot like what we call a poro and if you're not familiar with that term it just means plain old ruby object i put plain old but it might be old is the official way and it's just a class that doesn't inherit from anything or it's just plain ruby it seems to be kind of to me the most rails-us way to solve this problem because again uh it looks a lot like other concepts and rails however you know it's not necessarily the best fit i think for what a service object does because most service objects are really sequences of actions so in our previous example creating an order you take some params you might create the order price it send an email they kind of look more like the dreaded p word it's a procedure and that's what they actually are i know that's a very out of date way to describe them but that's actually to me what what service objects are doing and the final problem is if you use a very oo sort of structure for your service objects they can be hard to compose if you want to reuse context inside of them because your methods internal to them are really dependent upon the instance variables often that are in that particular context and so it's hard to reuse so let's take a look at some real code now to kind of actually hone in on this and there's a lot of code examples the rest of these slides to kind of elucidate these concepts they're somewhat contrived but i've definitely tried to make them as realistic as possible just to illustrate the concept so in this case on the left side you can see what our controller might look like in this case or any other place you're invoking this business process and instead of putting that logic to actually create the order in the controller we've now asked this other class called the order creator to do the work so we just create an instance of it passing in the params and the user we tell it to do its work and then we check to see if it worked or not so now we can test and work with our order creator irrespective of whether we've actually gone through our controller logic also good for testing so if you look on the right this could be a little bit of what it looks like so you have an initialize method it takes the parameters sets them as simple instance variables so if we look at the perform bang method it's essentially the thing that's doing the work and then calling other private methods in order to authorize build and price charge and send emails so you can kind of see what i meant by the fact that this kind of looks a little bit like a procedure you have a sequence of steps that generally have to occur in order um but the insides of those instance methods which again i wrote a couple of them you know just as examples here to see what could happen the insides you know they're interacting with the rest of the instance of it just via the instance variable so for example the build and price order is actually using an instance variable to set uh values of the order and actually accessing the params using uh you know the instance variable params so um this is sort of very very classic simple object-oriented design again not perfect but you can kind of get the uh the way that it works and if you need to kind of short circuit the whole thing you know and this and we're going to talk about this later like the various ways to do that in this case we're just raising you know uh errors uh in order to do that which you might raise custom errors in your case so it kind of makes sense that you have if you're familiar with basic sort of plain ruby this is a lot what it would look like but you have this object that represents what you need to do but now let's look at functional service objects so as an overall concept and i'm going to just be obvious about this i like this pattern a little bit better and that's why i'm probably going to say somewhat more positive things about it i do know that this is just not really the standard way that most rubyists think but i would encourage you to if you haven't done it before to take a look so the pros are that you know i think it's a better model for what most service objects are actually trying to do because they perform a sequence of steps in that previous example we kind of went down that authorize build order whatever and so it can be functional or procedural but i think some programming model that represents a sequence of things is actually a good way to think about this and i've also generally found with most of the tooling around this that whenever you have a lot that you need to do it's actually much easier to reason about a large sequence of steps in a functional pattern than try to figure out the exact right break points of how to make all of your different objects to do that work and yeah the last pro i like it better so you'll make me happy if you use it and that's always a pro should really be the main factor in most programming is whether i like it but people won't listen to me so anyways cons there is a slight learning curve if you're not familiar with functional programming if you've never used it before if you've come from maybe you know java to ruby or just been ruby and are very familiar with oo you know that's a pretty important consideration that you might just understand a different pattern better and you want to use it i will also say that um while in oo design it's easier just to use a poro because ruby itself is object oriented for the most part i find it much harder to really use functional programming as an approach or functional concepts as an approach for your service objects without using a third party library and later on we're going to talk about you know some of those examples there's interactor active interactor one i really like called light service um but the other thing to just think about is if you want to research this concept and have maybe some familiarity with other other things what we're really doing is you know a very similar thing to uh i guess a state moanad is one of my friends told me i know what that is i guess it's also called the railway pattern it's also called the command pattern i find they're all pretty much kind of a variant of the same thing so um i will also just say this because again a lot of people are not familiar with this uh this is going to be in the slides i found this uh slides on on slideshare and you can kind of get to it if you get the slides and click on this um it is the best explanation i've seen of how to do functional programming in ruby um so i i don't want to uh steal from it but i just wanted to kind of illustrate that if you wanted a deeper dive into this uh it's a fantastic fantastic look at this i also want to make a recommendation before we look at some code especially because as i mentioned it's hard to demonstrate this without a library i'm going to use light service which is one i've used for a long time and it's one of these libraries that helps with this problem of you know writing more functional ruby especially service objects so by code examples are going to use this library um it's pretty light on syntax but at the same time if you want to dig into it more i highly recommend looking at their github or using it so the functional version let's look at the left side where the controller is now you're pretty much going to see that it looks the exact same on the left side which is you know i named the service a bit differently just in a more verb format than a thing format um instead of an order creator just called create order and uh you can simply call it with some params and a user and then on the left right side again this kind of starts looking similar but it actually is a little bit different so in light service you have an organizer which organizes a sequence of steps and each step is called an action and then you only have one method which is called call so it takes your params creates a context which is just a bucket of data that's what that width line does and then it reduces or iterates over a sequence of actions and so our actions uh in this case are actually classes in the way light service does it and so we'll look at what those classes look like but we have an authorized class a build in price order class a charge order class they're not oo classes they represent just a step um but this is a a model that you know doesn't kind of by definition has to look and work like a sequence of steps so maybe this will be easier when we look at the next slide so this is what an individual action in the context of light service and this functional pattern might look now again if you use a different gem each step or the modeling of each step might have some different syntax but it is pretty similar conceptually so you have a concept of an action or a step um so in this case the action uh it can extend light service action and then the expects and promises are actually things inside of light service to say hey when i run i expect these two keys to be on that context so we in the previous step were able to take in the user and params and then put them on the context and so this action says hey like error if i don't see these on the sort of common context that has my data and i promise to add an order so during execution i need to add an additional piece of data um and so uh yeah if you look at the insides of that executed block it looks pretty similar and it kind of runs down the steps but you'll also see the very first line is the ctx or context.order where it's adding an order onto the context and then you'll also see below it doesn't actually need to raise whenever there's an error it can actually do a fail and return we'll talk about that in a sec well we should let's talk about it now fail in return is that you don't actually have to raise an exception in this case you can say hey this went wrong this isn't really exactly right let me specifically fail an error and stop the chain of events based on this particular error that i've had and so you can handle you know step four can error and actually the context and the whole system has a way of handling the pattern of rolling that up to the parent without raising an exception which is a really nice kind of thing you get alongside functional programming so that kind of brings me to the next point handling errors and this is the part i find absolutely trickiest uh that people don't think about you know i think it's really common for us to write our code initially for the happy path and not think like well these seven things are happening what happens if number four fails right do i just how do i bubble that up and how do i represent that to the controller and how do i return that in the view so this is really about when things go wrong um and in our first example of our object-oriented code you know you had those other private methods um and so what happens when it goes something goes wrong in step five or step six uh you know do you just return a value do you raise an exception i've found that there's maybe three kind of high level ways to tackle this problem so one is uh nested conditionals so a nested conditional just a bunch of sort of if else's you know do this and if it worked then do the next thing not saying this is a good idea but it's a way you can raise exceptions and so that's a way to short circuit execution wherever you are and it'll always just go up to the to the right catch statement or the right sorry rescue statement so that's one way to do it now there's some cons with this approach but that's all right there's also throw catch which is uh kind of like ray's rescue but it's not exactly the same and so we're gonna talk about where you might use it and i think in some ways it's a fairly elegant way to go about this problem so nested conditionals now i know i said i don't like to have absolutist statements and tell you what to do but i'm going to tell you what to do don't do this and if you look at that code example uh you know it's this is a bit more contrived but uh this is even very similar to what you know people can do when they kind of start out where you just end up with like do this if it worked and then you have to indent and then indent an indent um you know it's probably going to work fine in terms of a technical capacity in terms of the execution of the program but it is very hard to read and it's very hard to make changes it's very hard to break into smaller methods and you just end up with this thing that looks like a triangle in your code and you know you'll thank me later to warning you about warning you against this pattern so uh raising exceptions so maybe do this and i know exceptions uh you know they're very familiar to us there's a lot of uh gems that even uh run their control flow that way so for example like if you use the stripe gem and you're trying to run payments it communicates up to you that something went wrong using an exception and you'd have to really worry about how you've broken apart your code because if you do a bunch of things and break things into little private methods you know returns are how you want to normally do things but you know if you have a thing calling a thing calling a thing the return only goes one level up and now that thing has to be like oh that didn't work and then has to roll it up well raising an exception will just jump straight to the top where you can rescue at the very top most level and you can write custom errors that's great there is a con to this um exceptions are very slow this is something i don't think we talk about enough they're very slow the reason is that whenever you do a raise it's actually meant to be when something went wrong and just blew up in your system and so ruby has to collect the entire stack trace at the time and communicate that upwards now you may be rescuing that exception later on and actually using it as a known part of your application but that stack trace still has to be grabbed and it tends to be a pretty common cause of performance issues and applications that rely upon you know raising exceptions as a very common thing across their application if you look at the code on the right you can see you know you could have this is the oo version um you know a method to sort of check authorization and if it doesn't work erase some custom error and then the outside or maybe the top level inside your service or even in the controller you can rescue a custom error to say oh it didn't work now let me handle it so throw catch now this is somewhat similar to raising an exception i think this is probably a good way to go about it um it's semi-familiar to ruby's i'm sometimes uh find that people aren't as familiar with throw catch they think it's just ray's rescue but it's actually not and you get the same benefits of not having to worry about how you've broken apart your code so if you've taken your one long method and broke it into 17 smaller methods you know if you in a very deeply nested method throw it'll still catch wherever up of the chain that message is caught it's also not slow because it's actually designed for control flow it's not designed for exception handling so it doesn't have that same conceptual interference with raising an actual exception when things went wrong and it does look pretty similar too so the inside of wherever you are doing your work can throw a symbol in this case like throw error in this example with a message um and then the place that's calling your in this case authorization method can simply catch that exact string or symbol and now you have uh a result that says oh looks like this went wrong and if it you know is of length zero then uh it handles it for you now i will say this that if you're using the functional approach that we talked about earlier for example light service you'll actually find that this is what they do internally to do that fail and return which is kind of nice and in fact i think a lot of the other gems that i've seen that use the functional pattern are actually using throw catch internally for you so you don't even need to worry about this and so um the process of something going wrong is actually kind of built into functional programs where you'll have like or the command pattern where it's like well here's my success outcome and then here's my failure outcome which i kind of like and that's again one of the reasons i really like using those approaches to solve uh the problem of a service object or to build one of the service object out so return values what is a return value um and so return value is is really about like what does the controller see i can jump back a couple slides but it's like when you looked at the controller was like hey do order creation and save it in result and then saying was result success or failure that thing that's the result that's the thing that the controller or whatever is calling your service object gets to see or understand about what the result was of like please try this did it work did it not work um and so uh you're generally gonna be calling this from the controller and so what should the controller be able to see um and so at the most simplistic level i'm gonna have three options maybe more but here's the three big ones you can literally have the execution of a service object uh just return a boolean saying true false that worked it didn't work um that will work but i think it's got a lot of limitations um you can also have it return a struct or an open struct and we'll talk about what those are if you're not familiar with them but it's essentially a smarter hash that has data in it and so you can ask it was it successful was it false what was the error message and it just had it's a bucket of data that can hold that in your service object and construct it you can also use custom classes so again just use a poro again a plain old ruby object it could be you know a order creator result class that knows how to know if it succeeded if it failed what data it has inside of it etc so let's look at the true false the pros are that it's just crazy simple and universal i mean it's just very easy to understand but i think it has a lot of faults so if you just return a boolean you know you might have had a case where you created an order in the part of our example but because you just return true false the controller doesn't have access to the actual order so it might make it very hard for example to redirect to the path of that order and also you don't have any context of the error so if you're returning false and saying well actually you know maybe the user put in a bad email and they couldn't make this order given that data you don't actually have a way to say what was the result.error message so if you look at the code on the right if the result in this case was just true false then you can't really access you know that the line below isn't really possible and the line even below that in red where you want to access the air isn't exactly possible either if you want to set like a flash message or something like that so structs and open structs they're definitely fairly easy to use i think these are pretty good balance and this is the most common way of seeing people um at least in the ooo pattern respond with uh a data out of their service object you can define custom payloads per service so like open struct is sort of you can put anything in it you want if you make structs again you can define custom keys i'm not going to go into the details of structs but i'm just using open structs here because they're a bit more flexible to demonstrate and you can attach air context in a dynamic way so again each open structure what result is can be whatever you want i think that you know it sometimes could be a little too flexible where you can't predict in the controller exactly what data is going to be in the open struct but that being said if this works for you i don't really see a problem with it so custom classes again uh for the most part i think they're probably not needed because your result is usually just a bunch of data representing the result and so an openstruct is fine but classes are great whenever you want to make them flexible and direct or maybe you want to inherit from a common result class that knows how to you know maybe format the error message or something like that again it's familiar to those who prefer oo and if you look at the example on the right uh the top part is really what the service would look like and at the end the thing it's returning in this case is a instance of a success class and so um your controller would the result would be an instance of the order creator success class and it has inside of it you know maybe access to the order it can say what whether it was successful or not um and again you could extrapolate from this by having common success classes that everything inherits from you can go crazy so again it's very flexible very direct but um the con is maybe gives you a bit too much power to do things that become uh opaque and confusing and add more complexity where a simple bucket of data like a struct or an open struct would have done just fine so uh next question where do i put this code that you've been talking about all these fancy service objects this one is really simple to me and i actually don't have a great recommendation don't let the internet internet confuse you with too many opinions if you start researching this put the code wherever you want to as long as it has a pattern um and generally i think it's better to group by domains because you could just have for example um app services but you don't want just like 150 files and app services maybe you know namespace them a bit is like app services orders create order update order etc app services users create user update user just use your your own kind of common sense i would just say keep it extremely simple and keep it direct and don't worry about really fancy organizational structures all right so that's my talk about service objects i normally would be doing a q a at this point but of course we're going to be doing that on discord for those of you that are attending the conference and i look forward to your questions at that point
Info
Channel: Ruby Central
Views: 2,439
Rating: undefined out of 5
Keywords:
Id: XH1fbvexqyU
Channel Id: undefined
Length: 30min 56sec (1856 seconds)
Published: Tue May 18 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.