12
\$\begingroup\$

I am creating an API for consumption by other developers to interface with an internal framework. My goal is to be able to have the developers type something like:

profile.setPreference(new GroupPreference(id));

or

UserPreference preference = new UserPreference();
preference.setDefaultInbox("nameOfInbox");
// set any other options, classes simplified
profile.setPreference(preference);

The internal framework has all preferences persisted in the same manner. A user profile can be either a set of preferences specific to them (UserPreference) or a relation to a group preference using an ID (GroupPreference). An example of current API usage:

// Context object 
Profile profile = userService.getProfile("accountName");

// Preference returned here is immutable
Preference preference = profile.getPreference();
preference.getDashboardOptions();
preference.getDefaultInbox();
// etc..

// To modify a users preferences
UserPreference userPreference = new UserPreference(preference);
userPreference.setDefaultInbox("newInbox");
// etc..

profile.setPreference(userPreference);

// Or to link it to a group preference
profile.setPreference(new GroupPreference(groupPreferenceId));

I handle saving of the preference information through a PreferenceManager as a part of updating the overall profile:

preferenceManager.savePreference(preference, profileContext);

Now for the questions:

  • The protected save method feels extremely cludgy but I didn't want to have any methods in the interface that potentially exposes implementation details. Is there a better way?
  • How is this design overall? Something need to be reorganized?

public abstract class Preference {
    /**
     * Provides access to DashboardOptions object to manage what options are enabled
     * and disabled on the users dashboard.
     * 
     * @return DashboardOptions object
     */
    public abstract DashboardOptions getDashboardOptions();
    /**
     * Gets the default inbox for the user.
     * 
     * @return Default inbox
     */
    public abstract String getDefaultInbox();
    /**
     * Method that will be overridden to handle saving of the preference.
     * 
     * @param profile ProfileContext passed in to provide information when saving preferences
     * @param service PreferenceService object that handles saving of preferences
     */
    protected abstract void save(ProfileContext profile, PreferenceService service);
}

public class GroupPreference extends Preference {
    private Long groupPreferenceId;

    public GroupPreference(Long groupPreferenceId) {
        this.groupPreferenceId = groupPreferenceId;
    }

    public Long getPreferenceId() {
        return groupPreferenceId;
    }

    @Override
    public DashboardOptions getDashboardOptions() {
        return null;
    }

    @Override
    public String getDefaultInbox() {
        return null;
    }

    @Override
    protected final void save(ProfileContext profile, PreferenceService service) {
        service.setUserPreference(profile.getId(), groupPreferenceId);
    }
}

public class UserPreference extends Preference {
    private UserDashboardOptions options;
    private String defaultInbox = "";

    public UserPreference() {
        this.options = new UserDashboardOptionsImpl();
    }

    public UserPreference(Preference preference) {
        this.options = new UserDashboardOptionsImpl(preference.getDashboardOptions());
    }

    @Override
    public UserDashboardOptions getDashboardOptions() {
        return options;
    }

    @Override
    public String getDefaultInbox() {
        return defaultInbox;
    }

    public void setDefaultInbox(String defaultInbox) {
        this.defaultInbox = defaultInbox;
    }

    @Override
    protected final void save(ProfileContext profile, PreferenceService service) {
        // The SubjectPreference is part of an internal framework I have to interface with
        SubjectPreference subjectPreference = service.createSubjectPreference(profile);
        service.saveSubjectPreference(subjectPreference);
    }
}

