How to improve this simple template rendering class?
I just wrote a simple class. All it does is takes a string of input like
Hello @name, you look @adjective today!
And replaces the @variables
with their values from a dictionary. For example, passing in new Dictionary<string,object>{{"name","Ralph"},{"adjective","stunning"}}
would give:
Hello Ralph, you look stunning today!
Here's the class:
class Template
{
List<object> nodes = new List<object>();
public Template(string content)
{
for (int i = 0; i < content.Length; ++i)
{
char ch = content[i];
if (ch == '@')
{
var match = Regex.Match(content.Substring(i + 1), @"\w+");
if (match.Success)
{
nodes.Add(new Variable(match.Value));
i += match.Value.Length;
}
else
{
throw new Exception(string.Format("Expected variable name after @ symbol at character {0}", i));
}
}
else
{
nodes.Add(ch.ToString());
}
}
}
public string Render(Dictionary<string,object> dict)
{
var sb = new StringBuilder();
foreach (var item in nodes)
{
if (item is Variable)
{
sb.Append(dict[((Variable)开发者_Go百科item).Name]);
}
else
{
sb.Append(item);
}
}
return sb.ToString();
}
class Variable
{
public readonly string Name;
public Variable(string name)
{
Name = name;
}
}
}
Is this a good approach to such a problem? I want to do as much processing as possible in the constructor so that I can re-render the template over and over efficiently without reparsing it.
Right now I'm looping over the whole node list looking for variable nodes so that I can replace them. Maybe there's a way I can "skip" to those nodes? Would that help?
Also, I'm parsing it character by character, but then I use a regex (which I want to start at the next character, so i use .Substring to get the remainder of the string) to get a "chunk" of text. I'm not sure how else I might get the whole variable name w/out using a regex?
This class is about to get a lot more complicated, so I want to make sure I have the right approach before I go any further.
My 2 concerns are:
- What's a nice way to pull out a chunk of text from the current position, and advance the counter? For example, extracting the variable name once I've determined that the next bit of text should in fact be a variable name.
- How should I store the nodes (in this case, just Text and Variable nodes) such that I can quickly render the template over and over later?
What about:
s = s.Replace("@name", name);
s = s.Replace("@Adjective", adjective);
Also, I would avoid throwing exceptions when parsing text. Since user strings can contain virtually anything, it's better to just try and handle unexpected data as intelligently as you can.
You'll almost certainly want to use some sort of associative container for your nodes, might I recommend a Dictionary<string, Node>
instead of the List
? Also, I see no way to currently set those nodes. And you may as well go ahead and make nodes a class as well while you're at it.
As far as parsing char by char and then using a regex, yeah that's probably not ideal but writing a tokenizer/lexer isn't the simplest of tasks either. Fix it when you need to!
If you are going to use the template multiple times I would suggest parsing the template in 2 steps, first one splits the template in literal text and field objects and store these in a list, then for each time you need to render it just loop over the list and output any literal text directly and call through the dictionary for each field.
We did something similar with our own temlpate engine going from regex replace to a pre parsed list and increased performance about 20 times :D.
Your current code would be perfect to split the original string.
Example:
List<Node> nodes = var list = new List<node> {new node {Type=ntypes.literal,Value="Hello "}, new node{Type=ntypes.field,Value="Name"}};
Enum NodeType {
field,
literal
}
class Node {
public enum NodeType;
public string Value;
}
If you will use it only occationally you could still split it but then save the array in for eaxmple JSON or serialize it so that you could very easily recreate the list without having to use regex.
Also if the array contains elements with (type, value) you could add more intelligense to it over time, multiple different dictionaries or maybe simple functions like @@substring(@field, 10) that is then parsed and executed ;)
But for more complex situations, going for a real parser solution is better as with increasing complexity all simple solutions will get ungainly and sacrifice performance.
We finaly turned to an Antlr based parser and are currently looking at expression trees for the rendering phase.
Instead of looping through EVERY character of your string you should just execute your RegEx against the whole string and return a collection of Matches.
精彩评论