HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Composite and Visitor patterns for tree-based survey functionality in C#

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
surveypatternsvisitorforcompositebasedandfunctionalitytree

Problem

I have written some survey functionality for a project. Basically, a generic survey form that can be composed of sections and questions.

I have a Survey class, Questions and Sections. The Survey is basically a tree, where each node can be a Question or a Section. Nodes have children -- so essentially Question can have a collection of subsections and subquestions, and a Section can have a collection of subsections and subquestions.

The nodes in my Survey have derive from the abstract class SurveyPart.

namespace Surveys
{
    public abstract class SurveyPart
    {
        public abstract List Children { get; set; }
    }

    public class Survey
    {
        public List Children { get; set; }

        public Survey()
        {
            Children = new List();
        }
    }

    public class Question : SurveyPart
    {
        public override List Children { get; set; }
        public string QuestionText { get; set; }

        public Question()
        {
            Children = new List();
        }
    }

    public class Section : SurveyPart
    {
        public override List Children { get; set; }
        public string Header { get; set; }

        public Section()
        {
            Children = new List();
        }
    }
}


As far as I understand this is the Composite pattern? Not sure I've got it entirely right.

So with that I can build a survey (at present with the sections and questions coming from a DB.) Next thing is to render it. For that I'm attempting to use the Visitor pattern implemented with extension methods.

```
namespace ExtensionMethods
{
using Surveys;

public static class SurveyTextRenderer
{
public static int Depth;

public static void Write(this Survey survey)
{
Depth = 0;

Console.WriteLine("Survey");
Console.WriteLine(new string('-', "Survey".Length));

foreach (SurveyPart child in survey.Children)
{

Solution

That design will work well for multiple renderers - when you stated in the comment that you would it made much more sense. You should remove the static Depth variable though - it makes your code not thread-safe. Also, you can reduce the number of overloads. Here's a way to refactor your renderer:

public class SurveyTextRenderer
{
   public Write(Survey survey)
   {
      Console.WriteLine(survey.Name);
      Console.WriteLine(new string('-', survey.Name.Length);

      for (SurveyPart part in survey.Children)
      {
         processNode(part, 0);
      }
   }

   protected void ProcessNode(SurveyPart part, int depth)
   {
      if (part is Section)
         WriteSection(part as Section, depth);
      else if (part is Question)
         WriteQuestion(part as Question, depth);
      else
         // Error handling or default case

      for (SurveyPart part in survey.Children)
      {
         ProcessNode(part, depth + 1);
      }
   }
}


Note the error handling - what happens if you add a new SurveyPart and don't update the renderer? Also, I changed the name to use a Survey name, which you should see how to implement easily.

I did not implement WriteSection and WriteQuestion; they'll be pretty close to what you have already except that the recursion is removed. I don't think you really need statics in this case, but you can make them static if you want. However, you could make, say,

public abstract class Renderer
{
   public abstract void Write(Survey survey);
}


and extend that. It may or may not be useful to you. It depends on your calling code whether it would be worth adding that abstraction. If you have a method that is called like PrintSurvey(new TextRenderer(), datasource) where datasource is one of multiple places the survey could be stored (XML, database, file, etc.) it might be useful. You don't want to repeat yourself. In fact, you could have Survey extend SurveyPart (maybe rename it SurveyElement?) and remove the sort-of-redundant Write()->ProcessNode() calls.

Hopefully at least this gives you a few ideas!

Code Snippets

public class SurveyTextRenderer
{
   public Write(Survey survey)
   {
      Console.WriteLine(survey.Name);
      Console.WriteLine(new string('-', survey.Name.Length);

      for (SurveyPart part in survey.Children)
      {
         processNode(part, 0);
      }
   }

   protected void ProcessNode(SurveyPart part, int depth)
   {
      if (part is Section)
         WriteSection(part as Section, depth);
      else if (part is Question)
         WriteQuestion(part as Question, depth);
      else
         // Error handling or default case

      for (SurveyPart part in survey.Children)
      {
         ProcessNode(part, depth + 1);
      }
   }
}
public abstract class Renderer
{
   public abstract void Write(Survey survey);
}

Context

StackExchange Code Review Q#1729, answer score: 5

Revisions (0)

No revisions yet.