 Hello everyone, and thank you to engineers.sg for that live stream. My wife and daughter will be watching me say this talk for the fifth time in two days, and I hope you enjoyed it just as much. So, hello. Good afternoon. I am Tim Riley. I'm part of the DryRB and RomRB core teams, and I work at ISLAB over in Australia where, for these last nine months or so, we've adopted a whole new approach to building our web apps. So, what got us here? Well, a good many years of this, of following established community practices for Ruby, building our MVC apps, and still ending up with projects that became more and more difficult to work on over time. We wanted to find a methodology to improve this for us. And that's what I'm here to talk to you about today, about building better web apps with Ruby. We'll cover a lot of ground today, and we'll look at a lot of code, but I hope I can give you a sense, a new sense, for what's possible with Ruby, and inspire you to look at web apps from a fresh perspective. We'll look at three things today. We'll look at DryRB, a new collection of modern single-purpose Ruby gems, at RomRB, a flexible persistence toolkit for Ruby, and at Rota, which offers an expressive approach to routing. And we'll also combine these with a fresh approach to Ruby itself. With a hybrid of functional and object-oriented programming that we call Functional Ruby. So, let's start with that. Thinking about functional programming, what are a few things from it that can help us with our Ruby? Well, firstly, if we want to orient an app around functions, we need to treat those as values, so that we can pass them around and combine them to build larger systems. Our app is also simplest to work with if we favor immutability wherever possible, avoiding internal mutable state in our objects. And we also like to avoid external side effects wherever possible, so most of our code can be run in any sort of context. And together, this helps us better understand the flow of data at any part of our application. And we can satisfy all of these criteria by building functional objects in Ruby. Let's take a look at one now. This is the simplest possible functional object. It's a class, it has a core method, just like Ruby's own procs and lambdas. That core method accepts an input and returns an output. That's it. It doesn't mutate the object's state, and it shouldn't mutate the input data either. Functional objects in this way make a clear separation between data and behavior. In fact, the only data an object like this would hold as internal state are its collaborators or any other sort of static configuration. This is a state that we'll never need to change. And we set this state via the constructor only, just like we do here with this repo variable. This is, in fact, an example of constructed dependency injection, a classical object-oriented design technique that works to great effect here with our functional objects. And in the end, what this means is that we can create a functional object just once but then use it many times over. So it means, regardless of when or how these objects get constructed, they can then go on to become collaborators for other objects in turn. So let's take this technique and build a whole web app around it. We'll build the typical blog post example app. We'll build a get request so we can see reading data and a post request so we can see writing data. And we'll look at these step by step, building them up layer by layer. And in doing so, I hope to give you an idea about the small pieces that combine to make the general shape of a larger working app built in this way. So let's start with the get. Let's get some articles. We'll need to do five things to tackle this. We'll need to handle routing and HTTP. We'll need to look at object dependency management. We'll have to render some views. We'll need to query the database. And finally, we'll need to model the data coming back from that database. So let's start. The first thing we'll need to do, obviously, is handle the incoming HTTP request. And to do this, we'll work with dryweb and rotor. Dryweb is a high-level organizing gem that forms the umbrella around our application. And it works with rotor. This is what we'll use in our examples today and is what exists right now with the gem. But the plan is to actually go on and support other routing libraries as well. So in this way, we're not talking about a one-size-fits-all stack here. What we're talking about is a collection of libraries that work really well together to form a larger, more coherent web app. So we can start by installing the gem and generating a new project with it. And then we can go into a route. This is rotor at work. And rotor works by building up a routing tree. And it works by basically lots of nested blocks like this. And in a larger app, this can make for some really concise routing logic. Because we can set context in the outer blocks and have that context available for any block that's nested within it. Rotor is also a flexible routing toolkit because it starts with a minimal core and then offers plugins for us to layer on any sort of behavior on top of it. And that's what we're using here to provide this view method. This view method is provided by a plugin and we're using it here to render our index of articles. And this index of articles is provided for us by another object. And this is where dry component and dry container come in. Dry component sits at the heart of every dry web app. It is a high level organizing system for our app's objects. And it uses a simple naming convention to register those objects with a container. That container is provided by dry container. It's a lightweight inversion of control container for Ruby. And what it basically does is it allows us to access those registered objects later at any other time. So what does this actually mean? Well, it means when we call view with articles.index, it's translated into a fetch from the container with a particular identifier. And what's returned to us is a pre-built object, so we can go to work on it right away. And that pre-built object is automatically made available to us based on this simple naming convention from a Mattsing source file. And that convention doesn't use any magic. It uses basic language-level Ruby features, good old require and load path, which means it's easy to understand and easy to put to work for us. So after all of this, we have a view object. And this is taken care of for us by DriveView. DriveView is a helper-less, data-oriented view rendering system which combines functional view objects and templates to render our views. And just like the router, we plan to make view objects completely independent, or they are, so you can use whatever works best for you. So here's our index view class. What it does is it specifies a template to use, and then in its local method, it returns a hash of locals to use within the template. Now, right now, this isn't quite enough to render our list of articles. We need to fetch them from the database. And this is the job of a dependency. That dependency will be made available for us by DriveAutoInject. DriveAutoInject provides a nice way for a class to declare its dependencies to have those dependencies resolve for us from our container and made available for us to use anywhere inside the object. So let's go back to that view and set up a dependency. Here we are. We're importing the article's repository as a dependency in that include line there. And then inside the locals method, we're using it to pass our listing of articles to the view. How do we get that listing? We get that using ROM and ROM repository. ROM is a flexible persistence toolkit for Ruby. It offers explicit, customizable abstractions that we can use for every step of the persistence process. It's also built around functional Ruby principles, and it's a really big part of what makes these apps sing. And why is that? Because ROM lets us define a clear boundary between the core of our app and our persistence layer. It lets us hide away the nitty-gritty details about persistence and let our app's core objects stay clean and focused on their own jobs. How does this boundary look? Well, here it is with our repository class. We're saying here that we want to work with an article's repository and we define a listing method which specifies that we want published articles only in this case. Now, ROM is actually a multi-back-end persistence toolkit. And since we want to work with SQL here, we'll use this SQL adapter, and we'll use that adapter to set up a relation. A relation is an example of ROM's explicit abstractions that we can use at various points. What the relation does is define a mapping to a particular database table or database view. And here we're using it to define a published query method, and that specifies that we only want the rows where that published column is set to true. This is a great example of ROM's explicit design because it allows us to define exactly the persistence API that we need for our app and nothing more, which means that every interaction with the database goes through code that we've written. So we have full control over those interactions, and that will be a great comfort as apps grow larger and get worked on by many people. So with this relation in place, we can go back to our repository, which will now return the right data. And this is a very simple repository, of course, but in a real app, they'll grow fast because we'll set up methods for each unique data requirement in our app, and we can also compose data from multiple sources. So this is why it's helpful to keep that low-level query logic encapsulated back in those relations. By default, repositories just return plain struct objects, but we would like them to come back as objects that are more specific to our domain, and this is what we do here. We're specifying that we'd like our objects to come back as article entities. So how will we make these entities? Well, we'll make them with dry types. Dry types is a type system for Ruby, not a language-level type system, but more something that just gives you some stricter typing in the places where you care about it the most. And where we care about it in this case is when we're modeling our article entities. We want them to contain exactly a certain kind of data. And dry types here won't actually allow us to even initialize these objects within valid data. So it means when we have an instance of an article, we can be 100% confident that it will contain the data we expect, which means there will be much less second-guessing when we're working with these instances, and it makes building things like our views much more simple. And unlike active record models which we might be used to working with, we don't mutate these structs by convention, which means they're much simpler to pass around our app because we don't need to worry about accidental side effects or accidental mutation along the way. So back to our view with the repository in place, everything is complete. So we can make our template, call the view object, and get the output we want, our list of articles. And one thing to note here is that this is completely stand-alone view rendering. So we can use the same mechanic anywhere, not just for rendering pages, but for anything we want, even in things outside the normal HTTP cycle, just like rendering emails, for instance. So we can come all the way back to the top, back to our routes, and our job is done. We've got our list of articles. What did we do again? Well, we used Drive, Web and Rota to handle routing and HTTP. Our object dependency management was taken care of for us by Drive Component, Consainer and Auto Inject. Drive U rendered our views for us. We used ROM SQL and ROM repository to set up our database queries. And finally, we used Drive types to model the results from those queries. So the get request is done. We've got a list of articles. Now let's go and create one. We'll have to do a few different things this time. We'll need to validate input data from a form. We'll need to write commands to write data to our database. And we'll need to take care of success with the handling of our command. So we can go back to our routes, adjust them to accept a post request this time. And here we're using a different Rota plugin we provided. It's Resolve. And what it does, it just fetches an object from our apps container using this identifier and makes it available for us to use. In this case, we're working with CreateArticle, which is a functional command object that will create our article for us. And then we're calling it with the form post parameters. And again, just like before, DryComponent takes care of this for us. So we resolve this object. It comes out of our container as a ready to go instance that has been created automatically for us from this matching source file. And here's how this looks right now. A simple functional object. And right now, all it's doing is just validating the input data. And to actually make that validation work, we will work with DryValidation. DryValidation is a powerful, stand-alone validation library. It lets us model our validations in precision and work with any kind of data in any kind of context. So here's our schema. What does it do? Well, it requires that all four of these fields are filled. It sets an expectation that our title should have a minimum of three characters. And it also sets some type expectations for the published and published at keys there. One will be a boolean and one will be a time. And DryValidation uses dry types under the hood to give us some powerful coercion behavior around form posts. So we can start with this stringy form input where everything's a string. A boolean is represented as a one. A time is also a string. And then after we've run it through our schema, we actually get coerced data. We get a proper boolean and a proper time object. So this form schema actually acts as another critical boundary in our system. It handles all the baggage of HTTP and then out the other end, it lets our app's core objects work with properly structured and properly typed data. And we'll also get friendly error messages when something's gone wrong. We also get hints about checks in the schema that weren't able to be run yet. So in the end, the user will have all the information they need to correct any errors. So we can go back into our command, check for the success of this validation, and if this is successful, we can save the article to the database. We've done this by injecting our repository again. And then on the repository, we're calling create with the valid form data to persist the article. How will we do that? We do that by going back to ROM again. A big part of ROM's designs is that it separates queries from commands. And again, this is about giving you explicitness in your setting up your persistence layer. It ensures that we create a persistence API with a surface area that exactly fits our app's requirements. So if you want to create command for articles, we'll need to set it up. So to do this, we go back to our relation. This is the thing that models the table. And we'll define a schema to act as the canonical representation of that data source. And we do this here again because ROM is a multi-backend persistence toolkit. Not everything can have schemas that are inferrable, like from database tables. Now, if you're using ROM in a larger framework, where it's more integrated, for SQL, this might be taken care of for us. But it's still great to have this facility available to us because it means we can project data freely in a way that's optimal for any part of our app. But in the end, this schema is used here to ensure only the right data is written back to our table. So we can go over to our repository now with the schema in place. And all we have to do is declare this create command. We'd like it to offer a create command for us. And everything else is taken care of for us. It goes back to that schema and makes sure the data can get written right through. So now, our create article operation is fully functional. But, and we're doing one more thing here in that we're returning an instance of the article as the final result just so we have consistency in what our app's objects return. But so far, this is only handling success. We want to handle failures as well. So this is where Monads come in. Monads are another concept from functional programming that we can put to good use here in modeling success and failure results of our operation. And we'll do this with dry Monads and dry result matcher. We can integrate them into our operation class like this. And what it offers us is meaningful workable output for both success and failure results. And we do it by this. We do it by wrapping our return values in either right or left objects. Both of these are instances of the either Monad. But in practice, it just means that in the end, the single return value we get is something that has a consistent API regardless of whether it's a success or a failure. And it enables things like this. This nice flexible result matching that fits really well into our rotor routes. By working this way, we actually elevate failure handling to be a first class concern because working with failures becomes just as easy as working with the success case. And in this case, we do two things. Success, we redirect, and on failure, we re-render the form past the validation object back so we can present the user with some errors. Also notice here that this is the only place in this request that we've actually handled anything to do with HTTP. So just like our repository was a boundary between our app and the persistence layer, the rotor routes here are another boundary between the world of HTTP and the world of our app. Our app's objects only need to concern themselves with doing their job and then returning success or failure knowing that back up in the routes here they'll be handled as appropriate for the request to be completed. So we've posted our article, this request is complete, and we used dry validation to validate that input data. We went back to ROM to create some commands and then we handled the success or failure results with dry monads and dry result matcher. But our app is not really done here. We've done these two requests, and as we know as developers, our work is never done. Clients come with more work for us and we have to satisfy that. In fact, dealing with change gracefully and effectively is a key part of our jobs. And everything we've done so far today has been to prepare ourselves just for this very thing. So let's look at some examples of change. Say we've got a request to make a new article form for admins or power users. Well, to do this, we can do objects in a new template. And then we could decide whether we want to reuse that existing create article operation or make a new one that more appropriately satisfies our requirements. And either way, we can be confident about the changes we're going to make here. Because in an app built around loosely coupled objects, focused on single tasks with external dependencies injected, writing tests becomes very easy. In fact, we can do much more in true unit test style in complete isolation because our dependencies can be passed in as test doubles which lets us focus more on the code under test rather than the world around it. And this is particularly useful for things like database backed applications because there's no need to globally stub out database calls or anything like that. And the end result is better faster tests and more confidence in our work. In fact, Dry Component does a little bit extra to help keep our tests fast. When we use it and run any single individual tests, the components in our app boot up completely empty. They don't load any files. And then when the test runs, the smallest possible set of files just for that test to complete will be loaded. This means that individual tests run in the minimum possible time and it makes for a truly responsive TDD cycle. So that change was fairly straightforward. Let's look at a more complex one now. Say we want to email some subscribers a notification when a new article is created. Well, there are various ways we can handle something like this. But we have a truly elegant one at our disposal here. We can use dry transaction to make this change. We've already laid some groundwork in our app. We have a big container of functional objects. Each of those objects returns a left or right monad for success or failure. And with this in place, we have everything we need to start to combine these in interesting ways. Like using dry transaction here to model this as a business transaction. How it works is that it is a sequence of functional objects called one after the other with the output of each object becoming the input of the next. And this sequence continues as long as each one succeeds. This is the perfect arrangement to satisfy this change. All we need to do is create a notify subscribers operation, run it after create article, and it will only run if that initial creation has succeeded. And then, outside of this single transaction, these two operations are not coupled in any way, so we can reuse them as we need in any sort of context throughout our app. And dry transaction also offers an enhanced result matching API so we can hook into specific failure steps and handle them as we want, which is a really powerful new feature and it's been made possible by the design choices we've made And what are those design choices? Well, it's a decision to found our app on simplicity. What is simplicity here? It's an app that's built of many small components, each with a single focus, and we combine the best of object-oriented design principles with the data simplicity inherent in functional programming. This makes for components in our app that are therefore easy to understand, easy to reuse, and easy to test. And I think they're actually really easy and natural to write in Ruby code as well. We've also taken care to design for our app's domain first. This isn't a web app. It's a Ruby app first and foremost that just so happens to have a web interface around it. We've taken note of the critical boundaries in our app, the boundary between HTTP and our app, and the boundary between our app and persistence. And we built around them appropriately, leaving the core of our app free to focus on its main tasks on its own specific domain. And in working towards this goal, we've chosen tools to serve our app first rather than the other way around. We haven't contorted our app to fit the requirements of some external tool. So by focusing on simplicity, by focusing on our domain, on proper boundaries, on small components working in concert, we have an app that is easy to change. We have created a change-positive architecture. And this is the crucial advantage we create for ourselves as programmers, because this is what we have to deal with every day. A change-positive app is maintainable. We can easily understand it by starting at any point and looking back and forth, and we can adjust it as required just as we looked at earlier. A change-positive app is sustainable. Because of our design, we can work on it just as easily at day 1,000 as we did at day one. And this makes a change-positive app joyful to work on. And it's this sense of joy, this happiness as a programmer. It's that visceral thing that brought so many of us to Ruby in the first place. So we owe it to ourselves to carry this joy throughout every part of our jobs to building web apps, whether it's starting something fresh or starting on something with a rich legacy. And by choosing to work in this way, we've not only created an investment in our app, we've made an investment in Ruby because a healthy future for Ruby is a diverse one with a diverse range of competing tools and techniques. This is what drives innovation in our community and will give it life for years to come. Now, everything I've showed you today was put together by a hard-working team of volunteers. So I'd like to thank at this moment Piotz, Sonica, and Andy Holland for their work on DryRB and RomRB. Also, Jeremy Evans for ROTA and to all the contributors to these communities. They're on the rise. People are adopting them, people are contributing, and we'd love to see you there too. And it's not the only community of this type in the Ruby world either. You may be interested in checking out Hanami or the Trailblazer project. Both of these integrate with various DryRB gems and they offer their own unique takes on web app development. And one of them may be just right for you. This is my DryRB. I have a web page full of resources to go along with today's talk, including slides, links, and real working open source example apps. Please buy me at the conference. I'd love to talk to you. And of course, I have stickers to give away. Many of them, so come find me and let's have a chat. Thank you very much. I think we can open one question for Tim. Anyone? I want to build apps this way. I'm going to go to the Bitly, draw a RDRC, I'm going to learn about it. What's going to be the first speed bump that I hit, because you painted a lovely glorious picture, and I know that a lot of it is going to be easy. But when I hit that bump, if I hear from you what I'm going to expect, it might make it easy for me to get over. I think what it might be is that, yes, what we're doing here is doing a little more of the work than the pieces in our own apps that might have otherwise been taken care of for us by larger frameworks. But I think that so you might find there's a little bit more friction in getting those first few features out, because you've got to perhaps discover some of your own design patterns. Some of these, well, these libraries are young, so you may hit up against rough edges, but we do have really active communities, and they're getting better day by day. So I think we're going to go through those early steps. And also, Rom, compared to Active Record, for instance, gives you a lot more power, but you also have a little bit more work to do, and you sit a little bit closer to the SQL. But just like everything else, this isn't building our apps so we can be productive in the first week. It's building our apps so we can be productive for years. So I think it's keeping that end goal in mind, and knowing that the decisions we make today set us up for making our life much easier in years to come with the same code base. Thank you very much. Thank you.