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);
}
}
2 Answers 2
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).
-
\$\begingroup\$ In this case, how is the preference handled when it is being saved. The API user will have an instance of
Preference
, what willPreferenceManager
be able to do with that? \$\endgroup\$cldfzn– cldfzn2014年01月24日 22:10:51 +00:00Commented Jan 24, 2014 at 22:10 -
\$\begingroup\$ @cldfzn: Updated my answer. \$\endgroup\$ChrisWue– ChrisWue2014年01月24日 22:21:27 +00:00Commented 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– cldfzn2014年01月24日 22:36:41 +00:00Commented 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– cldfzn2014年01月24日 22:38:15 +00:00Commented Jan 24, 2014 at 22:38
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
- out-of-the-box defaults
- site-wide mandatory settings, not overridable by users
- site-wide defaults
- user overrides
You can decide whether you need such a system of cascading settings.
-
\$\begingroup\$ The preference options are not something I have control over. I am making the public API over an internal framework. \$\endgroup\$cldfzn– cldfzn2014年01月24日 21:37:31 +00:00Commented 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– cldfzn2014年01月24日 21:55:27 +00:00Commented Jan 24, 2014 at 21:55
Preference
should be an Interface instead of an Abstract class? \$\endgroup\$