Jeffrey provided an example of a SocialSecurityNumber Class in his recent post, called EVERY application has a natural domain model - level 200.
The class is really a template that leaves things rather wide open, but it served his discussion nicely and didn't need to be built-up futher. Here is the class "template":
public class SocialSecurityNumber
{
private string _rawNumber;
public SocialSecurityNumber(string number)
{
_rawNumber = number;
}
public string GetWithoutDashes()
{
return; //the ssn without dashes.
}
public override string ToString()
{
//format ssn nicely and output.
return; //the nice output.
}
public override bool Equals(object obj)
{
return true; //or false if they don't match.
//Use some intelligence to compare for sameness.
}
}
As I looked at it, my thoughts were that if you stuck 10 developers in a room to finish the template, each of them would probably come up with a different solution. Some would probably suggest it be a struct and others would modify or change the class completely. This sounds like a great plot for a Code Room Episode. Assuming we keep it as a reference object ( class ), I would probably make the following changes:
Implement IFormattable
One particular method, GetWithoutDashes, which returns the Social Security Number without dashes, suggests that clients of this class may want to format the SSN in multiple ways:
public string GetWithoutDashes()
{
return; //the ssn without dashes.
}
You can see a problem we might have in the future. We really don't want to keep adding new methods on this class as people need new formatting options.
This would mainly be a pain in the butt, but it also "violates" the Open-Closed Principle:
"Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification [Martin, p.99]" - Agile Software Development Principles, Patterns, and Practices
Classes that adhere to the Open-Closed Principle have 2 primary attributes:
- "Open For Extension" - It is possible to extend the behavior of the class as the requirements of the application change (i.e. change the behavior of the class).
- "Closed For Modification" - Extending the behavior of the class does not result in the changing of the source code or binary code of the class itself.
IFormattable
The .NET Framework offers an IFormattable Interface that closes the class for modification ( at least with formatting ) but opens it up for extension by allowing clients to inject their own ICustomFormatter to format the SSN as they wish:
public class SocialSecurityNumber : IFormattable
We can implement the interface like this:
#region IFormattable Members
public string ToString(string format, IFormatProvider formatProvider)
{
if (formatProvider != null)
{
ICustomFormatter formatter = formatProvider.GetFormat(
this.GetType())
as ICustomFormatter;
if (formatter != null)
return formatter.Format(format, this, formatProvider);
}
if (format == null) format = "G";
switch (format)
{
case "nd": return _rawNumber.Replace("-", "");
case "G":
default: return _rawNumber;
}
}
#endregion
Now we can specify we want the SSN without dashes simply as:
Console.WriteLine(string.Format("No Dashes: {0:nd}", ssn));
where "nd" is the new format for SSN with No Dashes. Weak I know :)
IFormatProvider and ICustomFormatter
However, the beauty is that now as a client I can specify my own classes to format the SSN if the need arises in the future. Here is a simple example:
public class MySSNFormatProvider : IFormatProvider, ICustomFormatter
{
#region IFormatProvider Members
public object GetFormat(Type formatType)
{
return this;
}
#endregion
#region ICustomFormatter Members
public string Format(string format, object arg,
IFormatProvider formatProvider)
{
switch(format)
{
case "secure":
default: return string.Format("***-**-{0}",
arg.ToString().Substring(7));
}
}
#endregion
}
We can now spit out a "secure" SSN via:
Console.WriteLine(string.Format(new MySSNFormatProvider(),
"{0:secure}", ssn));
If _rawNumber was "123-45-6789", the above would output "***-**-6789", which gets across the idea.
Overriding Equals and GetHashCode
We can't override Equals without overriding GetHashCode as the compiler will bark at us. Here is my simple attempt:
public override bool Equals(object obj)
{
if (obj == null) return false;
SocialSecurityNumber ssn = obj as SocialSecurityNumber;
if (ssn == null) return false;
return (this._rawNumber.Equals(ssn._rawNumber));
}
public override int GetHashCode()
{
return _rawNumber.GetHashCode();
}
We can get away with this, because every U.S. Citizen should have a unique SSN and Jeffrey has made the class immutable, which means the class is safe in a hashtable.
Overriding ToString
Let's just delegate this to the new IFormattable implementation by specifying the default format of "G":
public override string ToString()
{
return ToString("G", null);
}
Final Class
Here is draft of the class along with my MySSNFormatProvider Class. Yeah, this screams TDD / unit testing, but I was lazy :)
using System;
using System.Diagnostics;
public class MyClass
{
public static void Main()
{
SocialSecurityNumber ssn =
new SocialSecurityNumber("123-45-6789");
Console.WriteLine(ssn);
Console.WriteLine(string.Format("No Dashes: {0:nd}", ssn));
Console.WriteLine(string.Format
(new MySSNFormatProvider(), "{0:secure}", ssn));
Console.ReadLine();
}
public class SocialSecurityNumber : IFormattable
{
private string _rawNumber;
public SocialSecurityNumber(string number)
{
_rawNumber = number;
}
public override string ToString()
{
return ToString("G", null);
}
public override bool Equals(object obj)
{
if (obj == null) return false;
SocialSecurityNumber ssn = obj as SocialSecurityNumber;
if (ssn == null) return false;
return (this._rawNumber.Equals(ssn._rawNumber));
}
public override int GetHashCode()
{
return _rawNumber.GetHashCode();
}
#region IFormattable Members
public string ToString(string format,
IFormatProvider formatProvider)
{
if (formatProvider != null)
{
ICustomFormatter formatter =
formatProvider.GetFormat(
this.GetType())
as ICustomFormatter;
if (formatter != null)
return formatter.Format(format, this, formatProvider);
}
if (format == null) format = "G";
switch (format)
{
case "nd": return _rawNumber.Replace("-", "");
case "G":
default: return _rawNumber;
}
}
#endregion
}
public class MySSNFormatProvider : IFormatProvider, ICustomFormatter
{
#region IFormatProvider Members
public object GetFormat(Type formatType)
{
return this;
}
#endregion
#region ICustomFormatter Members
public string Format(string format, object arg,
IFormatProvider formatProvider)
{
switch(format)
{
case "secure":
default: return "***-**-" + arg.ToString().Substring(7);
}
}
#endregion
}
}
I beat that subject to death, but am wondering what others would do differently or add to the example? It's a rather neat "intellectual" exercise, albeit a bit bloated if it all isn't necessary. There are no doubt some things missing or perhaps done incorrectly.
Posted
Sun, Mar 12 2006 2:01 PM
by
David Hayden