Dependencies and umbrella projects

Dependencies and umbrella projects

This chapter is part of the Mix and OTP guide and it depends on previous chapters in this guide. For more information, read the introduction guide or check out the chapter index in the sidebar.

In this chapter, we will discuss how to manage dependencies in Mix.

Our kv application is complete, so it’s time to implement the server that will handle the requests we defined in the first chapter:

CREATE shopping
OK

PUT shopping milk 1
OK

PUT shopping eggs 3
OK

GET shopping milk
1
OK

DELETE shopping eggs
OK

However, instead of adding more code to the kv application, we are going to build the TCP server as another application that is a client of the kv application. Since the whole runtime and Elixir ecosystem are geared towards applications, it makes sense to break our projects into smaller applications that work together rather than building a big, monolithic app.

Before creating our new application, we must discuss how Mix handles dependencies. In practice, there are two kinds of dependencies we usually work with: internal and external dependencies. Mix supports mechanisms to work with both of them.

External dependencies

External dependencies are the ones not tied to your business domain. For example, if you need a HTTP API for your distributed KV application, you can use the Plug project as an external dependency.

Installing external dependencies is simple. Most commonly, we use the Hex Package Manager, by listing the dependency inside the deps function in our mix.exs file:

def deps do
  [{:plug, "~> 1.0"}]
end

This dependency refers to the latest version of Plug in the 1.x.x version series that has been pushed to Hex. This is indicated by the ~> preceding the version number. For more information on specifying version requirements, see the documentation for the Version module.

Typically, stable releases are pushed to Hex. If you want to depend on an external dependency still in development, Mix is able to manage git dependencies too:

def deps do
  [{:plug, git: "git://github.com/elixir-lang/plug.git"}]
end

You will notice that when you add a dependency to your project, Mix generates a mix.lock file that guarantees repeatable builds. The lock file must be checked in to your version control system, to guarantee that everyone who uses the project will use the same dependency versions as you.

Mix provides many tasks for working with dependencies, which can be seen in mix help:

$ mix help
mix deps              # Lists dependencies and their status
mix deps.clean        # Deletes the given dependencies' files
mix deps.compile      # Compiles dependencies
mix deps.get          # Gets all out of date dependencies
mix deps.tree         # Prints the dependency tree
mix deps.unlock       # Unlocks the given dependencies
mix deps.update       # Updates the given dependencies

The most common tasks are mix deps.get and mix deps.update. Once fetched, dependencies are automatically compiled for you. You can read more about deps by typing mix help deps, and in the documentation for the Mix.Tasks.Deps module.

Internal dependencies

Internal dependencies are the ones that are specific to your project. They usually don’t make sense outside the scope of your project/company/organization. Most of the time, you want to keep them private, whether due to technical, economic or business reasons.

If you have an internal dependency, Mix supports two methods to work with them: git repositories or umbrella projects.

For example, if you push the kv project to a git repository, you’ll need to list it in your deps code in order to use it:

def deps do
  [{:kv, git: "https://github.com/YOUR_ACCOUNT/kv.git"}]
end

If the repository is private though, you may need to specify the private URL [email protected]:YOUR_ACCOUNT/kv.git. In any case, Mix will be able to fetch it for you as long as you have the proper credentials.

Using git dependencies for internal dependencies is somewhat discouraged in Elixir. Remember that the runtime and the Elixir ecosystem already provide the concept of applications. As such, we expect you to frequently break your code into applications that can be organized logically, even within a single project.

However, if you push every application as a separate project to a git repository, your projects may become very hard to maintain as you will spend a lot of time managing those git repositories rather than writing your code.

For this reason, Mix supports “umbrella projects”. Umbrella projects are used to build applications that run together and have clear boundaries between them in a single repository. That is exactly the style we are going to explore in the next sections.

Let’s create a new Mix project. We are going to creatively name it kv_umbrella, and this new project will have both the existing kv application and the new kv_server application inside. The directory structure will look like this:

+ kv_umbrella
  + apps
    + kv
    + kv_server

The interesting thing about this approach is that Mix has many conveniences for working with such projects, such as the ability to compile and test all applications inside apps with a single command. However, even though they are all listed together inside apps, they are still decoupled from each other, so you can build, test and deploy each application in isolation if you want to.

So let’s get started!

Umbrella projects

Let’s start a new project using mix new. This new project will be named kv_umbrella and we need to pass the --umbrella option when creating it. Do not create this new project inside the existing kv project!

$ mix new kv_umbrella --umbrella
* creating .gitignore
* creating README.md
* creating mix.exs
* creating apps
* creating config
* creating config/config.exs

From the printed information, we can see far fewer files are generated. The generated mix.exs file is different too. Let’s take a look (comments have been removed):

defmodule KvUmbrella.Mixfile do
  use Mix.Project

  def project do
    [apps_path: "apps",
     start_permanent: Mix.env == :prod,
     deps: deps]
  end

  defp deps do
    []
  end
end

What makes this project different from the previous one is the apps_path: "apps" entry in the project definition. This means this project will act as an umbrella. Such projects do not have source files nor tests, although they can have their own dependencies. Each child application must be defined inside the apps directory.

Let’s move inside the apps directory and start building kv_server. This time, we are going to pass the --sup flag, which will tell Mix to generate a supervision tree automatically for us, instead of building one manually as we did in previous chapters:

