To make things clearer, some background is required that explains a particular scenario in which extending a class using delegates and/or interfaces can be extremely helpful.
To begin with, I'm working with a class that will be used for inferring the programming language used when provided with an out-of-context code snippet. The general theory is to provide each possible language's definition in an XML file along with what I'm calling, "KeywordSet" or sets of particular language keywords that can be used to identify the language. Additionally, the XML file contains associated sytle information relative to each KeywordSet. In this case, the style information can be used for syntax highlighting if that's what the intention is. I've borrowed from the XML schema which Scintilla uses for their syntax highlighting:
<?xml version="1.0" encoding="utf-8" ?>
<language name="C/C++" filemasks=".c,.cpp,.cc,.cxx,.h,.hpp,.hh,.hxx,.inl">
<style name="C++ Default"
keywordset_index="0"
fg="0,0,0"
bg="255,255,255"
bold="false"
italics="false"
underlined="false"/>
<style name="C++ Comment (normal)"
keywordset_index="1"
fg="160,160,160"/>
<keywords>
<set index="0" description="C++ Primary Keywords">
<keyword index="0" >if</keyword>
<keyword index="1">int</keyword>
<keyword index="2">long</keyword>
<keyword index="3">try</keyword>
<keyword index="4">catch</keyword>
<keyword index="5">while</keyword>
<keyword index="6">auto</keyword>
</set>
<set index="1" description="C++ Not Used"/>
<set index="2" description="C++ Secondary Keywords">
<keyword index="0">params</keyword>
<keyword index="1">return</keyword>
<keyword index="2">*</keyword>
<keyword index="3">+</keyword>
<keyword index="4">-</keyword>
<keyword index="5">/</keyword>
<keyword index="6">=</keyword>
<keyword index="7"><</keyword>
</set>
</keywords>
</language>
Having defined all my languages in seperate XML files, I create a method that will iterate through the directory containing those definitions and create my domain object model. This will make working with the language definitions much easier:
var definitions = new List<Language>();
foreach (var file in Directory.GetFiles(languageDefinitionPath, "*.xml"))
{
if (File.Exists(file))
{
#region LINQ parsing XML to Languate objets
XDocument xDoc = XDocument.Load(new FileInfo(file).FullName);
definitions.AddRange((from d in xDoc.Descendants("language")
let name = d.Attribute("name")
where name != null
let filemasks = d.Attribute("filemasks")
where filemasks != null
select new Language
{
Name = name.Value,
FileMasks = filemasks.Value.Split(','),
KeywordSets = (from k in d.Elements("keywords").Elements("set")
let index = k.Attribute("index")
where index != null
let description = k.Attribute("description")
where description != null
select new KeywordSet
{
Index = Int32.Parse(index.Value),
SetDescription = description.Value,
Keywords =
(from kw in
k.Elements("keyword")
let kwIndex =
kw.Attribute("index")
where kwIndex != null
select new Keyword
{
Index =
Int32.Parse(
kwIndex.
Value),
Value = kw.Value
}).ToList()
}).ToList(),
Styles = (from s in d.Elements("style")
let underlined = s.Attribute("underlined")
let italics = s.Attribute("italics")
let bold = s.Attribute("bold")
let attrBackground = s.Attribute("bg")
let attrForeground = s.Attribute("fg")
let attrName = s.Attribute("name")
where attrName != null
let attrIndex = s.Attribute("keywordset_index")
select new Style
{
Name = attrName.Value,
KeywordSetIndex =
Int32.Parse(attrIndex.Value),
ForegroundColor = Color.FromArgb(
Int32.Parse(
((string) attrForeground).
Split(',')[0]),
Int32.Parse(
((string) attrForeground).
Split(',')[1]),
Int32.Parse(
((string) attrForeground).
Split(',')[2])),
BackgroundColor = Color.FromArgb(
Int32.Parse(
((string) attrBackground ??
"0,0,0").
Split(',')[0]),
Int32.Parse(
((string) attrBackground ??
"0,0,0").
Split(',')[1]),
Int32.Parse(
((string) attrBackground ??
"0,0,0").
Split(',')[2])),
Bold =
bold != null && Boolean.Parse(
bold.Value),
Italic =
italics != null && Boolean.Parse(
italics.Value),
Underlined =
underlined != null && Boolean.Parse(
underlined.Value)
}).ToList()
}));
Now that I have an object model, I can create methods to use that object model to make determinations as to what language a particular code snippet is:
public Language InferLanguageByText(string text)
{
if(String.IsNullOrEmpty(text))
throw new ArgumentNullException("text");
IEnumerable<string> tokens = text.Split(' ');
if(!tokens.Any())
throw new Exception("Not enough tokens in text to make a language inference");
var inferredLanguage = new Language();
int matchCount = 0;
foreach (Language languageDefinition in LanguageDefinitions)
{
int intermediateMatchCount = 0;
if(!languageDefinition.KeywordSets.Any())
throw new Exception("No keyword sets to make language inference");
foreach (KeywordSet keywordSet in languageDefinition.KeywordSets)
{
var result = (from t in keywordSet.Keywords
where t.Value != null
select t.Value).ToList();
var thisLanguageMatchCount = result.Intersect(tokens).Count();
intermediateMatchCount += thisLanguageMatchCount;
}
if (intermediateMatchCount >= matchCount)
{
matchCount = intermediateMatchCount;
Language definition = languageDefinition;
inferredLanguage = LanguageDefinitions.FirstOrDefault(d => d.Name == definition.Name);
}
}
return inferredLanguage;
}
Now to the point. Obviously there are many possible ways to use the Language object model for making a determination. The above method is just one of those ways. I can't however possibly code for every possible option. This is where the open/closed principle of software development comes into play. The open/closed principle states that components should be closed for modification and open for extension. The meaning is that if in the future a better way is found to use my object model for determining a code snippet's language, I shouldn't have to open up the component and code the solution directly (technically that rule is broken here when using a class that implements IRule because you're modifying the original assembly, but I digress.)
The answer? Interfaces and/or method delegates.
Suppose I create a method that instead of running the logic inside, it accepts a delegate and will run a provided method against the text in question. Here is one such method:
public Language InferLanguageUsingRule(string text, Func<string, Language> rule = null)
{
if(String.IsNullOrEmpty(text))
throw new ArgumentNullException("text");
var inferredLanguage = new Language(); // Null object pattern
if (rule == null)
{
// Iterate through all "IRule" implementing types and execute IRule method.
// If a non-null object is returned, stop processing rules
Assembly assembly = Assembly.GetExecutingAssembly();
foreach (Type type in assembly.GetTypes())
{
Type t = type.GetInterface("IRule", true);
if (t != null)
{
var ruleObject = (IRule) Activator.CreateInstance(type);
inferredLanguage = ruleObject.ExecuteRule(text);
// Found a language, stop processing here
if (!String.IsNullOrEmpty(inferredLanguage.Name))
break;
}
}
}
else
{
inferredLanguage = rule(text);
}
return inferredLanguage;
}
This code can be used in two ways. The first of which is that you can call it with an explicit Func<string, Language> delegate ro run. This says it's expecting a method delegate that accepts a string argument and returns a Language object. This is a decent solution to allow future extension. In addition, classes can be defined which use the IRule (no pun intended) interface. With this known interface, my method can search all the types exposed in the assembly for types which implement IRule. The interface exposes one method, Language ExecuteRule(string text). Once a type has been found, it will execute the method and look to see if a Language was found. If not, it keeps looking. If so, it quits at that point. So, if later I decide that I want more ways to determine the language type, I can simply create a new class that implements IRule, put the .cs class file into the Rules folder (or anywhere in the assembly, but putting them in a specific folder makes things neater) and it will be run automatically for me when calling the InferLanguageUsingRule method.
Here is an explicit method, provided in an external .cs file that will be executed:
public class ExactFileExtensionNameMatch : IRule
{
#region IRule Members
public Language ExecuteRule(string text)
{
// Actual logic not implemented (as you can tell). This merely shows
var returnLanguage = new Language();
if (text == "viola")
returnLanguage = DefaultInferer.InfererContext.LanguageDefinitions.FirstOrDefault();
return returnLanguage;
}
#endregion
}
So, that was just a quick look at one way to provide extended functionality by using an interface which exposes a method that can be dynamically called at runtime, or consuming the class elsewhere and providing your own custom algorithm wrapped up in a method delegate. Yay!