Teaser Image

qnoid

Markos Charatzas - Edinburgh, UK




/**
     * Abusing if like there is no tomorrow
     */
    public class User
    {
        private static final String REGISTERING = "registering user '%s'";
        private static final String NOT_REGISTERED = "user '%s' not registered";
        private static final String ALREADY_REGISTERED = "User %s already registered";
        private static final String ALREADY_LOGGED_IN = "User %s already logged in";    

        private final String username;

        /*
         * Notice how your class is filled with non-final fields making it harder to
         * debug, make it thread-safe and extend.
         */
        private boolean registered;
        private boolean loggedIn;

        /**
         * @param username
         */
        public User(String username)
        {
            this.username = username;
            this.registered = false;
            this.loggedIn = false;
        }

        void nowRegistered() {
            this.registered = true;
        }

        void nowLoggedIn() {
            this.loggedIn = true;
        }

        public void register()
        {
            if(this.isRegistered()){
                throw new IllegalStateException( String.format(ALREADY_REGISTERED, this));
            }
            else
            {
                System.out.println( String.format(REGISTERING, this));
                this.nowRegistered();
            }
        }

        public void login()
        {
            if(this.isLoggedIn()){
                throw new IllegalStateException( String.format(ALREADY_LOGGED_IN, this) );            
            }
                    
            if(!this.isRegistered()){
                throw new UnregisteredUserException( String.format(NOT_REGISTERED, this) );
            }
            else
            {
                System.out.println( String.format("'%s' logged in", this) );
                this.nowLoggedIn();            
            }            
        }

        public boolean isRegistered() {
        return this.registered;
        }

        public boolean isLoggedIn() {
        return this.loggedIn;
        }

        @Override
        public String toString() {
        return this.username;
        }    
    }

    /**
     * Define your states
     */
    public enum UserState
    {
        UNREGISTERED,
        REGISTERED,
        LOGGED_IN;
    }

    /**
     * Common interface for your User and its states
     */
    public interface User
    {
        public void register();

        public void login();

        public boolean isRegistered();
        
        public boolean isLoggedIn();
    }

    /**
     * User can only register
     */
    class Unregistered implements User
    {
        private static final String REGISTERING = "registering user '%s'";
        private static final String NOT_REGISTERED = "user '%s' not registered";
        
        private final TheUser user;
        
        Unregistered(TheUser user)
        {
            this.user = user;
        }

        @Override
        public void register() 
        {
            System.out.println( String.format(REGISTERING, this.user));
            this.user.nowRegistered();
        }

        @Override
        public void login() {
        throw new UnregisteredUserException( String.format(NOT_REGISTERED, this.user) );
        }

        @Override
        public boolean isRegistered() {
        return false;
        }

        @Override
        public boolean isLoggedIn() {
        return false;
        }
    }


    /**
     * User can only login
     */
    class Registered implements User
    {
        private static final String ALREADY_REGISTERED = "User %s already registered";

        private final TheUser user;

        Registered(TheUser user)
        {
            this.user = user;
        }
        
        @Override
        public void register() {
        throw new IllegalStateException( String.format(ALREADY_REGISTERED, this.user));
        }

        @Override
        public void login()
        {
            System.out.println( String.format("'%s' logged in", this.user) );
            this.user.nowLoggedIn();
        }

        @Override
        public boolean isRegistered() {
        return true;
        }

        @Override
        public boolean isLoggedIn() {
        return false;
        }
    }

    /**
     * User cannot login or register
     */
    class LoggedIn implements User
    {
        private static final String ALREADY_REGISTERED = "User %s already registered";
        private static final String ALREADY_LOGGED_IN = "User %s already logged in";

        private final TheUser user;
        
        LoggedIn(TheUser user)
        {
            this.user = user;
        }

        @Override
        public void register() {
        throw new IllegalStateException( String.format(ALREADY_REGISTERED, this.user));
        }

        @Override
        public void login() {
        throw new IllegalStateException( String.format(ALREADY_LOGGED_IN, this.user) );
        }

        @Override
        public boolean isRegistered() {
        return true;
        }

        @Override
        public boolean isLoggedIn() {
        return true;
        }
    }

    /**
     * A user implemented as a state machine
     */
    public class TheUser implements User
    {
        public static final class TheUserBuilder
        {        
            private final String username;

            public TheUserBuilder(String username)
            {
                this.username = username;
            }

            /**
             * @return a new unregistered user
             */
            public TheUser build()
            {            
                TheUser user = new TheUser(this.username);
                
                User unregistered = new Unregistered(user);
                User registered = new Registered(user);
                User loggedin = new LoggedIn(user);
                
                user.addState(UserState.UNREGISTERED, unregistered);
                user.addState(UserState.REGISTERED, registered);
                user.addState(UserState.LOGGED_IN, loggedin);
                
                user.setState(unregistered);
                
            return user;
            }
        }

        /*
         * All possible states
         */    
        private final EnumMap<UserState, User> states = 
            new EnumMap<UserState, User>(UserState.class);
        
        private final String username;
        
        /*
         * The current state and only nonwfinal field
         */
        private User state;
        
        public User(String username)
        {
            this.username = username;
        }

        private void addState(UserState type, User state) {
            this.states.put(type, state);
        }

        private void setState(User state) {
            this.state = state;
        }

        private User getState(UserState type) 
        {
            User state = this.states.get(type);
            
            Preconditions.checkNotNull(state, 
                    String.format(UNSPECIFIED_STATE, state, state));
            
        return state;
        }

        private User registeredState() {
        return this.getState(UserState.REGISTERED);
        }

        private User loggedInState() {
        return this.getState(UserState.LOGGED_IN);
        }

        void nowRegistered() {
            this.setState( this.registeredState() );
        }

        void nowLoggedIn() {
            this.setState( this.loggedInState() );        
        }

        @Override
        public void register() {
            this.state.register();
        }

        @Override
        public void login() {
            this.state.login();
        }


        @Override
        public boolean isRegistered() {
        return this.state.isRegistered();
        }

        @Override
        public boolean isLoggedIn() {
        return this.state.isLoggedIn();
        }

        @Override
        public String toString() {
        return this.username;
        }
    }


    /**
     * Let's put it to the test
     */
    public class TheUserTest
    {
        @Test(expected=UnregisteredUserException.class)
        public void testUnregistered()
        {
            User user = new TheUserBuilder("cherouvim").build();                
            user.login();        
        }
        
        @Test
        public void testRegister()
        {
            User user = new TheUserBuilder("cherouvim").build();                
            user.register();
            
            Assert.assertTrue( user.isRegistered() );
            Assert.assertFalse( user.isLoggedIn() );
        }

        @Test
        public void testLogin()
        {
            User user = new TheUserBuilder("cherouvim").build();                
            user.register();
            user.login();
            
            Assert.assertTrue( user.isRegistered() );
            Assert.assertTrue( user.isLoggedIn() );
        }
    }

