DEV Community

Dwayne Crooks
Dwayne Crooks

Posted on

Wrapper modules can lead to better APIs

When building an Elm web app you may need to use a third-party package to help solve your problem. In some cases, your usage requirements may not perfectly align with the API provided by the package. In these cases, rather than scatter your use of the package throughout different modules of your application, it is usually better to limit your use of the package to a single module (which I will refer to as a wrapper module) with an API that better suits your application's needs.

Why use a wrapper module?

  1. It helps you reduce the coupling between different parts of your application. This in turn makes your application more flexible to changes. For e.g. if another package comes along that is better in some way, say it has better performance, then because the old package is used exclusively within a wrapper module it will be much easier to switch to the new package.
  2. You can define the API of your wrapper module independently of the API of the package. This allows you to design an API that's a better fit for your application.
  3. It can be easier to unit test your wrapper module.

Obviously, there are trade-offs and not every situation warrants it's use.

Learn more: Using third-party libraries - always use a wrapper?.

A practical example

The Flight Booker task from 7GUIs had certain date requirements that were easier to satisfy if I used a preexisting package. justinmimbs/date package had support for everything I needed to do and more but it did things a little differently.

My requirements were straightforward:

  • Be able to get today's date.
  • Be able to create a date from a string of the form DD.MM.YYYY.
  • Be able to determine if one date comes after another date.
  • Be able to convert a date to a string of the form DD.MM.YYYY.

By writing a wrapper module I was able to contain my use of justinmimbs/date package and expose the precise API I needed to handle the date requirements.

module Task.FlightBooker.Date exposing
    ( Date
    , fromString
    , isLaterThan
    , toString
    , today
    )

import Char
import Date as JDate
import Parser as P exposing ((|.), (|=))
import Task
import Task.FlightBooker.Parser as P


type Date
    = Date JDate.Date


today : (Date -> msg) -> Cmd msg
today toMsg =
    JDate.today
        |> Task.perform (toMsg << Date)


fromString : String -> Maybe Date
fromString s =
    case P.run dateParser s of
        Ok iso ->
            case JDate.fromIsoString iso of
                Ok date ->
                    Just <| Date date

                Err _ ->
                    Nothing

        Err _ ->
            Nothing


dateParser : P.Parser String
dateParser =
    P.succeed (\dd mm yyyy -> yyyy ++ "-" ++ mm ++ "-" ++ dd)
        |= P.chompExactly 2 Char.isDigit
        |. P.chompIf ((==) '.')
        |= P.chompExactly 2 Char.isDigit
        |. P.chompIf ((==) '.')
        |= P.chompExactly 4 Char.isDigit
        |. P.end


isLaterThan : Date -> Date -> Bool
isLaterThan (Date date1) (Date date2) =
    JDate.compare date2 date1 /= LT


toString : Date -> String
toString (Date date) =
    let
        dd =
            JDate.day date
                |> String.fromInt
                |> String.padLeft 2 '0'

        mm =
            JDate.monthNumber date
                |> String.fromInt
                |> String.padLeft 2 '0'

        yyyy =
            JDate.year date
                |> String.fromInt
                |> String.padLeft 4 '0'
    in
    dd ++ "." ++ mm ++ "." ++ yyyy
Enter fullscreen mode Exit fullscreen mode

Source: Task.FlightBooker.Date.

From the source code above you can see how I ended up with a smaller and focused API.

  1. There's a custom Date type that wraps the Date type from the justinmimbs/date package. A client of the wrapper module can only use the functions that work on my Date type. This is how usage of the external package is limited and controlled.
  2. There's one constructor named fromString that takes a String and returns a Date if the String is of the form DD.MM.YYYY.
  3. There's a function named isLaterThan , which is much nicer to use over direct use of the compare function that the justinmimbs/date package provides.
  4. There's a converter named toString that converts my Date type to a String of the form DD.MM.YYYY.
  5. And finally, there's a command tailored to my Date type for getting today's date.

Conclusion

Elm has great support for writing highly cohesive modular code that's loosely coupled and easy to maintain. With Elm's modules it's simple to hide implementation details and expose an API that caters to the precise requirements of your application. I hope the simple example I shared above gets you thinking about ways to clean up your own applications with helpful wrapper modules.

Top comments (2)

Collapse
 
eberfreitas profile image
Éber Freitas Dias

Amazing! Thank you for this post.

You can also do something "similar" but the other way around. Create a unified module with a custom type that will interface with multiple different libraries.

One scenario where that happened for me was when trying to use the same color values for different libraries. I wanted to use it for elm-css, which exposes a Color type, but I also wanted to manipulate it in ways that Css.Color wouldn't allow me to, so I created a "proxy" type that allowed me to convert it to different types as needed, while my application only had to deal with my own Color type.

Collapse
 
dwayne profile image
Dwayne Crooks

That's a nice approach.