Dependency Inversion in Elm

Brian Watkins
Apr 15, 2018 · 14 min read

The Dependency Inversion Principle (DIP) can help you write software that’s easy to understand and easy to change. I’ll briefly describe the DIP, and then give an example of how to apply this principle in the Elm programming language.

Note: The examples below have been updated for Elm 0.19 and Elmer 5.0.0

Strategy vs Tactics

To use the dependency inversion principle, you’ll need to distinguish two aspects of your software. On the one hand, there is the application strategy: the set of policies, rules, procedures, and elements that characterize what the application does at a high-level. On the other hand, there are situational tactics: the low-level details necessary to achieve the application strategy in a particular runtime environment. For example, we can distinguish the fact that an application needs to persist data under certain conditions — its strategy — from the various tactics by which this could be accomplished: an in-memory database, writing data to disk, making HTTP requests to a web service, and so on. In general, an application strategy can be achieved with a variety of situational tactics.

When writing code, it’s easy to confuse application strategy with situational tactics. Suppose you are writing a game that persists the player’s final score via an HTTP request to some web service. In Elm, you might define your update function, model, and messages like so:

type Msg
= GameOver Int
| ReceivedScores (Result Http.Error (List Int))
type alias Model =
{ scores : List Int
}
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
GameOver score ->
( model
, Http.send ReceivedScores <| generateHttpRequest score
)
ReceivedScores (Ok scores) ->
( { model | scores = scores }, Cmd.none )
ReceivedScores (Err _) ->
( model, Cmd.none )
generateHttpRequest : Int -> Http.Request (List Int)
generateHttpRequest score =
-- Some function that returns an appropriate request

Here, we’ve expressed our application strategy — persist the score when the game is over and expect, in return, a list of the current scores — directly in terms of situational tactics — send some HTTP request that posts an integer (the score) to a server and gets a list of integers (the current scores) in response. This approach leaves us with code that is more difficult to understand and to change. If we need to adjust the application strategy, we have to do so in terms dictated by the situational tactics. If we need to change tactics, we risk accidentaly modifying the strategy.

The DIP asks us to express the application strategy in a way that is agnostic to situational tactics. We’ll define abstract interfaces that specify just what the application strategy needs from any modules that implement situational tactics, and we’ll express our application strategy in terms of these interfaces. When it comes to implementing tactics, we’ll do so in a way that conforms to the requirements spelled out by these interfaces. In this way, we’ll invert the naive dependency relationship between strategy and tactics that we saw in the example above. Application strategy will no longer be expressed in terms of particular tactics; instead tactics will depend on requirements dictated by the strategy. We’ll be left with code that is easier to change. We’ll have clarified our application strategy and put ourselves in a position to shift situational tactics as necessary without affecting the overall strategy.

To apply the DIP, we need a way to define abstract interfaces that can be used to express aspects of our application strategy and to specify requirements for modules that implement situational tactics. Languages like Java, C#, and Swift have language constructs — interfaces, protocols — that can be used for this purpose. What about Elm?

Function Types as Interfaces in Elm

Our approach to applying the DIP in Elm will be to use function types as abstract interfaces. In Elm, each function has a type that’s defined by the number, order, and type of arguments, plus the type of the return value. Functions of the same type are replaceable; whenever my code requires a function of some type, I can provide any concrete function with that same type. So, to apply the DIP in Elm, we’ll use function types to help us express our application strategy independently of any particular situational tactics. We’ll then provide concrete functions that conform to those types, which implement the situational tactics necessary to achieve the application strategy in some runtime environment.

Let’s apply the DIP to fix the confusion between strategy and tactics in the example we discussed above. Our strategy is to persist the user’s score when the game is over and subsequently receive a list of the current scores; we don’t — for the purposes of strategy — care so much how this is accomplished. We’ll first define function types that will let us express this strategy; in turn these will specify the requirements to which any functions that implement tactics must conform.

type alias Score =
Int
type alias PersistScore msg =
Score -> Cmd msg
type alias ScoresTagger msg =
List Score -> msg

We’ve defined a PersistScore function type that takes a Score— just an alias for an interger — and returns a Cmd value. Luckily, any function that talks to the outside world in Elm must do so via a Cmd, so our function type here is agnostic to how precisely the persistence of the score will occur. We’ve also defined a ScoresTagger function that specifies how our application strategy expects the list of scores to be tagged when they are provided to the update function.

Now we just need to change our update function so that it will use a function of the PersistScore type. We’ll create a function called updateWith that produces a function that does just that.

updateWith : PersistScore Msg -> Msg -> Model -> (Model, Cmd Msg)

Note that, here, we are relying on Elm’s capacity for partial function application. We’ll call updateWith with one argument — some function that conforms to the PersistScore type — and that will return a function that conforms to the update function type required for HTML programs.

