RubyConf AU 2019
Views, From The Top
Summarized using AI

Views, From The Top

by Tim Riley

In his talk "Views, From The Top" at RubyConf AU 2019, Tim Riley addresses the complexity and challenges associated with server-rendered views in web applications. He advocates for a refreshed approach to building views, highlighting that current methodologies are outdated and lead to messy, hard-to-manage code.

Key points discussed include:

  • Outdated View Practices: Riley points out that server-rendered views have not significantly evolved since the initial release of Rails, suggesting that both Ruby developers and front-end developers often struggle with messy implementations.
  • The Need for a New System: He criticizes the mixed responsibilities present in the current MVC architecture, where views and controllers become tangled, resulting in less maintainable code.
  • Introducing Drive View: He proposes a new gem called 'Drive View' to address these shortcomings, emphasizing the creation of objects for views that inject dependencies, which helps manage logic in a clearer and more modular way.
  • Design Principles: A core principle is to separate concerns by creating distinct components: views, parts, scopes, and contexts. Each component serves a specific function while promoting clear flowing data and reducing clutter in templates.
  • Dependency Injection: By allowing views to have dependencies injected, Riley demonstrates how to streamline the rendering of articles in a more organized manner, enabling separation of HTML markup and application logic.
  • Encapsulation and Testing: He highlights the immutability of the created objects, which makes unit testing simpler and ensures the ease of revisiting code later, asserting that improved object-oriented design can significantly enhance code quality.
  • Broader Implications for the Ruby Community: The talk concludes by recognizing that improving view structures is not only beneficial for individual developers but also contributes positively to the wider Ruby ecosystem by encouraging new ideas and diversity within web frameworks.

In summary, Riley motivates developers to rethink how they construct views to ensure clearer, more maintainable, and testable code, positioning "Drive View" as a foundational tool for this evolution in Ruby web app development.