public class PreferenceManager {
    public void savePreference(Preference preference, ProfileContext profile, PreferenceService service) {
        preference.save(profile, service);
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ Preference should be an Interface instead of an Abstract class? \$\endgroup\$
    – rolfl
    Commented Jan 24, 2014 at 20:31
  • \$\begingroup\$ Needs to be an abstract class to declare the save methods as protected vs public. \$\endgroup\$
    – cldfzn
    Commented Jan 24, 2014 at 21:11

2 Answers 2

4
\$\begingroup\$

Your Preference class really looks like it should be an interface. And in fact it is actually two interfaces: The public one for the user and the internal one for the manager. So I'd suggest you split it up:

public interface Preference {
    /**
     * Provides access to DashboardOptions object to manage what options are enabled
     * and disabled on the users dashboard.
     * 
     * @return DashboardOptions object
     */
    DashboardOptions getDashboardOptions();

    /**
     * Gets the default inbox for the user.
     * 
     * @return Default inbox
     */
    String getDefaultInbox();
}

interface SaveablePreference extends Preference  // package private
{
    /**
     * Method that will be overridden to handle saving of the preference.
     * 
     * @param profile ProfileContext passed in to provide information when saving preferences
     * @param service PreferenceService object that handles saving of preferences
     */
    void save(ProfileContext profile, PreferenceService service);
}

Your users will only ever get the Preference but your implementations (UserPreference and GroupPreference) will implement SaveablePreference.

Update

Looking at it again it don't really see the point of the PreferenceManager. If the user will have to call the manager in order to save the preference then why don't make the save method public in the first place - so you end up with only one interface. Then the user can call preference.save(context, service) (the arguments he'd have to supply anyway when calling the manager).

\$\endgroup\$
4
  • \$\begingroup\$ In this case, how is the preference handled when it is being saved. The API user will have an instance of Preference, what will PreferenceManager be able to do with that? \$\endgroup\$
    – cldfzn
    Commented Jan 24, 2014 at 22:10
  • \$\begingroup\$ @cldfzn: Updated my answer. \$\endgroup\$
    – ChrisWue
    Commented Jan 24, 2014 at 22:21
  • \$\begingroup\$ It was mostly a bid at removing the call from the object in the public part of the API. I'd rather have that method serviced out to a service or repository layer object. In the case of a group preference, upon save the group preference data would be loaded in to an immutable preference object and set to return from profile.getPreference() (at least that is my thought currently). \$\endgroup\$
    – cldfzn
    Commented Jan 24, 2014 at 22:36
  • \$\begingroup\$ In terms of what the user would be doing Profile profile = userService.updateProfile(profile);. The call to save the preference wouldn't be explicit currently. \$\endgroup\$
    – cldfzn
    Commented Jan 24, 2014 at 22:38
3
\$\begingroup\$

Your design seems incredibly specific and inflexible. Any enhancement to the application is likely to require your Preference class to be modified. Furthermore, you will probably have to write a lot of custom code to implement persistence for the settings. Anytime you modify that code, you risk breaking compatibility with records already persisted to disk. Therefore, it would be best to stay simple and flexible.

Consider how preferences are stored on OS X (preference list), Windows (registry), and GNOME (gconf, an XML database). All of these are just key-value maps, with arbitrary strings as keys and a limited set of data types as values.

Something generic like the following suggestiong should probably be able to accommodate most applications.

public interface Preferences {
    void setString(String key, String value);
    void setBoolean(String key, boolean value);
    void setInt(String key, int value);
    void setTuple(String, int[] value); // to store color values, for example
    void setMap(String key, Map<String, String>value);
    void setBytes(String key, byte[] value);

    boolean hasKey(String key);

    String getString(String key, String default);
    boolean getBoolean(String key, boolean value);
    int getInt(String key, int default);
    int[] getTuple(String key, int[] default);
    Map<String, String> getMap(String key, Map<String, String> default);
    byte[] getBytes(String key, byte[] default);
}

On Java, see if you can reuse anything from java.util.Properties.


A more advanced feature of some preference managers is the ability to specify

  1. out-of-the-box defaults
  2. site-wide mandatory settings, not overridable by users
  3. site-wide defaults
  4. user overrides

You can decide whether you need such a system of cascading settings.

\$\endgroup\$
2
  • \$\begingroup\$ The preference options are not something I have control over. I am making the public API over an internal framework. \$\endgroup\$
    – cldfzn
    Commented Jan 24, 2014 at 21:37
  • \$\begingroup\$ For now, don't think of these objects as preferences and evaluate them as just some objects doing some things. \$\endgroup\$
    – cldfzn
    Commented Jan 24, 2014 at 21:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.