At this point, we can see one key benefit of adopting dependency inversion. To write this code — to complete the implementation of the updateWith function — I don’t need to worry about how scores will be persisted. My only concern here is with clearly expressing the application strategy; I can defer decisions about situational tactics until later.

This means we can write a test that describes our application strategy without specifying the tactics used to achieve this. We do so by providing an implementation of PersistScore that adapts our code to the testing environment. Let’s do it!

To keep things simple, we’ll assume that our game consists merely in clicking the ‘Game Over’ button, and that the only score you receive is 4. We want to write a test that shows we save the score when the game is over, and that we do the right thing with the list of scores that is returned. We’ll use elm-test and Elmer to write our test:

import Elmer
import Elmer.Spy as Spy exposing (Spy, andCallThrough)
import Elmer.Spy.Matchers exposing (wasCalledWith, intArg)
import Elmer.Html as Markup
import Elmer.Html.Matchers exposing (hasText, element)
import Elmer.Html.Events as Events
import Elmer.Html.Selector exposing (by, id)
import Elmer.Command as Command
import App
gameOverTests : Test
gameOverTests =
describe "when the game is over" <|
let
testUpdate =
App.updateWith <| Spy.inject (\_ -> persistScoreFake)
state =
Elmer.given App.defaultModel App.view testUpdate
|> Spy.Use [ persistScoreSpy ]
|> Markup.target << by [ id "game-over-button" ]
|> Events.click
in
[ test "it saves the score" <|
\() ->
state
|> Spy.expect (\_ -> persistScoreFake) (
wasCalledWith [ intArg 4 ]
)
, test "it displays the updated scores" <|
\() ->
state
|> Markup.target << by [ id "high-scores" ]
|> Markup.expect (element <|
hasText "4" <&&>
hasText "97" <&&>
hasText "101"
)
]
persistScoreFake score =
Command.fake <| App.scoresTagger [ score, 97, 101 ]
persistScoreSpy : Spy
persistScoreSpy =
Spy.observe (\_ -> persistScoreFake)
|> andCallThrough

Notice a few things. First, we created a ‘fake’ implementation of PersistScore called persistScoreFake. We create a Spy to observe this function with persistScoreSpy and use Elmer.Spy.inject to provide the fake to the code under test.

Our test double, persistScoreFake, sends a fake command with a list of scores tagged with our app’s scoresTagger function; we added 97 and 101 to simulate some other scores. In this way, we mimic the behavior we expect from a real implementation of PersistScore— it saves the score and eventually sends back a list of the current scores. Our persistScoreSpy allows us to observe the arguments passed to the persistScoreFake so we can make expectations about them during our test.

To initialize our test, we provide the default model, the view function, and the update function. Notice that we obtain the update function by calling updateWith with our test implementation of the PersistScore function type (that’s what Spy.inject does). We then assert that when the game over button is clicked, two things happen: the PersistScore function was called with the score and we display the scores that are tagged in the appropriate way.

Let’s make it pass!

scoresTagger : ScoresTagger Msg
scoresTagger =
ReceivedScores
updateWith : PersistScore Msg -> Msg -> Model -> (Model, Cmd Msg)
updateWith persistScore msg model =
case msg of
GameOver ->
( model, persistScore 4 )
ReceivedScores scores ->
( { model | scores = scores }, Cmd.none )

We implemented scoresTagger to expose the message by which scores can be tagged. And we implemented the updateWith function to take in a PersistScore function and return a standard update function. Inside updateWith, we simply need to call the provided PersistScore function at the right time. We would need to implement the view function as well, but I’ll leave that as an exercise.

Our application strategy is now more clear, easier to understand, and effectively independent of any partcular tactics we might use to adapt this code to a particular environment. Plus, our code so far has been easy to write — we’re able to test the expected behavior at this level of our application without worrying yet about the low-level details required to save a score.

Simple Tactics First

Now that we’ve test-driven our application strategy, we can focus on the situational tactics needed to make our app work in production. We believe that we want to persist scores in a web service so players can compare their high scores. However, writing, standing up, and maintaining a web service will take time and money. By applying the DIP in our software, we’ve made it easy to shift tactics as necessary, and this frees us to try something simpler first. Instead of persisting scores in a web service, let’s use the LocalStorage API to store scores in the player’s own web browser. It will be easier to implement and we can get fast feedback from users in production to help us decide whether a web service for persisting scores is really worth it.

To communicate with the LocalStorage API in Elm, we’ll need to use ports. We’ll send a command via a port to write a score to the browser’s local storage and we’ll create a port subscription through which we can receive the updated list of scores. When we write the javascript side of the port, we’ll make sure that whenever we save a new score to local storage, we’ll send a message with the current scores back to the app via the subscription port. Here, I’ll just focus on the Elm side of things.

