Open-Closed Principle – IFormattable, IFormatProvider, ICustomFormatter

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.

This entry was posted in C#. Bookmark the permalink. Follow any comments here with the RSS feed for this post.

16 Responses to Open-Closed Principle – IFormattable, IFormatProvider, ICustomFormatter

  1. Mike Stortz says:

    I’m working with social service software, and as a matter of messy reality, it is possible to have people with duplicate SSNs. Not common, fortunately, but possible.

    Also, some people do not recall their entire SSN, and have no documentation handy, so any class that I could use would have to support blank digits (“X”?). This would, of course, result in more possible duplicates.

    That said, excellent article, and I will be sure to implement this pattern where applicable in my classes. Slick!

  2. dhayden says:

    Thanks, Johan.

  3. Johan Eijlders says:

    Hi David,

    Thanks for taking the time to explain some of the fun things of design and development. I’m a frequent visitor of you’re blog and i think the subjects you discuss are very interesting.
    Keep up the good work.

    Johan

  4. dhayden says:

    You’re asking if the Agile Software Development book gives credit to Bertrand Meyer on the OCP? Yes, it references his book:

    Object Oriented Software Construction, Bertrand Meyer, Prentice Hall, 1988, p 23

    Regards,

    Dave

  5. Daniel Moth says:

    The “Open Closed principle” was, afaik, first defined by Bertrand Meyer (creator of Eiffel) in his most excellent book. I am just genuinely curious if the reference you cited gives him credit for that?

  6. dhayden says:

    Thanks, stephen!

  7. dhayden says:

    Johan,

    Yes, because the strategy is not about the format. It is about the concrete IFormatProvider strategy determined at runtime and plugged into the string.Format method.

    As an example, use a Factory Method to return the concrete IFormatProvider strategy and then plug it in:

    IFormatProvider ssnFormatProvider = SSNFormatProvider.Create(CurrentUser.Roles);

    Console.WriteLine(string.Format(ssnFormatProvider, “0:doesntmatter”, ssn));

    It doesn’t matter what you plug in for the format and the formatting doesn’t really need to vary based on the concrete strategy, it is really about the runtime determination of what concrete strategy of IFormatProvider will be implemented.

    Regards,

    Dave

  8. stephen says:

    Thanks for explaining this. It was said earlier, but I’ll say it again, this is finally making sense. The MSDN documentation explains it, but doesn’t really give an understandable instance of why one would go to the trouble of implementing a formatting class.

  9. Johan Eijlders says:

    Hi David,

    Thanks for the reaction.
    So you’re saying. In order to be a strategy more than one algorithm is needed. I understand that.
    Do you think it still a strategy if one would supply the format method of CustomerServiceSSNFormatProvider with “{0:notsosecure}” and only change the implementation of the CustomerServiceSSNFormatProvider to:

    public string Format(string format, object arg, IFormatProvider formatProvider)
    {
    switch(format)
    {
    case “notsosecure”:
    default: return “***-” + arg.ToString().Substring(4);
    }
    }

    Johan

  10. dhayden says:

    Johan,

    The more I think about it, you could consider MySSNFormatProvider a concrete strategy if looking from the perspective of the .NET Framework.

    This is similar to injecting your own custom IComparer to sort objects in an ArrayList like:

    ArrayList items = new ArrayList();
    items.Add(“One”);
    items.Add(“Two”);
    items.Add(“Three”);

    IComparer myComparer = new CoolComparer();
    items.Sort(myComparer);

    In this case, myComparer will be used to compare objects in the ArrayList as opposed to the default IComparable implementation of each object.

    Great question.

  11. dhayden says:

    Thanks, Johan.

    I, personally, wouldn’t call it the strategy pattern as written.

    I think of the strategy pattern more in terms of using differing / pluggable algorithms based on configuration or context, like having an IPricingStrategy and a family of classes that implement the Interface.

    One, class, called LowestPriceStrategy, for example, implements the Interface and calculates the pricing so that the customer gets the lowest price.

    A second class, called ValuePricingStrategy, would implement the interface that gives the customer most bang for his/her buck.

    A third class, called HighestPriceStrategy, would implement the interface so that the retailer makes the most profit.

    Depending on the configuration or context, one of these pricing strategies would be in play.

    In the example with SSN, I am really only implementing an interface that allows for custom formatting, not changing the algorthm of the formatting based on context.

    However, you could evolve this into a strategy pattern if you were to have a family of classes that implement the “secure” format and are plugged into the WriteLine statement based on context.

    Maybe we have different format providers per application role.

    AdminSSNFormatProvider implements the IFormatProvider and ICustomFormatter interfaces and displays the “secure” format as “123-45-6789″. We have a CustomerServiceSSNFormatProvider that implements the same interfaces and displays the secure format as “***-**-6789″. And, CustomerSSNFormatProvider displays the secure format as “123-45-6789″ as well.

    Therefore you could do something like:

    Console.WriteLine(string.Format(ssnFormatProvider, “{0:secure}”, ssn));

    where ssnFormatProvider is declared as IFormatProvider and could be anyone of the concrete classes mentioned above based on the role of the person logged in. In this case, we are differing the algorithm for displaying a “secure” SSN based on context.

    This is purely my humble opinion and others may think of it differently.

    Regards,

    Dave

  12. Johan Eijlders says:

    Nice example David,

    Is it correct to say that MySSNFormatProvider is a concrete Strategy?

  13. dhayden says:

    Thanks, Jeffrey.

  14. David,
    Great example of extension.

  15. dhayden says:

    Thanks, Robert.

    I appreciate you taking the time to say so.

  16. Robert says:

    David,

    Really good post. For the first time, all of this makes complete sense.

    Robert

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>