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
User
type, 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
User
record - 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]
= [george, porter]
users where
= User
george = "george"
{ userName = 1000
, userInternetPoints = "secret"
, userPassword = "gbird2015@example.com"
, userEmailAddress
}= User
porter = "porter"
{ userName = 500
, userInternetPoints = "hunter2"
, userPassword = "woofwoof@example.com"
, userEmailAddress }
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)
User{..} password
authenticateUser | userPassword == password = Just User{..}
| otherwise = Nothing
lookupUser :: String -> Maybe (User Unauthenticated)
=
lookupUser name -> userName user == name) users find (\user
Our 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.Authentication
Create 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
= User
makeUser name passwd email points = name
{ userName = passwd
, userPassword = email
, userEmailAddress = points
, userInternetPoints }
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 == userPassword user passwordAttempt
We’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
, testUserPasswordwhere )
Once 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]
= [george, porter]
users where
= makeUser "george" "secret" "gbird2015@example.com" 1000
george = makeUser "porter" "hunter2" "woofwoof@example.com" 500 porter
As 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
= userName user
name = userEmailAddress user
email = userInternetPoints user
points
lookupUser :: String -> Maybe (User Unauthenticated)
=
lookupUser name -> userName user == name) users find (\user
Which 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.