We’ll create a module called LocalStoragePersistScore with two exposed functions. The execute function will conform to the PersistScore function type and the subscriptions function will produce a subscription that tags the incoming list of scores in the way specified by the ScoresTagger function type. We’ll also define two ports, one to represent the command to save a score and one subscription to receive the incoming scores. Here are the types we’ll be working with:

port saveScore : Score -> Cmd msgport scores : ScoresTagger msg -> Sub msgexecute : Score -> Cmd msgsubscriptions : ScoresTagger msg -> Sub msg

Using Elmer, we can write a test for this module that evaluates the command generated by the execute function and simulates receiving a new list of scores via the subscription. Here it is:

import Elmer exposing (exactly)
import Elmer.Spy as Spy exposing (Spy, andCallFake)
import Elmer.Spy.Matchers exposing (wasCalledWith, intArg)
import Elmer.Subscription as Subscription
import Elmer.Command as Command
persistScoresTests : Test
persistScoresTests =
describe "when a score is persisted" <|
let
testCommand = \() ->
LocalStoragePersistScore.execute <| 217
testSubscriptions = \_ ->
LocalStoragePersistScore.subscriptions ScoreTagger
testState =
Command.given testCommand
|> Spy.use [ saveScoreSpy, getScoresSpy ]
|> Subscription.with (\() -> testSubscriptions)
|> Subscription.send "scores-sub" [ 190, 217, 218, 332 ]
in
[ test "it requests the scores" <|
\() ->
testState
|> Spy.expect (\_ -> LocalStoragePersistScore.saveScore) (
wasCalledWith [ intArg 217 ]
)
, test "it sends the recorded scores" <|
\() ->
testState
|> Command.expectMessages (exactly 1 <|
Expect.equal (ScoreTagger [ 190, 217, 218, 332 ])
)
]
type TestMsg
= ScoreTagger (List Score)
saveScoreSpy : Spy
saveScoreSpy =
Spy.observe (\_ -> LocalStoragePersistScore.saveScore)
|> andCallFake (\_ ->
Cmd.none
)
getScoresSpy : Spy
getScoresSpy =
Spy.observe (\_ -> LocalStoragePersistScore.scores)
|> andCallFake (\tagger ->
Subscription.fake "scores-sub" tagger
)

We use two Spy values to observe the port functions during our test. The saveScoreSpy stubs the saveScore port; it allows us to see that this port was called with the right arguments during the test. The getScoresSpy stubs the scores port so that it returns a fake subscription that we can use to provide data during the test. Our tests expect that once the command returned by the execute function is processed, the saveScore port will be called with the right argument (the score) and when values are received via the subscription they will be tagged in the appropriate way.

Our implementation is quite simple. We just need to call the ports at the right time.

port requestScores : Score -> Cmd msgport scores : ScoresTagger msg -> Sub msgexecute : Score -> Cmd msg
execute =
requestScores
subscriptions : ScoresTagger msg -> Sub msg
subscriptions tagger =
scores tagger

We can now configure our app to use the situational tactics implemented by this module. In our main function, we just use the functions from the LocalStoragePersistScore module as necessary to produce the update and subscriptions functions.

subscriptions : model -> Sub msg
subscriptions _ =
LocalStoragePersistScore.subscriptions App.scoresTagger
main =
Html.program
{ init = App.defaultModel
, view = App.view
, update = App.updateWith LocalStoragePersistScore.execute
, subscriptions = subscriptions
}

Shifting Tactics

Let’s suppose that time has passed, our game has proven to be popular, and we now want to let players compare their scores. We need to shift tactics so that scores are no longer persisted in the player’s browser but in a web service that maintains a common list of high scores.

Since we’ve followed the DIP in building our software, this kind of change is relatively easy. We write a new implementation of the PersistScore function type — and configure our app to use it instead of LocalStoragePersistScore.

Let’s call our new module HttpPersistScore. It will expose one function called executeWith that we use to build a function that conforms to the PersistScore type. This function will need to POST a score to the web service, GET the current list of scores, and tag those scores using a function that conforms to the ScoresTagger type.

Let’s start with a test!