This is possibly the most hideous usage of if since not only makes your code hard to follow but also you have to keep track of your object's state since execution flow changes every time. All in your head that is.

In addition it's hard to figure out how the object behaves at each state as well as how the transitions between them happen. Because of that it makes it a pain to debug and to extend.

What you need to do instead is think of all the possible states your object can have and create an implementation for each. This way each state effectively encapsulates only what's responsible for. Notice how each state also sets the next valid state of your object depending on what method you called, making your object look as if it's a different one. For that you need to have a reference to your actual class.

As a result each state mirrors your class interface. If a method isn't valid for a particular state make sure that you throw an IllegalStateException. In that case you'll know early that something unexpected happened, which is most likely an error on how you wired your code.

Effectively you end up with state being the only non final field in your class.

Summary

  • Create an interface with all the methods related to the object's state
  • Create an implementation for each state
  • Have a single field in your actual class holding the current state
  • Delegate each call to the current state

As to how to identify the state machine itself, it really goes back to the beginning

Kudos to Ioannis Cherouvim who is a Java developer and regularly writes blogs posts about using Java effectively. Trying to convince him to join @qnoid and hopefully write some more here as well. If you have an invite to spare please let me know :)

The source code is available on github as stated previously. Feel free to fork it and add a LoggedOut state which allows the user to logout and set the appropriate state