00:00:00.000 Next, we have the wonderful Tim Riley. This is Tim's second time speaking at RubyConf. He spoke two years ago when the conference was in Melbourne. His talk is on views and comes with a bit of a disclaimer: these are not the views you're thinking of. Tim is a big advocate for Ruby and its various implementations, such as dry-rb and rom-rb. He is an open-source contributor to both, as well as to Hanami. Other than preparing for this talk, he hasn't actually written Rails in about three years, though he uses Ruby pretty much every day. He's originally from Canberra but has also lived in Adelaide. He has settled in Canberra, and apparently, he loves the hot weather, although some might say he made a terrible decision by living there. He lives there with his two young children. His four-year-old started preschool this week, which was very exciting. Tim is here with us to tell us all about 'Views from the Top.' Please welcome Tim.
00:02:00.830 In our time today, I'd like to do something a little bit different. I want to take you behind the scenes, adopt you as coworkers of mine, and bring you into a meeting. So let me just get set up. Did anyone bring a whiteboard marker? No? I probably can't reach anyway, so we'll deal with some comics. Welcome everyone, thanks for coming along. I know your time is precious, and for some reason, this meeting room is just booked solid today. I think we've got maybe nineteen minutes left together, so let's get into it. We're here because we decided it's time we do something about our views. I have to say, these days I'm not talking about anything that runs inside the browser; I'm talking about the humble server-rendered view. These views continue to be important in our applications, and if I think about it, probably across the whole internet at large. But for something so essential, I have to say things have gotten out of hand—our views at least just feel like a mess.
00:02:47.450 I know that might sound special because server-rendered views occupy this intersection between Ruby code and markup, between Ruby developers and front-end developers. That doesn't mean they can't become prone to becoming messy, but I don't think that's an excuse. We can and should be trying to do better. We can't continue to write using the same old methodologies and somehow expect to get different results. The truth is we're writing views in pretty much the same way as they shipped in Rails 1.0. That's kind of damning if you think about it! Nearly fifteen years have passed since then, and collectively, have we learned nothing in that time about how to write better views? Rails 1 shipped back in 2005, and if you cast your mind back, you might remember that as the year when Vin Diesel released 'The Pacifier.' This may not be the most glorious moment in cinema history, but it serves as an interesting point in time. We've seen Mr. Diesel relentlessly iterate with every subsequent release, becoming faster and more furious. But, with our views, it feels like we haven't changed, and that's what makes me furious!
00:03:43.350 I don't think it's just that we're writing bad code; I think it's this whole approach to views that has issues. Let's review that situation now. We'll start with controllers. While the HTTP processing layer is where we often want to render a view, this shouldn't have to be a view's forever home—that's just mixing too many responsibilities together. As for helpers, what can I say? It might look like we've organized these things, with all those modules we create, but in the end, they all get mixed together into this big global soup of implicitly interdependent methods put together into a context we have no control over. I asked around the team in the last few days, and none of you feels good about writing a helper. It's just something we do when we feel like we have no better option.
00:04:27.220 Yes, we feel like add-on decorator gems might be a better option, but it turns out they occupy this uncanny valley of libraries. They're helpful, yes, but because they're not native to the view system, they lack certain abilities. Since these are opt-in additions, it always felt like there was a little too much friction around using them. In the end, this lack of good options has meant we just end up with too much logic cluttering our templates. We'd much rather be clear of code and clear of mind to focus on things like markup and presentation. With this approach, it’s just too hard to write good view code, and really what we should be striving for is for our view code to be good—any good code! Just because we're working in views doesn’t mean we should sacrifice all the good guiding principles we try to apply to other parts of our codebases. Let's fix this together today; let's build a better view system.
00:06:09.480 We should publish it as a gem, and we'll call it Drive View. Thanks for taking the time to consider this meeting after I sent you the invite, and for sending me those requirements you put together for how we should build better views. After looking them over, I think there's a way we can bring them together into a nice, coherent system. The first word of your requirements is that views should be a thing; they should be an object—something that we can address directly and pass around. So we'll start with that! Let's define a class that will inherit from Drive View, and yes, it looks like we can make this into an object in typical Ruby style.
00:06:38.090 Next, we’ll make it so our views can have some dependencies injected as well. This means they can cleanly collaborate with whatever other parts of our application they might need to do their job. Today we'll be building a view for showing an article—nice and simple. In this case, we want to give it an article repository so it can load articles from the database, and then we can pass it in when initializing the view. So now we have our view object. The next thing you said is that we should be explicit about declaring what values we make available to our templates because we are crossing a boundary at that point; we should be purposeful about it.
00:07:32.000 I think we can solve this by adding exposures to our view. In this case, we’ll expose just a single article, and inside our exposures, we can do whatever we need to prepare those values. Here, we want a slug to be passed to us, and then we’ll use our article repository to find the matching record in our database. If we are going to pass this to a template, let’s give ourselves a place to specify the template’s name. That brings us to your next requirement. Yes, you say we still should build these things with templates, and I’m good with that too. We're comfortable working with templates, and there’s still a great place for our front-end developers to go to work.
00:08:03.530 Here’s our template for today. We'll build this one with the Slim template language, and what we're doing here is putting a header on our page and using that article we exposed earlier to fill it in. With that template in place, we can go back to our view object and call it while passing in a slug for the article. Look, here is our rendered view—nice and simple! One, two, three! I think we are done. Early mark, everyone! Oh, you’re saying we’ve got the basics but there is more we can do? Yes, we wanted to use templates, but we don’t want to get mired in the morass of complex logic that weighed us down before. We want our template code to be simple and to push as much logic into other parts of our view system.
00:08:38.940 Let's look at some aspects of this logic now. Here’s how we might have used a helper before to render an article's body from its markdown source. The downside to using helpers in this way is that we now have to remember to use it in every single place we deal with an article. The accumulation of hundreds of these across multiple concerns is what got us into this mess in the first place. Now, this is more like what you wanted to see in our templates—the view-specific behavior hanging off the values that we passed to the template in the first place. What we’re seeing here is some kind of decorator object in action—something that wraps the value we pass in from our exposure and carries that view-specific behavior.
00:09:37.940 Let’s build one of these now. We’ll call these parts, and this one here will carry the behavior for articles. With this, we have a place to put this body HTML method right there alongside the article's own data. Now, we’ve simplified our templates and we’ve taken care of the other side of the equation, which is to decorate our exposure values with objects that carry that view-specific behavior. Your next requirement is an interesting one because I can see you’ve looked at how we worked with decorators in the past and realized that a decorator is only so good if we actually use it.
00:10:25.820 What you want is a system that nudges us towards the right abstractions every time so that things like decorators can be made automatically from the beginning. If we go back to our view and figure out a part namespace, we can make it so that the value from every exposure automatically gets wrapped in a matching part object. It does this by matching by name! So all we need to do is create that part class, and it will be picked up automatically. By doing this, we're much more inclined to utilize these when we have to add extra view logic. I get what you're saying: if we need to do our own take on decorators, we should make it worthwhile and close the gap we felt before when using those add-on decorator systems.
00:11:10.300 What you want to see is parts become fully integrated with the rest of our system so that anything we can do in templates, we can also do in parts. Let’s find a way to make this work. We’ll go back to our article template and say we need to put a sharing widget at the bottom. The markup here is pretty tedious, and we have to pass in all the different aspects of our article that we want to appear in this widget. At this point, we might realize that this is a self-contained component that we want to reuse, so we’ll extract it into a partial. Yes, we still have partials in this system!
00:11:45.680 You can see us working with one here by the render method. But even with the partial in place, we still have all this awkward attribute passing every time we want to use it. What if we could render partials from within our view parts just like we can in templates? This means we can now move everything right here into this article part class and hide away the particulars of the rendering. This leaves us with a much cleaner, more intention-revealing template. Now that’s the kind of outcome we wanted to see from having view facilities that are fully integrated! Things are feeling pretty good now! However, a few of you are still pointing out a gap: you're saying with parts, we figured out how to add view-specific behavior around particular values, but what about behavior that doesn't live alongside any one value—behavior that might go along more with a template or with a partial.
00:12:45.370 I noticed a few of you shared an example of where something like this could help, like with this related article partial. Now, this rendering looks pretty innocuous, but if we dig inside, we might find some pretty hairy-looking template code. It’s just some logic I don’t like to see in templates. If I see the defined keyword anywhere, I know something is probably going wrong. I can see what we’re trying to do here: we're trying to make it so that if this partial is rendered without these locals explicitly passed in, we’d want to provide some default values. However, I still believe this is a wrongheaded approach. Let’s build up a scope for this related article partial. While parts deal with a single value, the idea of scopes is that they can handle whole templates, allowing access to that template's complete set of locals.
00:13:37.740 With this setup, we can now write methods that properly handle a missing local and can provide a default value in its place, such as whether or not to show the author in this related article. Additionally, we can provide a default for the prefix text in the link. In fact, we can even combine that prefix with the article's title to give us the full text. This means we can remove that cluttered code and end up with a much nicer template, using the scope’s methods to tidy everything up. To ensure our partials render with that scope, we need to build it up first, pass it the same name, pass it the article, then tell the scope to render itself. It’ll go ahead and pick up that existing partial. This list is looking pretty complete now; we’re really done at this point, right?
00:14:29.670 Wait! You want to add helpers back after everything I said? Okay, I see there’s a fair case for this. It's likely there will be a small number of things truly common to all views and their templates; some kind of baseline rendering context. Let's take that name and make it the name of a new facility—a context object. Just like views, the context may want to interact with other parts of the app, so we can inject some dependencies here. For example, we’ll pass in a static assets manifest, the kind of thing that Webpack generates for us externally. Then, we can write a method that uses the manifest to give us the full asset path for any given name. To tie everything together, we’ll make it so that whatever methods we define in this context class are also available to use in any template.
00:15:18.620 Also, since we care about our view facilities being integrated, we’ll make those methods available inside our parts and scopes. This means we can continue to keep our view logic in the best possible places while still taking advantage of every feature of our view system. I like this point you make: you’re saying that while these might feel like helpers in their end usage, that’s the only thing they share in common with the helpers we had before, because in practice, these are regular methods defined in a regular class, with its own state and its own clear dependencies. That’s something we can definitely keep on top of and refactor more easily over time.
00:16:09.030 I think we've made it now, and it’s not just because we're out of space on our giant whiteboard. I believe we’ve given this system everything it needs, and we did that by working through those requirements you shared, using them to help discover the concepts that can better organize our views. We started with views themselves—views as standalone objects carrying configuration dependencies, declaring exposures. Those exposures prepare the values we want to pass to our templates. The templates define the markup that gives us our view output, and if we want to add extra behavior around specific values, we use parts. We have scopes to give us behavior around inputs or partials, and we have the context to tie everything together and provide what we want in every situation. That was a thrilling ride, but I think it’s good now to step back and ask: what have we learned through this process of invention?
00:17:25.700 We rightly recognized that views are complex enough that templates and helpers are just not up to the task of handling them anymore. In working through those requirements, we created six distinct places to keep our view logic, with each serving a necessary role. This isn’t just architecture for the sake of architecture; it feels like minimum viable views. Each of these facilities exists to help us write better view code. If we were to remove any, our view code would suffer. The benefit of having all those structures from the get-go is that they make the easy thing to do also the right thing to do. When those things align, that’s when the quality of our code can truly level up. By representing our views as standalone objects and clearly injecting the other parts of their application they need to do their job, we enable a clean separation of concerns.
00:19:00.120 We disentangle the views from the rest of our app and let them stand apart in a truly distinct layer. By moving the bulk of our view logic into part and scope objects, we are properly encapsulating that logic, taking our custom behavior and putting it in the same place as the data it relates to. We’ve designed all of these different objects in our view system to be immutable, so once they’re initialized, their state never changes. This means they’re easier to understand on their own and provides a much clearer flow of data throughout all our views. All this makes for a view layer that is now far easier to test. We can now unit test our views more than ever! Whatever level of granularity makes the most sense—say we have a part or a scope with some complex behavior—now we can unit test those single methods right away.
00:20:08.380 Or if we want to test a view in full, we can build a view object, passing test values as input and then making assertions against its output text. We can do this in complete isolation—no web requests, no database access required! This ease of testing reinforces the lesson that better view code is simply better object-oriented code. This is something we should strive for in every aspect of our applications, and views should be no exception. Better object-oriented code gives us views that are a joy to write, views we can be proud of—views we can trust will work as intended. These are views we're happy to revisit two months down the track when we have to maintain them. This helps us do our jobs better and build better applications.
00:21:29.000 Whether or not attendees use Drive View, hopefully, these ideas can help push their views in a better direction. This is about more than writing better apps; it's about helping to create a better and stronger Ruby ecosystem. By creating a new view system, we've established choice, and choice leads to diversity. Through diversity, we have a healthy sharing of ideas that helps drive our community forward. Importantly, what we've created here is open to everyone. As long as you use Ruby, you're good to go, regardless of what flavor of web framework is your preference.
00:21:43.600 Once you're using it, how you use it is up to you! Yes, render web responses, but consider using the same view system for emails, so you can take advantage of all the same facilities. In fact, you could render views in the background, because this system is standalone and flexible! How you choose to implement it is completely up to you. In establishing this, we've innovated in a space that's been largely stagnant for the last 15 years. We felt the existing tools were getting in the way of writing good view code, and so we took action! It turns out, this solution is about 900 lines of Ruby—none of this is rocket science.
00:22:43.600 Anyone could do it! We've shown that while many may consider Ruby’s story to be settled, there’s always an opportunity to re-examine something fundamental and offer a new and interesting perspective. In summary, what did we do? We invented Drive View together and finally gave ourselves the chance to write better views again. With that, I think someone else needs the meeting room, so I will wrap this up. I’m Tim; please come and have a chat. I’d love to talk about views or anything else. The Drive View website has more information available. Thank you for coming, and I’ll see you at tomorrow’s stand-up!
00:22:58.000 You.
Explore all talks recorded at RubyConf AU 2019
+10