Wrapper modules can lead to better APIs

Dwayne Crooks - Aug 9 '23 - - Dev Community

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player