Initiating some activities depending on the presence of certain URL parameters

Posted on

Problem

What is the best way to refactor this long if else statements?
make it shorter and more readable code.

I thought to use sub-classes but I don’t understand how to implement that, so examples will be blessed.

 public void redirectFromScheme(String scheme, Context context) {

        if (!scheme.contains(SCHEMA_WALLA)) {
            return;
        }

        HashMap<String, String> params = paramsUrlSplitter(scheme);
        int itemType = Integer.parseInt(params.get(KEY_ITEM_TYPE));
        String itemId = params.get(KEY_ITEM_ID);
        String genreId = params.get(KEY_GENRE_ID);

        if (scheme.startsWith(SCHEMA_MAIN)) {
            startMainActivity("");
        }

        else if (scheme.startsWith(SCHEMA_MOVIE)) {

            if (params.containsKey(KEY_GENRE_ID))
                startGenreActivity(params.get(KEY_GENRE_ID), MOVIES_TITLE, MOVIES_ID);
            else
                startMainActivity(MOVIES_TITLE);
        }

        else if (scheme.startsWith(SCHEMA_SERIES)) {

            if (params.containsKey(KEY_GENRE_ID))
                startGenreActivity(genreId, SERIES_TITLE, SERIES_ID);

             else if (params.containsKey(KEY_SERIES_ID))
                startSeriesActivity(context, params);
             else
                startMainActivity(SERIES_TITLE);
        }

        else if (scheme.startsWith(SCHEMA_KIDS)) {

            if (params.containsKey(KEY_GENRE_ID))
                startGenreActivity(params.get(KEY_GENRE_ID), KIDS_TITLE, KIDS_ID);
            else
                startMainActivity(KIDS_TITLE);
        }

        else if (scheme.startsWith(SCHEMA_ITEM)) {
            startMediaItemActivity(itemId, itemType);
        }

        else if (scheme.startsWith(SCHEMA_SEARCH)) {

            try {
                String url = URLDecoder.decode(params.get(KEY_SEARCH_WORD), UTF8);
                startSearchActivity(context, url);

            } catch (UnsupportedEncodingException ex) {
                Log.d(TAG, ex.getMessage());
            }
        }
    }

Solution

You can make a class Schema and subclasses that represent a specific schema (movie, series, kids, …). write a haldler-function that returns one of the subclasses. It depends a little bit what you are doing after all that with it.

I’d use a mixture of a Strategy Pattern and a Service Locator Pattern like system (without a cache, but you are free to add the cache if you like) where you have a central registration service where concrete services can register themselves and on querying the service locator it will iterate through the registered instances and evaluate the best matching service which implements a certain strategy which is then executed.

The general interface of a service or strategy does look like this:

public interface Activity {
    String getName();
    Activity copy(); // only needed if state should be stored in the activity
    void start(Map<String, String> params);
}

An activity registers itself at a service locator instance, which is a singleton for simplicity reasons. On a lookup method invocation, the registry will iterate through all registered activities and check it the provided scheme name starts with the the name of an activity. If found, a copy of the matching activity is returned.

public enum Registry {

    INSTANCE;

    private Set<Activity> registeredActivities = new HashSet<>();

    public void registerActivity(Activity activity) {
        this.registeredActivities.add(activity);
    }

    public void removeActivity(Activity activity) {
        this.registeredActivities.remove(activity);
    }

    public Optional<Activity> lookup(String schema) {
        for (Activity activity : this.registeredActivities) {
            if (schema.startsWith(activity.getName()) {
                return Optional.of(activity.copy());
            }
        }
        return Optional.empty();
    }
}

As the registry will return a reference to the registered activity on a match, storing any state will thus be shared among multiple lookups of this activity. I.e. if a movie activity is looked up twice and some state is stored inside the activity, this will also affect the other movie as both are references to the same object (the one stored in the registry). To avoid this behavior, a copy is returned by the registry. If no state will ever be stored inside an activity, the copy method can simply be removed.

A concrete activity (strategy) implementation might look something like the sample below. The actual registration of the activity in the registry may of course also be done outside of the class, f.e. in the main method when you setup your application.

public Movie implements Activity{

    public Movie() {
        Registry.INSTANCE.registerActivity(this);
    }

    @Override
    public Activity copy() {
        return new Movie();
    }

    @Override
    public String getName() {
        return SCHEMA_MOVIE;
    }

    @Override
    public void start(Map<String, String> params) {
        if (params.containsKey(KEY_GENRE_ID)) {
            // invoke genre logic
        } else {
            // invoke main activity logic
        }
    }
}

Each concrete activity-constructor needs to be invoked at least once in order to get registered with the Registry class. Therefore, not registering the activity within the constructor implicitly but registering the activity explicitely using Registry.INSTANCE.registerActivity(new Movie()); may thus be clearer compared to simply putting new Movie(); at the start of your application (or at least before the redirectFromScheme method is invoked for the first time).

The current redirectFromScheme(String, Context) method could now get refactored to something like this:

public void redirectFromScheme(String scheme, Context context) {
    if (!scheme.contains(SCHEMA_WALLA)) {
        return;
    }

    HashMap<String, String> params = paramsUrlSplitter(scheme);

    Optional<Activity> activity = Registry.INSTANCE.lookup(scheme);
    if (activity.isPresent()) {
        activity.start(params);
    }
}

Instead of an interface for the Activity you can also use an abstract base class which implements common logic needed by the different actions like the main activity.

The code does not contain any exception handling to focus on the core concept.


As you don’t have Optional class available pre Java 8, you can easily re-implement it like this:

public class Optional<T> {

    private T value;

    private Optional(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }

    public boolean isPresent() {
        return null != value;
    }

    public static <T> Optional<T> of(T value) {
        return new Optional<>(value);
    }

    public static <T> Optional<T> empty() {
        return new Optional<>(null);
    } 
}

This does not cover the full set of possiblilities the Java 8 Optional class offers, but it covers the functionality needed for the proposed changes.

Leave a Reply

Your email address will not be published. Required fields are marked *