When I write the Google API for .NET, there are a lot of string parameters, like imageColor and safeLevel etc.
So I create an enum for every kind of parameter.
public enum SafeLevel
{
off,
moderate = 0,
active,
}
I even set the moderate of SafeLevel as default value. And I can use the ToString method to get the string value most of the time.
Not every string parameter are so readable. They use “r” for relevance and “d” for date for instance.
At this time, I use extension method + switch.
public string GetString(this SortType value)
{
switch (value)
{
case SortType.relevance:
return "r";
case SortType.date:
return "d";
default:
return null;
}
}
Or I use some helper class like LanguageUtility.
Everything is OK until Google add more choices of parameters.
Someone complained Google now can translate XXX language. Then I have to release a new version to follow the pace of Google!
The bad day has gone. Now I use string parameter directly. One can translate any language Google supported without complain and waiting.
But without enum and the helper class, API user need to check every parameter code from Google (like me).
For keeping life easy for all of you, I create the simple Enumeration class
public enum SafeLevel
/// <summary>
/// The enumeration. For parameters of Google APIs.
/// </summary>
public abstract class Enumeration
{
private readonly string name;
private readonly string value;
private readonly bool isDefault;
/// <summary>
/// Initializes a new instance of the <see cref="Enumeration"/> class.
/// </summary>
/// <param name="value">The value.</param>
protected Enumeration(string value)
: this(value, value)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Enumeration"/> class.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
protected Enumeration(string name, string value)
: this(name, value, false)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Enumeration"/> class.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
/// <param name="isDefault">if set to <c>true</c> it is default value.</param>
protected Enumeration(string name, string value, bool isDefault)
{
this.name = name;
this.isDefault = isDefault;
this.value = value;
}
/// <summary>
/// Gets a value indicating whether this instance is default.
/// </summary>
/// <value>
/// <c>true</c> if this instance is default; otherwise, <c>false</c>.
/// </value>
public bool IsDefault
{
get
{
return this.isDefault;
}
}
/// <summary>
/// Gets the value.
/// </summary>
/// <value>The value.</value>
public string Value
{
get
{
return this.value;
}
}
/// <summary>
/// Gets the name.
/// </summary>
/// <value>The name.</value>
public string Name
{
get
{
return this.name;
}
}
/// <summary>
/// Performs an implicit conversion from <see cref="Google.API.Enumeration"/> to <see cref="System.String"/>.
/// </summary>
/// <param name="enumeration">The enumeration.</param>
/// <returns>The result of the conversion.</returns>
public static implicit operator string(Enumeration enumeration)
{
if (enumeration.IsDefault)
{
return null;
}
return enumeration.Value;
}
/// <summary>
/// Returns a <see cref="System.String"/> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="System.String"/> that represents this instance.
/// </returns>
public override string ToString()
{
return this.Name;
}
/// <summary>
/// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
/// </summary>
/// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
/// <returns>
/// <c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.
/// </returns>
/// <exception cref="T:System.NullReferenceException">
/// The <paramref name="obj"/> parameter is null.
/// </exception>
public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj == null)
{
return false;
}
if (!this.GetType().IsInstanceOfType(obj))
{
return false;
}
return this.Value == ((Enumeration)obj).Value;
}
/// <summary>
/// Returns a hash code for this instance.
/// </summary>
/// <returns>
/// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
/// </returns>
public override int GetHashCode()
{
Debug.Assert(this.Value != null, "Value cannot be null.");
return this.Value.GetHashCode();
}
}
}
It has three readonly properties: Name, Value and IsDefault.
It can also convert to string implicitly. Then you can use it as string. Or you can make it looks like the type of parameter is Enumeration but not string.
TranslateClient client = new TranslateClient();
string translated = client.Translate(text, Language.ChineseSimplified, Language.English);
It looks the same of the old enum parameter version. But the Language is not a enum but a subclass of Enumeration and the English is a public static readonly field.
public static readonly Language English = new Language("English", "en");
Here I want add two more static method and a implicit convert method.
public static Language GetDefault();
public static ICollection<Language> GetEnums();
public static implicit operator Language(string value)
It’s not hard using reflection. But I need repeat them in every subclasses of Enumeration.
Although code template tool can help me a lot, but I do not think it’s a good I idea. Don’t repeat yourself!
So I create a generic Enumeration class:
public abstract class Enumeration<T> : Enumeration, IEquatable<T> where T : Enumeration<T>
This class has a very interesting constrain. We can only create a subclass like this:
public sealed class SortType : Enumeration<SortType>
The subclass must pass itself as the generic parameter. (Tell me if I’m wrong.) In other words, the T in Enumeration is must be the type of subclass.
Here I marked SortType as sealed, because the Enumeration can only know it subclass but not sub-subclass.
Here is the whole code of Enumeration:
/// <summary>
/// The enumeration. Provide more static methods and properties for every concrete enumeration.
/// </summary>
/// <typeparam name="T">The type of concrete enumeration.</typeparam>
public abstract class Enumeration<T> : Enumeration, IEquatable<T>
where T : Enumeration<T>
{
private static T @default;
private static IDictionary<string, T> dictionary;
/// <summary>
/// Initializes a new instance of the <see cref="Enumeration<T>"/> class.
/// </summary>
/// <param name="value">The value.</param>
protected Enumeration(string value)
: base(value)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Enumeration<T>"/> class.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
protected Enumeration(string name, string value)
: base(name, value)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Enumeration<T>"/> class.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
/// <param name="isDefault">if set to <c>true</c> it is default value.</param>
protected Enumeration(string name, string value, bool isDefault)
: base(name, value, isDefault)
{
}
/// <summary>
/// Gets the dictionary of value and enumeration.
/// </summary>
/// <value>The dictionary.</value>
protected static IDictionary<string, T> Dictionary
{
get
{
Initialize();
return dictionary;
}
}
/// <summary>
/// Gets the default enumeration.
/// </summary>
/// <returns>The default enumeration</returns>
public static T GetDefault()
{
Initialize();
return @default;
}
/// <summary>
/// Gets all enumerations.
/// </summary>
/// <returns>All enumerations</returns>
public static ICollection<T> GetEnums()
{
return Dictionary.Values;
}
/// <summary>
/// Indicates whether the current object is equal to another object of the same type.
/// </summary>
/// <param name="other">An object to compare with this object.</param>
/// <returns>
/// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
/// </returns>
public bool Equals(T other)
{
if (other == null)
{
return false;
}
return this.Value == other.Value;
}
/// <summary>
/// Initializes this instance.
/// </summary>
protected static void Initialize()
{
if (dictionary == null)
{
dictionary = new Dictionary<string, T>();
var type = typeof(T);
////var enums =
//// from propertyInfo in
//// type.GetProperties(
//// BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.GetProperty)
//// where
//// propertyInfo.PropertyType.IsAssignableFrom(typeof(T)) &&
//// propertyInfo.GetIndexParameters().Length == 0
//// select propertyInfo.GetValue(null, null) as T;
var enums =
from fieldInfo in
type.GetFields(
BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.GetField)
where fieldInfo.FieldType.IsAssignableFrom(type)
select fieldInfo.GetValue(null) as T;
foreach (var @enum in enums)
{
Debug.Assert(@enum != null, "enum cannot be null.");
dictionary[@enum.Value] = @enum;
if (@default == null && @enum.IsDefault)
{
@default = @enum;
}
}
}
}
/// <summary>
/// Converts the specified value to this enumeration.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="construct">The constructor function.</param>
/// <returns>The enumeration.</returns>
protected static T Convert(string value, Func<string, T> construct)
{
if (value == null)
{
return GetDefault();
}
T @enum;
if (!Dictionary.TryGetValue(value, out @enum))
{
@enum = construct(value);
}
return @enum;
}
}
Here I use the lazy load trick but not initialize in the static constructor, because the static constructor will run before than the static fields in subclass have been assigned.
And the implement convert operation can only defined in the subclass. So I put most of the logic into the Convert method.
Here is what a subclass looks like:
/// <summary>
/// The search safety level.
/// </summary>
public sealed class SafeLevel : Enumeration<SafeLevel>
{
/// <summary>
/// Disables safe search filtering.
/// </summary>
public static readonly SafeLevel Off = new SafeLevel("Off", "off");
/// <summary>
/// Enables moderate safe search filtering. Default value.
/// </summary>
public static readonly SafeLevel Moderate = new SafeLevel("Moderate", "moderate", true);
/// <summary>
/// Enables the highest level of safe search filtering.
/// </summary>
public static readonly SafeLevel Active = new SafeLevel("Active", "active");
private SafeLevel(string value)
: base(value)
{
}
private SafeLevel(string name, string value)
: base(name, value)
{
}
private SafeLevel(string name, string value, bool isDefault)
: base(name, value, isDefault)
{
}
/// <summary>
/// Performs an implicit conversion from <see cref="System.String"/> to <see cref="SafeLevel"/>.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>The result of the conversion.</returns>
public static implicit operator SafeLevel(string value)
{
return Convert(value, s => new SafeLevel(s));
}
}
Although the solution has some smell, like visit static method in superclass and static protected method.
I hope it can solve my problem well.
Thanks for reading.