Refactoring UserInfo
Refactoring UserInfo
Refactor the HaskellBook.Examples.UserInfo module into smaller modules. Try to
look at different ways that you can separate out the concerns of authentication,
looking up a particular user, and getting information about a user.
Hints
Click to reveal
You may want to rethink the export list from the module. Do you need to export more things if you break the module up? Are there ways around that?
Solution
Click to reveal
If we review the final version of our UserInfo module from Chapter 5, you’ll
see that our module is doing several different things:
- Defines the
Usertype, and helper functions for interacting with it - Creates a known set of users and lets us look up a user by name
- Provides a method for authenticating a user, given their password
At first glance it would seem like we could easily separate out these three concerns into three separate modules without any trouble. Looking a bit more closely though, we have a problem: In our original module, we opted not to export any functions directly related to the password. This let us ensure that anyone using our module couldn’t misuse the password in some insecure way. Unfortunately, we need access to be able to supply a password to each of our known users, and we need to be able to check to see if a password is valid if we want to build authentication. If we want to refactor the module, we’ll need to make some decisions. Let’s look at a few of our options. None of these choices are necessarily better than the others, they simply offer different tradeoffs.
Don’t Restrict the Password Field
Perhaps the simplest approach to refactoring our code is to break our module up into three new modules that:
- Define the
Userrecord - Provides a set of default users
- Handles finding and authenticating users
Let’s take a look at this approach to refactoring (Don’t worry about the change in module layout outside of this exercise, this is an artifact of the way the solutions are written for this site).
First, we’ll look at our new User module. we’re exporting everything from
User record now, so there’s no need for an explicit export list:
module EffectiveHaskell.Exercises.Chapter5.UserInfo.User where
data User isAuthenticated = User
{ userName :: String
, userPassword :: String
, userEmailAddress :: String
, userInternetPoints :: Int
}Next let’s write our KnownUsers module, which will container our predefined
user list:
module EffectiveHaskell.Exercises.Chapter5.UserInfo.KnownUsers where
import EffectiveHaskell.Exercises.Chapter5.UserInfo.User
users :: [User a]
users = [george, porter]
where
george = User
{ userName = "george"
, userInternetPoints = 1000
, userPassword = "secret"
, userEmailAddress = "gbird2015@example.com"
}
porter = User
{ userName = "porter"
, userInternetPoints = 500
, userPassword = "hunter2"
, userEmailAddress = "woofwoof@example.com"
}You’ll notice that in this example we’re defining the list of users, but we
haven’t yet written our lookupUser function. That will be included in the
Authentication module that we’ll look at next. The reason for this is that
we’ve also not yet defined our Authenticated and Unauthenticated
types. Since we want lookupUser to return an unauthenticated user, we’ll need
Unauthenticated to be in scope when we write lookupUser. Let’s go ahead and
look at our Authentication module now:
{-# LANGUAGE RecordWildCards #-}
module EffectiveHaskell.Exercises.Chapter5.UserInfo.Authentication where
import EffectiveHaskell.Exercises.Chapter5.UserInfo.User
import EffectiveHaskell.Exercises.Chapter5.UserInfo.KnownUsers
import Data.List
data Authenticated
data Unauthenticated
authenticateUser :: User Unauthenticated -> String -> Maybe (User Authenticated)
authenticateUser User{..} password
| userPassword == password = Just User{..}
| otherwise = Nothing
lookupUser :: String -> Maybe (User Unauthenticated)
lookupUser name =
find (\user -> userName user == name) usersOur Authentication module needs to import both the User module, for the
definition of the User record, and the KnownUsers module, for the list of
users.
Provide a High-Level Module With Select Re-Exports
Our earlier approach benefitted from requiring very little refactoring, but it
had a couple of drawbacks: The first problem was that, to interact with our
code, we’d often end up needing to import several of our modules. Before
refactoring, someone using our code would only have had to import a single
module. The second problem is that our refactoring required that we export
password information in our User module. One way that we can address that is
to create a high level module that selectively re-exports some features out of
the three modules we’ve just defined. For example, we might want to export
everything except for the userPassword field from User:
module EffectiveHaskell.Exercises.Chapter5.UserInfo
( module EffectiveHaskell.Exercises.Chapter5.UserInfo.User
, module EffectiveHaskell.Exercises.Chapter5.UserInfo.KnownUsers
, module EffectiveHaskell.Exercises.Chapter5.UserInfo.Authentication
)
where
import EffectiveHaskell.Exercises.Chapter5.UserInfo.User hiding (userPassword)
import EffectiveHaskell.Exercises.Chapter5.UserInfo.KnownUsers
import EffectiveHaskell.Exercises.Chapter5.UserInfo.AuthenticationCreate An Alternative To Directly Accessing Passwords
So far we’ve looked at solutions that involved compromising on our original
design by exporting the password field for our User type. That’s not the only
option we have. An alternative would be to add functions to the User module to
let us do only the things that we need to do with a password. In our case, we
need to be able to test whether a given password attempt for a user is correct,
and we need to be able to create new users, including setting a password for
them. We can do that by adding two new functions. First, makeUser will take
the place of the normal User value constructor, and will let us set a
password:
makeUser :: String -> String -> String -> Int -> User a
makeUser name passwd email points = User
{ userName = name
, userPassword = passwd
, userEmailAddress = email
, userInternetPoints = points
}Next, we can write a function called testUserPassword that will tell us if a
password attempt for a user is correct or not:
testUserPassword :: User a -> String -> Bool
testUserPassword user passwordAttempt =
passwordAttempt == userPassword userWe’ll also update the export list for our User module export our new
functions, and make sure it doesn’t export userPassword:
module EffectiveHaskell.Exercises.Chapter5.UserInfo.User
( User
, userName
, userEmailAddress
, userInternetPoints
, makeUser
, testUserPassword
) whereOnce we’ve finished up our changes to User we’ll need to update KnownUsers
and Authentication as well. Let’s start by updating KnownUsers to use
makeUser:
module EffectiveHaskell.Exercises.Chapter5.UserInfo.KnownUsers where
import EffectiveHaskell.Exercises.Chapter5.UserInfo.User
users :: [User a]
users = [george, porter]
where
george = makeUser "george" "secret" "gbird2015@example.com" 1000
porter = makeUser "porter" "hunter2" "woofwoof@example.com" 500As you can see, there’s not much that needs to change in our known user
definitions. Let’s move onto Authentication where we’ll need to change the
definition of authenticatedUser to call testUserPassword instead of testing
the password directly. This function doesn’t need to be changed much, but we’ll
need to stop using record wildcards now that we’re no longer exporting the
User constructor.
module EffectiveHaskell.Exercises.Chapter5.UserInfo.Authentication where
import EffectiveHaskell.Exercises.Chapter5.UserInfo.User
import EffectiveHaskell.Exercises.Chapter5.UserInfo.KnownUsers
import Data.List
data Authenticated
data Unauthenticated
authenticateUser :: User Unauthenticated -> String -> Maybe (User Authenticated)
authenticateUser user password
| testUserPassword user password = Just $ makeUser name password email points
| otherwise = Nothing
where
name = userName user
email = userEmailAddress user
points = userInternetPoints user
lookupUser :: String -> Maybe (User Unauthenticated)
lookupUser name =
find (\user -> userName user == name) usersWhich Approach is Most Common?
All of the approaches we’ve looked at in this exercise are things you might see in a real codebase- including the original version that didn’t break the larger module up into smaller components. If you do opt for doing a refactor, choosing a higher level module that has a more restrictive export list than the lower level modules it re-exports from is likely the most common and has the best ergonomics.