- Read Tutorial
- Watch Guide Video
Great job in going through that section. You should now have a better idea on how controllers work in Rails, specifically how they can manage data flow and each one of the responsibilities they have when it comes to managing the application.
In this deep dive. I want to take a look at a very special topic called the null object pattern. Now if you've never heard of that before don't worry. It's a design pattern in the software development world. Essentially it protects us from errors and it can allow us to extend our applications with a little bit more intelligence.
For example, let's say that we have our concept of users, which our app has, and we have our current user method. That's something that's provided by devise. We've shown how we can call that method and then on certain pages we can put current user first name and that is going to call the user database and then it's going to call that first name which has the virtual attribute that we built.
What happens if someone who's not logged into the system goes to that page. Our application is either going to throw an error because there is no such thing as a current user or we're going to have to build a bunch of work arounds which is really a bad practice.
In this deep dive we're actually going to stick with our application. I know most of our deep dives we take a different application and we isolate that, however, this is a perfect fit for what we're actually building so we're going to stick with it and we're going to implement the null object pattern. Which means that from now on after we've done this whenever a user goes to a site whether they're logged in or whether they're logged out, we're going to be able to give custom behavior based on if they're logged in or out. We're not going to handle it in the view, that would be a bad practice. Instead, we're going to manage it from the controller side and that's the reason why this is the deep dive for this section.
Let's jump into the demo and let's implement the null object pattern.
I have our application running and I do not have a user that is logged in and we can see first and foremost one of the little bugs in our application. Where it says "Hi" and then has nothing here at all because no one is logged in. If I log in then it'll say Hi Jordan and this is not a very good user experience. You wouldn't never want to have application that has a greeting and then has just a blank empty space.
There is another problem with this code, I have our application.html.erb file and our home.html.erb file open, the other issue is this little piece of code.
This is considered a very bad practice in Rails development, it's considered being insecure about your code. Now if you've never heard of what being insecure means in relation to development. Essentially it means is that you're writing code in almost a defensive kind of manner because that's technically what we're doing we're saying we want you to show the current user and we want you to show their first name but only do that if a current user exists. The reason why we have to have this, watch what happens if I hit delete.
We get a no method error. It says undefined method 'first_name' for nil:NilClass. It even shows us right here in our home file. We are calling current user but now the problem is that current user doesn't exist when no user is logged in.
Current users is a method that's provided by devise. The issue though is that current user is only available when a user is signed in. And that's a bad thing because now we have to put in all these little conditionals like say I want you to do this only if the user is signed in. this is an example of a poor use of conditionals in the view.
Now there are times when a conditional is perfectly fine and where it's natural. For example in our application file eventually we're going to move this and we're going to implement what's called a helper method that's going to encapsulate all this data and we can use it in other parts of the app and it's considered a better practice.
Right now it's perfectly fine and this is all because here we're not being defensive about our code. We're adding a logical flow to our code. We're saying if the current user is here I want you to show the logout link but if there is no current user then I want you to show register and login. This is an example where conditionals are fine.
You won't find a lot of this kind of code in your views because it's considered a bad practice. And like I said eventually we are going to move this out into its own helper method so we can call this with a single line of code and it'll be a lot easier to read and not quite as messy here.
This is different though. We do not want to do these kinds of things like put in these defensive checks and so this is what we're going to implement in this deep dive is a way where we can be more confident about the type of code we write and what we're going to implement is called the null object pattern. Feel free to google it if you want to know more about it. I think the best way of understanding it is by actually building something.
I'm going to open the application_controller.rb because this is going to be where we are going to place this at least initially and then eventually we're going to move it into its own concern which you can kind of see my pattern with doing that. I like having my application controller file nice and clean like this. My pattern is to always start with this type of process. I always put my code in the easiest spot to place it so that when I'm testing I know that the code placement isn't the problem.
Say that I was to build the include method for the concern put all the code in there and then it doesn't work. I wouldn't know if the problem was a little typo in my concern, if it was the code placement or something like that. In the application controller, if I put a method in here I know exactly where it is. I know that all the other child controllers will have access to it and that it's easy to implement.
We're going to actually override the current user method.
One of the nice things about how Ruby works is you can override methods, in some languages this is called overloading methods. Essentially we're saying OK current user right now is only available to devise users but that's not really what we want. We want current user to be available even with no user is logged in.
In our home call right here we don't have to say if current user because there's always going to be a current user. This is going to make life a little bit easier for us.
In the current user method (application_controller.rb), the first thing we're going to do is call
super. Now don't worry. I remember the first time I saw super in Ruby and I thought it was one of the weirdest things and I really didn't have any clue what it did. Let me kind of give you a breakdown of what this is going to do and a good example is just saving the file and then seeing that literally nothing is going to change.
I didn't change anything in this file and nothing's changed. By putting super in here nothing is going to change, if I hit refresh, you can see that this works perfectly. If I hit log in everything still works exactly the same. We have access to current user. Everything is still working. Super says whenever you're overriding a method because remember this is something provided by devise, when we implemented the devise gem we were able to get all of the methods inside of the devise source code. One of those is current user. Super says "don't change anything, I simply want the exact same behavior as in the method that we're overwriting."
Now let's get into an alternative, there is a very cool syntax that we can do for this and I think one of the best ways is we're going to actually open up pry here.
If you remember back to our deep dive in the auth section for bcrypt, you should have pry installed. If you don't you can run gem install pry and that will get you exactly what you need.
I'm going to say pry and from here what I can do is I can show you how a conditional can work a one line kind of format. We have the ability to check for true or false values and say sometimes something can be true and sometimes it can be false. In other words, if I say false and then use this double pipe operator, which stands for "or," it's the same thing as saying
false || true this is going to return true."
Essentially Ruby looks at this first item and it says OK this is false. This entire system is going to be false unless this side is true. If I did
false || false it returns false because Ruby looked and said false on this side and false on this side. There's nothing in here that's true. We have something that is similar in terms of comparison except it requires both of them to be true. I could say a
false && true this is going to return false because the ampersand is the same as saying "and." This is saying that in this case false needs to be true and true needs to be true but because false was false the entire system's going to return false.
Now you may be wondering why in the world of explaining this to you this doesn't seem to have anything to do with the null object pattern in users and that kind of thing, however, it does actually in terms of our implementation. I want the ability for our current user method to be dynamic and say if current user exists, I want you to just treat it like normal.
In other words, I want you to treat it like a regular devise user and if not I want you to do something else. Do you notice how this is kind of the same pattern as with these first two items here? Imagine that we say
super || guest_user we're essentially saying if super turns out to be false meaning that no user is logged in and that's exactly the way Rail's is going to treat it, it's going to say OK we're looking and we're going to look at both sides of this argument, if super is false we're going to look over here and make sure that this guest user is always true.
Exactly like our first line, this first line is always going to result in true whether the first argument here is false or whether it's true. Because we have true in the right-hand side, it's always going to return true.
That's exactly what we need to implement because in order to be able to do something like this where we can get rid of this conditional check and throughout our entire application we can call current user. We need to make sure that this never returns nil. That is exactly what it does whenever a user is not logged in.
We have the first part of our system written which is we're going to say if current users there which this is going to check is super true which means it's current user true. Is there a user logged in. Ok if that's the case then we can even ignore everything there and it just treats it like normal. Kind of like how we saw when we just had Super there all by itself. If super is true then this is going to stay exactly the same. Everything works with devise everything is normal.
Now what we have to do is build out the right-hand side of this. The right-hand side is where we need to build our null object pattern. The way we're going to do it is by actually mimicking a guest user. We don't need to create a user such as someone in the database. We simply need to have someone who acts like a user and that we can treat like a guest user. A great way of doing this is by using a tool in ruby called open struct.
If you do not have this on your system, I'm not sure if this ships with the core version of Ruby or not. If this throws an error simply quit out to pry and run gem install ostruct. I believe it does ship with Ruby. If not it's pretty easy to import. We do not have to do this in Rails we don't have to call a requirement in rails for ostruct because it does ship with that.
guest = OpenStruct.new(name: "Jordan Hudgens", first_name: "Jordan", last_name: "Hudgens", email: "firstname.lastname@example.org")
Let's say in a store this in a variable called guest and say open struct.new and what open struct does is it essentially gives you a little mini data structure. The cool thing about it is it acts a lot like a database query. Just like if we were to return a user from the database and that's what current user essentially does it runs a database query it takes the ID and it says OK this user in the database? If so we have all the parameters we have their name their e-mail everything like that. With open struct we can do something very similar by saying OpenStruct.new I can say name and pass in Jordan Hudgens. I can give a first name and for this one will say Jordan you can do a last name and say Hudgens and I can give an email.
If I hit return it creates an open struct. Now watch what I can do. I can type guest.name and it brings my name, I can do guest.first_name and it shows my first name. The cool thing about openstruct is we can create one of these and treat it like a regular user. What we're going to be able to do is mimic a user and then we can handle all of these parameters right here. Then this is what would be used, if we call first_name then we're not going to get a null value anymore. We are actually going to get in this case Jordan but in our case what we're to implement we're going to get guest or something like that.
We can copy all of this put it inside of our current user and we'll refactor this in a second.
What we're doing here is we're saying if a user is logged in, if the left-hand side of the argument is true then call super treat everything normal and the way that Ruby works, It just completely ignores everything else to the right. This doesn't even take place if super is called.
If this is false though Ruby's going to come here and it's going to say OK well if that's false than this side we need to test and when it tests that it's going to run this. Because of how Ruby works, it's going to automatically return this value. Our new current user method is actually going to be this open struct value. The same way that we were able to call guest.first_name and everything like that.
Hit Control D to get out of pry and we can start up the rails server and switch these values so her name should be guest user. First name can be guest last name can be a user and obviously you can use these however you want and eventually will probably change these and email can be email@example.com. Everything here looks good. Let's see if this is working. I'm going to open up the site. First I want to test to make sure we know the errors. Everything looks good. If I log out now it says hi guest. This is pulling in our values perfectly. So far so good.
Now let's come and let's pull our defensive code out.
Now let's come and hit refresh, everything's still working and it doesn't have any errors. This is a much better way of doing it, if I hit logout and we're logged out successfully.
You may notice I logged out successfully but my links aren't working. That's because we have to update our application file. Right now, this is looking for current user. Now we have a little problem because our current user is always going to exist which means with this code here it's always going to show log out even when a user is not logged in.
Let's jump back into pry because it's one thing to go through this and build out the system. I want to teach you how to understand it. When you're not following along in a tutorial you're going to know the right way to think about it. Say that I ran into that problem in a real-life app, which I did, that's the way that I figured out how to implement this. The first thing that I would do is say OK what here differentiates a current user in terms of a devise current user compared with this type of current user. The cool thing is the way that Ruby works we have access to call the class type. And so what we can do is we can say what type of class is this coming from so current user is just a method when it comes from super. It's coming from a devise class. However, when it's on this side it's actually going to be coming from the open struct class. The way that we figure that out is if and I'm going to copy all this.
Let's create another open struct
name = OpenStruct.new(name: "Guest User", first_name: "Guest", last_name: "User", email: "firstname.lastname@example.org"
Run that again, it creates our struct for us. Now what we can do is a
name.is_a?(OpenStruct) this returns true.
In case you're wondering how I even got that part, if I do
name.class the way Ruby works is it returns that.
If I were to put a string together and it .class this is going to say it is a string. It inherits from the string data type where this is a type of open struct. This right here is a type of string. If we were to do this in Rails with current user when we users log in we're going to see a different type of class and let's actually see exactly how we would de-bug that.
Open rails server and I want to for the sake of making this easy and just going cut this out just for a second and let's come here hit refresh. Oh and we're back to our little bugs. Put that in real quick. This is all just for demonstration purposes. We have register log in. These are all working now. Now inside of our home directory let's say that let's login because we want to see what devise is. If I come here and just put this in a paragraph tag I'll say current_user.class save and you can see this is of type user which is our model.
That tells us two things. It tells us that we have two types of classes. And the nice thing about this is that here current user can only be two type of classes. We know which for sure going to be either of type user or it's going to be of type open struct. All we have to do is inside of our application file, I can say if current user is_a and then pass in user then I want you to show log out because we know that if current user is a user meaning it's coming from our user model then I want you to show log out. If not then we'll just automatically know that it's the open struct and it will show these other links.
let's come here. Let's pull out this code.
Our application controller looks good. let's try this on now.
Hit refresh and look at that were logged in and we have the log out button. I press log out. It switches to guest. You can see down here a class is open struct and now we have a much better way of doing this.
Let's switch to home. Delete that. And before we keep going. Congratulations you now know how to implement the null object pattern. There are many mid to sometimes even senior level devs that rarely are able to implement that.
This is a consider a design pattern which definitely falls more in the advanced category of development and it is a considered a best practice so that's something I wanted to give you guys. Hopefully, you can appreciate the importance and how helpful this can be in a real-life scenario.
Now before we end this and before we merge our branch in let's build our concern because once again I don't like having just methods hanging around inside of my application controller.
In the application_controller.rb, say
include CurrentUserConcern I want to make sure that there's not some other module that may be overwriting this or that this may override. Just to be safe I'm going to say include current user concern and a potential refactor in the future. I may even add the word concern to each one of these modules and these filenames just to make it incredibly clear that here we're including concerns. Right now I'm not going to worry about it but that is a potential refactor.
Come up to controllers concerns hit right click, new file, save. This is going to be a
current_user_concern.rb Now I can create a module called CurrentUserConcern and let's just come in grab our source here. So set source. We know we need to extend active support and then we should just be able to bring in our methods so now we should be able to just call current user save paste that in save.
Let's come back one more time let's refresh see if it's working. So far so good, everything here is working. All of our current users are being called guest is called so everything is good in our application controller.
The last refactor I want to do is one that's incredibly popular in the Ruby and Rails community which is you wouldn't want to have a single line of code that says this long. If a professional rails developer went and saw your code and saw something like this that's probably not the best way to do that.
We can actually turn this into its own method, I can come here and say
def guest_user OpenStruct.new(name: "Guest User", first_name: "Guest", last_name: "User", email: "email@example.com") end
I think Guest User is really nice and explanatory. I can save paste in guest user here.
Let's see if this is still working here. Everything still is working perfectly.
The last thing is really just a formatting item it's bad practice to have code in all in the same line like that. let's get all of these a semblance of being on one or on multiple lines just so it's easier to add more of them in the future if we have to.
Now we have a method that creates and generates this open struct to Guest User. And I also like the way this code reads. Our current user is either going to call super meaning it's going to call its parent method or it's going to create a guest user. And that is the method right here. Now if you show this code to a professional rails developer they're going to be much happier with how this works not just from a functionality standpoint but also from a code organization standpoint.
- git add .
- git commit -m "Added guest user for current_user."
- git push origin controller
Let's go and merge this into master on GitHub directly. Go to devcamp portfolio. In my case whatever you have your repo name and now we can look at our branch we also could come over here to where it says branches and you can see that this has all of the branches that we've worked on. And you can come to controller and click on new pull request. There is no difference in doing this compared with clicking this button. I just want to show you that there are different ways of accessing the same exact points.
Here at the controller we can see that we have some nice commit messages. And here in the description say that implemented a number of controller based functionality such as.
- null object pattern
- additional helper concerns for users
- strong param refactor for our portfolio items
- added session tracker
Click on Create pull request and from here we can say merge pull request confirm merge everything's done now if we switch back our master branch has all of our changes and just to keep everything clean. Go to
git checkout master in the terminal run
git pull this will pull down all of our latest code and now all of the code on our master branch locally and on GitHub is all right here.
Great job going through this section.