One day the trunk of my 2005 Toyota Camry got stuck. It was an old car, but had been reliable in the past, so it was surprising. My wife showed me how the trunk made a grinding sound when it opened and closed. I discovered the trunk had become detached from this long metal bar that acted as the spring. This metal bar made the trunk float up when you opened it and held it open when you were getting things out. Now it was broken. I attempted, but was unable to fix it. Now it had become a guillotine for hands. Not only that, but it was also super heavy to lift—far more than I would have assumed.
Cars are a great example of good abstraction. Their interface is simple, yet they are complex on the inside. The trunk appeared to be the simplest part of the car, yet it was complex when I looked at how it worked. It hid the weight of the trunk from me for years. The trunk would float up, and that's all I knew.
The power of an Interface
The idea of an "interface" (also called an "API") exists in software as well. This is because, like cars, software tends to be complex on the inside, but should provide a simple interface on the outside.
Interfaces hide complexity
The best interfaces hide complexity so the user should never need to "look under the hood" to know how to drive. Otherwise, the abstraction would be unhelpful, and you would have to be an expert mechanic to even use the thing. This is not what you want.
A car's steering wheel is this way. You could say: "please rotate the tire-rod mover counter-clockwise 20 degrees," but you don't have to. You can just say: "turn left here!"
Interfaces Unify
The other cool part of an interface is it's "combining power" - meaning it can help bring many different parts of a machine together into one. For instance, both the steering wheel and the brakes are completely different parts of the car. Even the stereo and the heat are right next to each other in the interface, yet are completely separate subsystems. The interface of the car combines them into a seamless interface.
In a software context, imagine you're making a new API endpoint for your microservice. You might get your data from joining 4 different tables, then doing some in-memory calculation. Not only that, but you mix that with some data from an external API. That would be very complex if another person had to understand all that and do it themselves. But with a nice interface, the user will never be the wiser. You could even refactor it in the future to make it simpler, and nobody's code would be broken (I can dream can't I?).
The Facade Pattern
There are many ways to create interfaces in different programming languages. One way is to make some of the functions private, so that other ones operate as the public API. In Elixir, you can change a function to be private (unable to be accessed by other files) by just a bit of syntax—defp <function name>
, instead of def <function name>
(note the p
). For example:
defmodule MyApp.Example do
# I'm public
def public_add(a, b) do
private_add(a,b)
end
# I'm a private, helper function
defp private_add(a, b) do
a + b
end
end
*(NOTE: I'm going to use Elixir for all my examples in this article, because that's the primary backend language we use at Podium, but, this could be used in any language. So long as you understand the example above, you should be able to follow the rest of the article)*
These constructs only work on a file level. But what happens if you want to "hide" a lot of files under one interface? This is where the Facade pattern comes into play.
The Facade Pattern is a design pattern1 where a module/class provides a simplified interface to a larger body of code.
That's cool, but how do we implement that in Elixir?
File structure
To start, we organize things the way Phoenix (the web framework for Elixir) recommends — using the "context" pattern. If you're unfamiliar, never fear, it's the pattern I'm going to describe in this article with small additions, primarily the presence of the Facade file. If you're curious you can read about it in the Phoenix docs.
All contexts have a name; which, in our example below, there is a widgets
and an orgs
context.
These both have a file outside their folder with the same name (widgets.ex
and orgs.ex
). These two files are the facades. They act as the Public API to their sub-modules in the widgets/
and orgs/
folders, respectively.
lib
├── your_app_web/
└── your_app/
├── widgets.ex (<-- facade)
├── widgets/ (<-- context folder)
│ └── private/ (<-- Widgets' private modules)
│ ├── create.ex (<-- private function file)
│ └── update.ex (<-- private function file)
│
├── orgs.ex (<-- facade)
└── orgs/ (<-- context folder)
└── private/ (<-- Orgs' private modules)
└── whitelist.ex (<-- private function file)
Now let's talk about each of these files, in turn, to understand their purpose more and the rules that govern them.
The Facade File (Public API)
Facade files adhere to the following rules:
- They Don't contain any
def
s, onlydefdelegates
. (See the defdelegate docs, also explained below) - They are the main place of documentation for the context's API.
- Named plurally (
Orgs
notOrg
). This allows the nameOrg
to be used for a database struct. It's also the convention that Phoenix uses. - Each
defdelegate
should have docs of why it exists, what it does, and a working example. (Doctests helps with the working part)
Example of a context's facade file:
defmodule YourApp.Widgets do
@moduledoc """
A widget is a unit of sale...
"""
alias YourApp.Widgets.Private.{
Create,
Update,
}
@doc """
Create a widget in the database. Any configuration options not specified will
be provided with defaults.
# Example
iex> {:ok, id} = Widgets.create()
"""
defdelegate create(config \\ %{}), to: Create, as: :call
@doc """
Update a widget's configuration options. If the update is a no-op it will return an error.
# Example
iex> {:ok, id} = Widgets.create()
iex> Widgets.update(id, color: "red")
"""
defdelegate update(id, config), to: Update, as: :call
end
The defdelegate
might be unfamiliar to you, so let me explain it. It's a part of the Elixir programming language, although not well known. I think it's best explained with an example.
This:
defdelegate update(id, config),
to: Update,
as: :call
is the same as this:
# 🚫 Don't do this, just an example
def update(id, config) do
Update.call(id, config)
end
Private Function Files
Private function files adhere to the following rules:
- Their filename matches the name of the function they represent.
- They have a single exposed function named
call
at the top of the module. - They must never be accessed directly, but rather through the facade.
- Should have
@moduledoc false
at the top so they are hidden from the generated documentation.
Example of a private function file:
defmodule YourApp.Widgets.Private.Create do
@moduledoc false
# this is the only public function
def call(config \\ %{}) do
# do stuff
end
defp helper_function() do
# ...
end
end
What does Private mean?
Public vs Private here means that different contexts (Widgets and Orgs) should never reference the other's private modules directly. For example, if widgets/private/create.ex
, needs access the functionality in orgs/private/whitelist.ex
it should go through the facade Orgs.whitelist()
rather than going directly to Orgs.Private.Whitelist.call()
.
If it looks like this, you're doing it wrong:
defmodule YourApp.Widgets.Private.Create do
# ❌ BAD, this has a `Private` in the alias, and it's not in the same context.
alias YourApp.Orgs.Private.Whitelist
def call() do
# 🚩 Red flag, shouldn't see `.call` in your code
Whitelist.call()
end
end
Example of doing it right:
defmodule YourApp.Widgets.Private.Create do
# ✅ Good, you only alias the facade
alias YourApp.Orgs
def call() do
# ✅ Good no `.call` in the your code
Orgs.whitelist()
end
end
If you're wondering about why I have an extra level of nesting using the private/
folder and the .Private.
in the module name, the reason is, it will make it more obvious that you're doing things wrong because you'll see Private
in your alias ...
line and, hopefully, rethink your decisions. I even dream that someday I'll write my own linting rule to look for people aliasing private modules, but haven't done it yet.
Another benefit to nesting under the Private
is that auto-complete will work better in iex
and VSCode, because you'll see the right functions first, rather than the modules (modules are usually listed first for some reason).
Also note that it's fine to have files that aren't listed in the context, but are in the Private folder. I often have a private/util.ex
file to share functionality between
the different private modules that are all in the same folder.
10% pattern 90% design
Although this pattern has helped me a lot in my projects, I will say that the main thing it did for me is force me to think about my API's.
Because when you have one file dedicated to the interface of your sub-modules, it's a lot easier to spot functions that do the same thing,
to spot naming inconsistencies, and it becomes a lot more obvious when something doesn't have a @doc
at the top of the function.
Despite this, if you don't care about your API's, it's not going to make any difference.
"If your application is difficult to use from
iex
* your APIs are probably wrong"
Some recommendations would be:
- The first argument is the id of the context (where it makes sense). For instance, do
Widget.update(id, settings)
notWidget.update(settings, id)
- Don't stutter in your APIs, meaning, do
Widget.create()
notWidget.create_widget()
. - Make the names work well together and are guessable. For instance all functions that get something start with
get_
. I find it useful to group functions this way due to autocomplete. I can always start typingWidget.get_...
and then it will tell me the different ways I can do it. Some style guides hate this, but I personally find it useful. - Don't pass in arguments that you could get from the database. For instance, don't pass in
Org.get_widgets(id, widget_ids)
just because the front-end happens to have those ids. It's better to doOrg.get_widgets(id)
even if it's a little slower. This is especially important because of security (it's easy to forget to filter out ones that aren't owned by the user).
FAQs
1. Isn't it a bit crazy to have NO def
s in the Facade? Why can't we move them out into a new file when they get too big?
The main reason I made a rule about only using defdelegate
as opposed to normal def
s is the Facade is public. When I say public, think "town square" or "neighborhood park". Everybody owns it and everyone needs to take care of it. Its main purpose is to be a sort of crossroads into other modules.
While this may seem harmless, over time, it's very likely people will put all their code into this file until it becomes thousands of lines long. A "Tragedy of the Commons" occurs:
The tragedy of the commons is a situation in a shared-resource system where individual users, acting independently according to their self-interest, behave contrary to the common good of all users by depleting or spoiling the shared resource through their collective action.
~ Wikipedia
When everybody is trying to get their ticket done, they are aren't too concerned with making sure the facade file stays clean, especially if nobody else is. This leads to everyone adding their 1 line. Nobody is incentivized to pull out the function into a new file, and so nobody does.
This gets even worse the more helper functions (namely defp
s) you get in this file, because it's unclear which ones are reused and which ones are there to make some other function shorter.
Pulling the implementation into a new function file doesn't quite solve the tragedy of the commons problem, but it limits its effect.
So keep facade files to @docs
s, @spec
s and defdelegates
. You'll be happier if you do. While you're at it, make sure there's a good @moduledoc
.
Also, it's important to always remember to design for a nice experience using your app via the REPL (iex
). Why? Because REPLs It allows you to see things really work without the mocking that you would have to do in tests. I also find when you are forced to type everything into a REPL the really long function names, and complex argument lists start to become a lot more apparent.
2. Doesn't this mean it takes more jumps to get to the right file?
Yeah, it does if you're following the references directly. However, I can take advantage of the fact that function files are named the same as the function they represent and just use a file finder as I have in VSCode with ⌘-P
(on a Mac, probably CTRL-P
on Windows?). So if I see Org.whitelist()
, I know if I do ⌘-P
and start typing whitelist...
, this will be quite a bit faster than digging through a huge file with all the functions in it, don't you think?
3. This seems like too much work. How can we make it easier?
I'm glad you asked (you didn't 😂, but just go with it). Code Generators!
Code Generators
One barrier to implementing this pattern is the fact that it's more work. Namely in modifying the facade file and creating a new file every time you want to put a function on the context, rather than doing def
in an existing file.
To fix this problem, I made a mix
command (mix gen.fn
) to generate new function files and update existing facade and function files. This command even generates a failing test to hint you towards doing TDD. Not to mention a few # TODO:...
comments to make credo force you to write some documentation.
It works like this:
λ ~/lib/your_app/ mix gen.fn "Email.feedback_email_contents(user_metadata, orgs, feedback)"
./lib/your_app/email.ex doesn't exist, creating...
File wrote to: ./lib/your_app/email.ex
File wrote to: ./lib/your_app/email/feedback_email_contents.ex
File wrote to: ./test/your_app/email/feedback_email_contents_test.exs
File modified: ./lib/your_app/email.ex
Here's the mix task. It's not a super clean implementation, but might serve you as a thing that you could change to suit your needs.
Concrete benefits
Some of the most concrete benefits my team and I have experienced are:
- Functions make more sense in context. It's easier to know that I'm creating a widget when I do
Widgets.create()
rather thanCreate.call
. - Easier to remember what to alias. I can
alias Widget
instead of the three or four functions I need from it. - Because of the previous point, it is super easy to use this API in IEx.
- Nothing is tied to the API endpoints, so it's easy to write a mix task that does the same thing.
- People write documentation! Crazy!
- Because the public functions are reusable, they get reused! I've even implemented new functions out existing ones, which feels good.
- The facade file looks very clean, which helps people feel like our app is something we take care of. No broken windows.
Conclusion
I hope you'll try this pattern out, it has helped me find and reuse code. I no longer feel like I'm digging through the guts of my car to just understand how to drive it.
Footnotes
- Software design pattern, if you're unfamiliar, are essentially templates of how to layout your code. They usually aren't embodied in a library although maybe in some cases they could be. For my purposes I'm not going to use any libraries, only using a bit of code that generates the file structure that I'm looking for that I'll include at the end.
Top comments (0)