DotNetSlackers: ASP.NET News for lazy Developers

Saturday, January 8, 2011

Domain Specific Language using C# 4.0 - Part 1


Hope you know what DSL (domain specific language) is. It is a programming language aimed for a particular domain with the users in that domain in mind. DSL is different from general purpose programming languages (C#, Java, Ruby, Python) in that DSL is limited to a particular problem domain whereas general purpose languages are made for addressing any kind of problems in the world. For example, your NANT build scripts and Unix shell scripts are some of the example DSLs. Yes, DSL is not new. Note that the area for DSL can be financial, maths, science, health-care, space control, etc.
A DSL can be created either on top of existing programming languages like C#, Ruby, etc., or created from scratch by using parser-generator tools like ANTLR, Irony. Martin Fowler coined the name "Internal DSL" for the first one and for the second one he called as "External DSL". Internal DSLs are designed by using the syntax and the level extensibility provided in the hosting language. For internal DSL, Ruby is the first class citizen for this due to its by birth nature and other features like metaprogramming. By the way, C# (my primary language) also has the capability to build a DSL.
In this article, I create a DSL for examination question paper in C#. This DSL enables to create an examination with multi-choice questions. Also, it provides API to validate the answers. Let us start with plain C# classes without any DSL flavor. During the journey, I'll top-up the DSL flavors until we reach a point where you should feel it is the language for this particular problem domain.

Level 0: Vanilla Exam Model

The below diagram depicts the domain model for this domain.
00

Exam Class

 Collapse

public class Exam 
 { 
public string Title { get; private set; } 
public Level Level { get; private set; } 
public List<Question> Questions { get; private set; } 
 
public Exam(string title, Level level) 
{ 
Title = title; 
Level = level; 
Question.QNo = 1; 
Questions = new List<Question>(); 
} 
 
public void AddQuestion(Question question) 
{ 
if (!Questions.Contains(question)) 
Questions.Add(question); 
} 

public void VerifyAnswers(ref Dictionary<int, AnswerSheet> answers) 
{ 
foreach (KeyValuePair<int, AnswerSheet> answer in answers) 
{ 
Question question = Questions.Find(q => q.Number == answer.Key); 
if (question.Answer.SequenceEqual(answer.Value.Answer)) 
answer.Value.IsCorrect = true; 
else 
{ 
answer.Value.IsCorrect = false; 
answer.Value.CorrectAnswer = question.Answer; 
}    
} 
} 
} 

Question Class

 Collapse

public class Question : IEquatable<Question>
{        
internal static ushort QNo { get; set; }

public ushort Number { get; private set; }
public string Description { get; private set; }
public Dictionary<string, string> Options { get; private set; }
public List<string> Answer { get; private set; }

public Question(string description)
{
Number = QNo++;
Description = description;
Options = new Dictionary<string, string>(4);            
}

public void AddOption(string choice, string descripton)
{            
if (!Options.ContainsKey(choice))
Options.Add(choice, descripton);
}

public void AddAnswers(string[] choices)
{
Answer = choices.Distinct().ToList();
}

public override bool Equals(object obj)
{            
if (ReferenceEquals(this, obj)) return true;
if (obj == null || !(obj is Question)) return false;
Question other = obj as Question;

return Number == other.Number &&
Description.Equals(other.Description) &&
Options.Equals(other.Options) &&
Answer.Equals(other.Answer);
}

public override int GetHashCode()
{
return Number.GetHashCode() ^ 786 ^
Description.GetHashCode() ^ Options.GetHashCode() ^
Answer.GetHashCode();
}

#region IEquatable<Question> Members

public bool Equals(Question other)
{
if (ReferenceEquals(this, other)) return true;
if (other == null) return false;

return Number == other.Number &&
Description.Equals(other.Description) &&
Options.Equals(other.Options) &&
Answer.Equals(other.Answer);
}

#endregion
}

The source code for AnswerSheet and Level are not required, since the class diagram represents them well. If you think that the above code is fine to create a exam paper by a question paper preparer, you will definitely reconsider after you see the below code.
 Collapse

var dotnetExam = new Exam(".NET Fundamentals", Level.Beginner);
Question question = new Question("Expansion of CLR");
question.AddOption("A", "Common Language Runtime");
question.AddOption("B", "Common LINQ Runtime");
question.AddOption("C", "C# Language Runtime");
question.AddOption("D", "C Language Runtime");
question.AddAnswers(new string[] { "A" });
dotnetExam.AddQuestion(question);

The above lines are very bulky, object name and methods are messed up with C# syntax for just adding one question. Practically, a question paper contains at least 25 questions. Just consider the question paper preparer who is going to use this API. He needs to play lot around with (, ), Shift key, {, } characters without semantically understanding these.

No comments:

Post a Comment