Object-oriented and functional programming, two households, both alike in dignity… except in this story, they’re not foes. Turns out that if you’re writing a web app, they’re perfect compliments, like barbecue and beer. A perfect meal has both. Using objects in a functional style makes a web app clean and testable, but it takes a few carefully considered design patterns, and they’re probably not the ones you’re used to.
To be clear, I’m talking about server-side web development. This is an article about how to improve your Ruby on Rails, your Play!, your Django, your .Net MVC by using a class design paradigm that’s different from what’s commonly taught as best practice. Other types of development require other patterns. These are what I’ve found work best after years of working on server-side web.
Enough with the domain models
The first step is to realize that domain models were a bad idea in web. A domain model isn’t something complicated, it’s a class that represents a thing. If you have a class called “Person” that represents a real-life person, that’s a domain model.
This pattern came from desktop apps, which is where OO design was invented. There, domain models were more useful. When you instantiated a person, it stuck around, maybe on the screen, or maybe just in memory. Instance variables in your person class held its state and made programming easier.
The web is mostly stateless. Except for the cookie, and the data in the database, nothing sticks around. Everything is created and destroyed during the request. Nevertheless, web frameworks in the early aughts took patterns from desktop apps - from the Gang of Four - because that’s what their authors knew. This was a mistake.
Compounding the problem, MVC web frameworks typically include an object-relational mapper (ORM), wherein a class called “Person” is both a domain model and a representation of a database table called “person”. Conflating these was also a mistake, like drinking vodka with barbecue. Step one in making your app easier to reason about is taking the business logic out of the models and putting it elsewhere. A model should be just an abstraction of a database table, and nothing more.
The Slicing Problem
The problem with domain models in web apps is that they slice things the wrong way. Say your app has four routes, those being: create user, confirm user, login user and delete user (just kidding about that last one, no one deletes anything anymore). In a domain-driven design, you’d put the logic for all four of those things in the user model. That’s a very broad interpretation of Separation of Concerns, because those four routes might do things as varied as domain logic, querying, database transaction management, sending emails, checking security permissions, enqueuing background jobs, reporting analytics and transforming data for the response. Just a single route does enough varied things that putting it all in one class is stretching the bounds of sensibility. Add everything for four routes to one model class or one controller, and you’re over 100 lines of code. These are not OO best practices.
Exacerbating the confusion, a typical web request is often querying data in multiple tables. Even a simple-looking page may query thirty plus tables. And much of the business logic operates on data from multiple tables, so putting it in a particular model is not a natural fit. Putting it all in controllers is also problematic, because it’s not reusable and controllers should do other things, like error handling and redirecting.
Classes as Verbs
Enlightenment comes when you use objects in a server-side web app to model actions, not things. In an action-oriented web app, classes that do things (classes used by POSTs) are verbs: CreateUserCommand, ConfirmUserCommand, AddToCartCommand. Classes that read things (used by GETs) are grouped by the function they perform, not the objects they represent: HighestRatedProductsService, RecentlyPurchasedItemsService, ShoesByBrandService. Each class has only a single public method, because it only does one thing. Such class design keeps things small, modular, composable.
It also leads to better queries, because it enables thinking in terms of operations and transactions. The code for a single request is mostly in one place, and it’s clear what operation that code is used for. In domain-driven design it gets mixed around with code from other operations. Refactoring becomes hard. Classes grow huge.
The Functional Imperative
There’s another abstraction in programming that models an action. It’s called a function. When classes are verbs, they act like functions. Moreover, they become simply containers for functions. Suddenly your classes are less about what they represent and more about what functions they hold. A class with a single public method is just a wrapper for private methods that belong to the same operation. It’s less of a new abstraction and more of a powerful, more granular type of namespacing. Since command classes can call other command classes, they’re reusable and can be composed.
Slicing your app this way reduces fear of the wrong abstraction because the code can be moved with little or no modification. It improves testability by keeping the scope of the class clear. If the code in your classes follows functional principles, and your classes act like functions, then you have Functional Object-Oriented Design.
Functional Object-Oriented Design allows you to differentiate between pure classes (functions) that possess no side effects and classes that do, using naming conventions, different base classes or just by putting them in different directories. That means separating classes containing business logic from classes that query, by convention. Put business logic into pure classes with no side-effects (I.E. no queries) and you have embarrassingly testable classes. Keep the queries together in a different class where they stay next to each other and thus can be easily wrapped in a single transaction.
Functional Object-Oriented Design models a web app for what it really is: a pipeline of functions that transforms data as it moves between the browser and the database. There are no domain models in this world, just actions — stateless data transformation pipelines that use classes to encapsulate related private functions behind a single public method. Your app is sliced correctly, for readability, testability, sanity. It’s like a cookout with family on labor day: everyone’s happy, and every bite of barbecue goes down with a sip of beer.