$ cd kv_umbrella/apps
$ mix new kv_server --module KVServer --sup

The generated files are similar to the ones we first generated for kv, with a few differences. Let’s open up mix.exs:

defmodule KVServer.Mixfile do
  use Mix.Project

  def project do
    [app: :kv_server,
     version: "0.1.0",
     build_path: "../../_build",
     config_path: "../../config/config.exs",
     deps_path: "../../deps",
     lockfile: "../../mix.lock",
     elixir: "~> 1.4",
     start_permanent: Mix.env == :prod,
     deps: deps]
  end

  def application do
    [extra_applications: [:logger],
     mod: {KVServer.Application, []}]
  end

  defp deps do
    []
  end
end

First of all, since we generated this project inside kv_umbrella/apps, Mix automatically detected the umbrella structure and added four lines to the project definition:

build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",

Those options mean all dependencies will be checked out to kv_umbrella/deps, and they will share the same build, config and lock files. This ensures dependencies will be fetched and compiled once for the whole umbrella structure, instead of once per umbrella application.

The second change is in the application function inside mix.exs:

def application do
  [extra_applications: [:logger],
   mod: {KVServer.Application, []}]
end

Because we passed the --sup flag, Mix automatically added mod: {KVServer.Application, []}, specifying that KVServer.Application is our application callback module. KVServer.Application will start our application supervision tree.

In fact, let’s open up lib/kv_server/application.ex:

defmodule KVServer.Application do
  # See http://elixir-lang.org/docs/stable/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    # Define workers and child supervisors to be supervised
    children = [
      # Starts a worker by calling: KVServer.Worker.start_link(arg1, arg2, arg3)
      # worker(KVServer.Worker, [arg1, arg2, arg3]),
    ]

    # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: KVServer.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Notice that it defines the application callback function, start/2, and instead of defining a supervisor named KVServer.Supervisor that uses the Supervisor module, it conveniently defined the supervisor inline! You can read more about such supervisors by reading the Supervisor module documentation.

We can already try out our first umbrella child. We could run tests inside the apps/kv_server directory, but that wouldn’t be much fun. Instead, go to the root of the umbrella project and run mix test:

$ mix test

And it works!

Since we want kv_server to eventually use the functionality we defined in kv, we need to add kv as a dependency to our application.

In umbrella dependencies

Mix supports an easy mechanism to make one umbrella child depend on another. Open up apps/kv_server/mix.exs and change the deps/0 function to the following:

defp deps do
  [{:kv, in_umbrella: true}]
end

The line above makes :kv available as a dependency inside :kv_server and automatically starts the :kv application before the server starts.

Finally, copy the kv application we have built so far to the apps directory in our new umbrella project. The final directory structure should match the structure we mentioned earlier:

+ kv_umbrella
  + apps
    + kv
    + kv_server

We now need to modify apps/kv/mix.exs to contain the umbrella entries we have seen in apps/kv_server/mix.exs. Open up apps/kv/mix.exs and add to the project function:

build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",

Now you can run tests for both projects from the umbrella root with mix test. Sweet!

Remember that umbrella projects are a convenience to help you organize and manage your applications. Applications inside the apps directory are still decoupled from each other. Dependencies between them must be explicitly listed. This allows them to be developed together, but compiled, tested and deployed independently if desired.

Summing up

In this chapter we have learned more about Mix dependencies and umbrella projects. While we may run kv without a server, our kv_server depends directly on kv. By breaking them into separate applications, we gain more control in how they are developed, tested and deployed.

When using umbrella applications, it is important to have a clear boundary between them. Our upcoming kv_server must only access public APIs defined in kv. Think of your umbrella apps as any other dependency or even Elixir itself: you can only access what is public and documented. Reaching into private functionality in your dependencies is a poor practice that will eventually cause your code to break when a new version is up.

Umbrella applications can also be used as a stepping stone for eventually extracting an application from your codebase. For example, imagine a web application that has to send “push notifications” to its users. The whole “push notifications system” can be developed as an umbrella application, with its own supervision tree and APIs. If you ever run into a situation where another project needs the push notifications system, extraction should be straight-forward as long as the web application respects the push notification API boundary. Regardless if it happens in 2 weeks or in 3 years from development. Once extracted, the push notifications system can be moved to a private git repository or a public hex.pm package.

Developers may also use umbrella applications to break large business domains apart. The caution here is to make sure the domains don’t depend on each other (also known as cyclic dependencies). If you run into such situations, it means those applications are not as isolated from each other as you originally thought, and you have architectural and design issues to solve. Overall, umbrella applications do not magically improve the design of your code. They can, however, help enforce boundaries when the code is well designed.

Finally, keep in mind that applications in an umbrella project all share the same configurations and dependencies. If two applications in your umbrella need to configure the same dependency in drastically different ways or even use different versions, such is impossible in umbrellas, and those apps likely need to be moved to separate projects.

With our umbrella project up and running, it is time to start writing our server.

© 2012–2017 Plataformatec
Licensed under the Apache License, Version 2.0.
https://elixir-lang.org/getting-started/mix-otp/dependencies-and-umbrella-apps.html

在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号

意见反馈
返回顶部