We have all seen them. The strings lying around here and there in the code base. Some times at least exposed as constants in the class. But what else can you do? Well, you can have things like dedicated constant classes or key masters or value-objects or just allow the user to easily pick a value using a simple Func<,>
.
Before looking at the three "solutions" this blog post provides, we need to set a scene. Lets have some thing with a member that accepts a "country" being passed to it:
public class ScoreKeeper
{
public void Register(string country, int score)
{
//Do something with country and score
}
}
Ehh... What Country do you mean?
Glad you asked. That is the issue with this. You as a consumer of the Register
method gets very little help of what you can pass. Perhaps there is some documentation stating what values can be passed or what ISO-standard for country codes it support. If not, we are left with something that could be used in various ways:
scoreKeeper.Register("SWEDEN", 12);
scoreKeeper.Register("SE", 12);
scoreKeeper.Register("SWE", 12);
Lets improve.
The old friend "CountryConsts"
To be clear. I don't like this one. I just don't. For the sake of discovery, a class with constants helps very little. Sure there's a class that is named in a way that you know is about countries, but other than that, there isn't that much of an improvement.
scoreKeeper.Register(CountriesConst.Sweden, 12);
public class CountriesConst
{
public const string Sweden = "SE";
public const string Norway = "NO";
public const string Denmark = "DK";
}
Introducing the above would not help in discovery but it would at least improve the code base seen to refactoring, organization etc.
The Value-object "Country"
Introducing a Country
value-object gives us both assistant when it comes to discovery and also refactoring friendly and organised code. Further more, the Country class can be extended to e.g. implement implicit or explicit operator overloading for converting between a string coming from e.g. a database or similar. Sounds terrific, right? Do we have a winner perhaps?
scoreKeeper.Register(Country.Sweden, 12);
public class ScoreKeeper
{
public void Register(Country country, int score)
{
//Do something with country and score
}
}
public sealed class Country : IEquatable<Country>
{
private readonly string _code;
public static readonly Country Sweden = new Country("SE");
public static readonly Country Norway = new Country("NO");
public static readonly Country Denmark = new Country("DK");
private Country(string code)
{
_code = code;
}
public override bool Equals(object obj)
=> Equals(obj as Country);
public bool Equals(Country other)
{
if (ReferenceEquals(null, other))
return false;
if (ReferenceEquals(this, other))
return true;
return string.Equals(
_code,
other._code,
StringComparison.OrdinalIgnoreCase);
}
public override int GetHashCode()
=> StringComparer.OrdinalIgnoreCase.GetHashCode(_code);
public static bool operator ==(Country left, Country right)
=> Equals(left, right);
public static bool operator !=(Country left, Country right)
=> !Equals(left, right);
public override string ToString()
=> _code;
}
The simple Func key-selector
The final solution we will look at in this post is a simple solution where the consumer gets a key-selector in the form of: Func<Countries, string>
.
scoreKeeper.Register(c => c.Sweden, 12);
public class ScoreKeeper
{
public void Register(Func<Countries, string> selector, int score)
{
var country = selector(Countries.Instances);
//Do something with country and score
}
}
public class Countries
{
public readonly string Sweden = "SWE";
public readonly string Norway = "NO";
public readonly string Denmark = "DK";
public static readonly Countries Instances = new Countries();
private Countries() { }
}
It sure helps with discovery, but you can also return any string you want. This could be prevented by e.g. using a combination of the Country value-object and the Func, allowing you to pick a Country from a list instead of a string.
//Daniel