import Elmer exposing (exactly)
import Elmer.Command as Command
import Elmer.Spy as Spy exposing (Spy)
import Elmer.Spy.Matchers exposing (wasCalledWith, intArg)
import Elmer.Http as Http exposing (HttpResponseStub)
import Elmer.Http.Matchers exposing (hasBody)
import Elmer.Http.Stub as Stub exposing (withBody)
import Elmer.Http.Route exposing (get, post)
persistScoreTests : Test
persistScoreTests =
describe "when a score is persisted"
[ describe "when the request is successful" <|
let
testCommand = \() ->
HttpPersistScore.executeWith
"http://fake-server/scores"
ScoreTagger
87
state =
Command.given
|> Spy.use
[ Http.serve
[ storeScoreStub 87
, scoreRequestStub [ 81, 87, 98, 123 ]
]
]
in
[ test "it POSTs the new score to the score service" <|
\() ->
state
|> Http.expect (post "http://fake-server/scores") (
exactly 1 <| hasBody "{\"score\":87}"
)
, test "it GETs the scores" <|
\() ->
state
|> Http.expectRequest (get "http://fake-server/scores")
, test "it returns the scores" <|
\() ->
state
|> Command.expectMessages (exactly 1 <|
Expect.equal (ScoreTagger [ 81, 87, 98, 123 ])
)
]
type TestMsg
= ScoreTagger (List Score)
storeScoreStub : Score -> HttpResponseStub
storeScoreStub score =
Stub.for (post "http://fake-server/scores")
|> withBody ("{\"score\":" ++ toString score ++ "}")
scoreRequestStub : List Score -> HttpResponseStub
scoreRequestStub scores =
Stub.for (get "http://fake-server/scores")
|> withBody (bodyForScores scores)
bodyForScores : List Score -> String
bodyForScores scores =
"["
++ String.join ","
(List.map (\s -> "{\"score\":" ++ toString s ++ "}") scores)
++ "]"

Our HttpPersistScore.executeWith function takes a string that specifies the web service endpoint and a function to tag the list of scores; it returns a function that conforms to the PersistScore type. In the test, we define an HttpResponseStub for the two requests to the web service — one to save the score and one to fetch the current list. Our tests describe the expected behavior: when the command is processed, the score is posted, the current list is fetched, and the data is tagged in the expected way.

Once we start to make this test pass, we notice a snag. The elm/http module needs to tag the result of an Http request with a function of the type Result Http.Error a -> msg, where a is the type of value returned from successfully processing the response and Http.Error is an error message that would be provided should anything go wrong. This is worrisome since we don’t want to express our application strategy in terms dictated by situational tactics. To avoid this, we’ll create a function in HttpPersistScore that wraps the kind of tagger specified by our application strategy — a ScoresTagger — and translates it into the kind that can be used by the Http module.

import Http
import Task exposing (Task)
import Json.Decode as Json exposing (Decoder)
import Json.Encode as Encode exposing (Value)
executeWith : String -> ScoresTagger msg -> Score -> Cmd msg
executeWith uri tagger score =
let
tagHighScores = highScoreTagger tagger
storeScore = storeScoreTask uri
getScores = getScoresTask uri
in
storeScore score
|> Task.andThen (\_ -> getScores)
|> Task.attempt tagHighScores
storeScoreTask : String -> Score -> Task Http.Error ()
storeScoreTask uri score =
Http.post uri (Http.jsonBody <| encode score) (Json.succeed ())
|> Http.toTask
|> Task.onError (\_ -> Task.succeed ())
getScoresTask : String -> Task Http.Error (List Score)
getScoresTask uri =
Http.get uri (Json.list scoreDecoder)
|> Http.toTask
highScoreTagger :
ScoresTagger msg
-> Result Http.Error (List Score)
-> msg
highScoreTagger tagger result =
case result of
Ok scores ->
tagger scores
Err _ ->
tagger []
scoreDecoder : Decoder Score
decoder =
Json.field "score" Json.int
encode : Score -> Value
encode score =
Encode.object
[ ( "score", Encode.int score )
]

Notice the highScoreTagger function. It takes the tagger we want, one that conforms to the ScoresTagger type, but returns a function we can use to tag the results of Http requests. In this way, we prevent details about tactics from leaking through our abstraction.

To use the new tactics defined in HttpPersistScore we just need to alter our main function:

persistScore : PersistScore msg
persistScore =
HttpPersistScore.executeWith
http://my-server.com/scores"
App.scoresTagger
main =
Html.program
{ init = App.defaultModel
, view = App.view
, update = App.updateWith persistScore
, subscriptions = \_ -> Sub.none
}

We no longer need any subscriptions, but we do need to configure the update function. We produce a function that conforms to PersistScore by calling HttpPersistScore.executeWith with the proper API endpoint and the ScoresTagger function required by our application strategy. We then just plug our configured PersistScore implementation into App.updateWith.

Our app now uses our web service to persist scores. We achieved a major shift in tactics without touching the code that defines our application strategy.

To sum up, we’ve seen several benefits of using the DIP:

  • We can focus first on expressing our application strategy, deferring decisions about tactics until later.
  • Our code becomes easy to test.
  • A clear separation between strategy and tactics makes our code easy to understand.
  • It’s easy to shift tactics as we learn more about what works best for the software and its users.

That’s the power of dependency inversion, even in a functional language like Elm! If you’d like to see a complete, test-driven implementation of an Elm app that puts this principle to work, check out this repo.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade