【问题标题】:.NET class design question.NET 类设计问题
【发布时间】:2009-10-30 15:06:58
【问题描述】:

我有一个名为 Question 的类,它有一个名为 Type 的属性。基于这种类型,我想以特定的方式将问题呈现为 html(多选 = 单选按钮、多选 = 复选框等......)。我从单个 RenderHtml 方法开始,该方法根据问题类型调用子方法,但我认为将呈现逻辑分离到实现接口的各个类中可能会更好。但是,由于这个类是使用 NHibernate 持久化到数据库中的,并且接口实现依赖于一个属性,所以我不确定如何最好地布局这个类。

有问题的班级:

public class Question
{
    public Guid ID { get; set; }
    public int Number { get; set; }
    public QuestionType Type { get; set; }
    public string Content { get; set; }
    public Section Section { get; set; }
    public IList<Answer> Answers { get; set; }
}

基于 QuestionType 枚举属性,我想呈现以下内容(只是一个示例):

<div>[Content]</div>
<div>
   <input type="[Depends on QuestionType property]" /> [Answer Value]
   <input type="[Depends on QuestionType property]" /> [Answer Value]
   <input type="[Depends on QuestionType property]" /> [Answer Value]
   ...
</div>

目前,我在一个名为 RenderHtml() 的函数中有一个大的 switch 语句,它完成了肮脏的工作,但我想把它移到更干净的地方。我只是不确定如何。

有什么想法吗?

编辑:感谢大家的回答!

我最终使用以下界面使用了策略模式:

public interface IQuestionRenderer
{
    string RenderHtml(Question question);
}

以及以下实现:

public class MultipleChoiceQuestionRenderer : IQuestionRenderer
{
    #region IQuestionRenderer Members

    public string RenderHtml(Question question)
    {
        var wrapper = new HtmlGenericControl("div");
        wrapper.ID = question.ID.ToString();
        wrapper.Attributes.Add("class", "question-wrapper");

        var content = new HtmlGenericControl("div");
        content.Attributes.Add("class", "question-content");
        content.InnerHtml = question.Content;
        wrapper.Controls.Add(content);

        var answers = new HtmlGenericControl("div");
        answers.Attributes.Add("class", "question-answers");
        wrapper.Controls.Add(answers);

        foreach (var answer in question.Answers)
        {
            var answerLabel = new HtmlGenericControl("label");
            answerLabel.Attributes.Add("for", answer.ID.ToString());
            answers.Controls.Add(answerLabel);

            var answerTag = new HtmlInputRadioButton();
            answerTag.ID = answer.ID.ToString();
            answerTag.Name = question.ID.ToString();
            answer.Value = answer.ID.ToString();
            answerLabel.Controls.Add(answerTag);

            var answerValue = new HtmlGenericControl();
            answerValue.InnerHtml = answer.Value + "<br/>";
            answerLabel.Controls.Add(answerValue);
        }

        var stringWriter = new StringWriter();
        var htmlWriter = new HtmlTextWriter(stringWriter);
        wrapper.RenderControl(htmlWriter);
        return stringWriter.ToString();
    }

    #endregion
}

修改后的 Question 类使用内部字典,如下所示:

public class Question
{
    private Dictionary<QuestionType, IQuestionRenderer> _renderers = new Dictionary<QuestionType, IQuestionRenderer>
    {
        { QuestionType.MultipleChoice, new MultipleChoiceQuestionRenderer() }
    };

    public Guid ID { get; set; }
    public int Number { get; set; }
    public QuestionType Type { get; set; }
    public string Content { get; set; }
    public Section Section { get; set; }
    public IList<Answer> Answers { get; set; }

    public string RenderHtml()
    {
        var renderer = _renderers[Type];
        return renderer.RenderHtml(this);
    }
}

对我来说看起来很干净。 :)

【问题讨论】:

  • Switch 语句太脏了。 95% 的时间都有更好的方法。
  • 这就是我寻求更好方法的原因。 :)

标签: c# .net


【解决方案1】:

一般来说,每当您看到 Type 或 Enum 上的开关时,这意味着您可以将对象替换为“Type” - 换一种说法,polymorphism 的情况。

这实际上意味着您将为每种问题类型创建一个不同的类并覆盖RenderHTML() 函数。每个 Question 对象将负责知道它应该输出什么输入类型。

这样做的好处是您可以删除 switch 语句并生成良好的基于​​ OO 的代码。缺点是您为每个问题类型添加一个类(在这种情况下影响最小。)

【讨论】:

  • +1 表示 switch 语句的多态性。还提倡单一职责。
  • @Dave:你必须在某个地方切换......通过继承你只需将它移动到实例化。我更喜欢作文(策略模式,如下面的 Konamiman 回答)
  • Konamiman 的答案现在在上面,因为它是正确的……克里斯说。
  • @kai - 在这里更喜欢继承(或将其与组合一起使用)的唯一原因是不同的问题小部件似乎是可替代的。此外,大多数需要渲染的 html 在不同的显示类型中都是通用的。
【解决方案2】:

例如,您可以使用the strategy pattern:

  1. 让所有 HTML 渲染器实现一个通用接口,例如 IQuestionRenderer,方法名称为 Render(Question)

  2. 在您的应用程序中有一个Dictionary&lt;QuestionType, IQuestionRenderer&gt; 的实例。在初始化时填充它,可能基于配置文件。

  3. 对于给定的问题实例,执行:renderers[question.Type].Render(question)

或者,您可以拥有名为 RenderXXX 的方法,其中 XXX 是问题类型,并使用反射调用它们。

【讨论】:

  • #1 正是我在等待答案时开始的路径。听起来我可能走在正确的轨道上......
【解决方案3】:

这是一个使用对象继承来实现你想要的经典案例。任何时候你看到一个大的 switch 语句切换一个对象的类型,你应该考虑某种形式的子类化。

我看到了两种方法,具体取决于这些问题类型的“常见程度”以及渲染是否是它们之间的唯一区别:

选项 1 - 继承 Question 类

public class Question
{
    public Guid ID { get; set; }
    public int Number { get; set; }
    public string Content { get; set; }
    public Section Section { get; set; }
    public IList<Answer> Answers { get; set; }

    public virtual string RenderHtml();
}

public class MultipleChoiceQuestion 
{
    public string RenderHtml() { 
      // render a radio button
    }
}

public class MultipleAnswerQuestion 
{
    public string RenderHtml() { 
      // render a radio button
    }
}

选项 2 - 创建一个渲染界面,并将其作为问题类的属性

public class Question
{
    public Guid ID { get; set; }
    public int Number { get; set; }
    public string Content { get; set; }
    public Section Section { get; set; }
    public IList<Answer> Answers { get; set; }

    public IRenderer Renderer { get; private set; }
}

public interface IRenderer {
    void RenderHtml(Question q);
}

public class MultipleChoiceRenderer : IRenderer
{
    public string RenderHtml(Question q) { 
      // render a radio button
    }
}

public class MultipleAnswerRenderer: IRenderer
{
    public string RenderHtml(Question q) { 
      // render checkboxes
    }
}

在这种情况下,您将根据问题类型在构造函数中实例化渲染器。

如果问题类型的不同之处多于呈现方式,则选项 1 可能更可取。如果渲染是唯一的区别,请考虑选项 2。

【讨论】:

  • 我考虑过对问题本身进行子类化,但我不确定如何让 NHibernate 根据属性类型返回特定的子类型。理想情况下,我不想在从数据库中检索到对象后进行大量转换。
  • 在这种情况下,策略模式(选项 2)将是理想的。您可以将 Question Type 保留为内部或私有属性(如果仍然需要,则保留公共属性),并根据该值实例化您的 IRenderer。
【解决方案4】:

将渲染逻辑分离到自己的类中是个好主意。您不希望将呈现逻辑嵌入到应用程序的业务逻辑中。

我将创建一个名为 QuestionRenderer 的类,它接收一个问题,读取它的类型,并相应地输出渲染。如果你使用 ASP.NET,它可以输出 web 控件,或者你可以做一个输出 HTML 的服务器控件。

【讨论】:

    【解决方案5】:

    为什么不拥有一个QuestionRenderer 类(实际上,它将是一个控件),它将Question 公开为您可以设置的属性。

    在渲染方法中,你可以根据问题类型决定渲染什么。

    【讨论】:

    • 我实际上在考虑与接口类似的东西,接口的每个实现都会处理一个特定的 QuestionType
    【解决方案6】:

    我不喜欢渲染细节与数据在同一个类中的想法。

    因此,一种选择是让您的渲染方法简单地生成一组处理实际 HTML 渲染的用户控件中的一个。

    另一种方法是拥有一个单独的类 QuestionRenderer,它具有问题类型的各种子类(每个子类都会呈现正确的 HTML)。

    【讨论】:

      【解决方案7】:

      我认为您想要的是一个 IUserType,它通过一些问题工厂将属性从休眠映射转换为正确的控件类型。

      可以在此处找到使用 IuserType 的示例: NHibernate IUserType

      在示例中,它将 blob 转换为图像以供客户端使用,但同样的想法,您可以使用 QuestionType 创建页面。

      【讨论】:

        【解决方案8】:

        您可以结合使用策略模式 (Wikipedia) 和工厂。

        public class Question
        {
            public Guid ID { get; set; }
            public int Number { get; set; }
            public QuestionType Type { get; set; }
            public string Content { get; set; }
            public Section Section { get; set; }
            public IList<Answer> Answers { get; set; }
        
            private IQuestionRenderer renderer;
        
            public RenderHtml()
            {
                 if (renderer == null)
                 {
                      QuestionRendererFactory.GetRenderer(Type);
                 }
                 renderer.Render(this);
            }
        }
        
        
        interface IQuestionRenderer
        {
            public Render(Question question);
        }
        
        
        public QuestionRendererA : IQuestionRenderer
        {
            public Render(Question question)
            {
                 // Render code for question type A
            }
        }
        
        public QuestionRendererB : IQuestionRenderer
        {
            public Render(Question question)
            {
                 // Render code for question type B
            }
        }
        
        public QuestionRendererFactory
        {
            public static IQuestionRenderer GetRenderer(QuestionType type)
            {
                // Create right renderer for question type
            }
        }
        

        只有公共属性需要包含在 NHibernate 中。

        【讨论】:

        • 该死,我好像写得太慢了:-)
        【解决方案9】:

        渲染绝对是一个 UI 问题,所以我将它与 Question 类分开并添加一个工厂来隔离切换逻辑(QuestionControl 基类继承自 WebControl 并将包含大部分渲染逻辑):

        RadioButtonQuestionControl: QuestionControl {
            // Contains radio-button rendering logic
        }
        
        CheckboxListQuestionControl: QuestionControl {
            // Contains checkbox list rendering logic
        }
        
        QuestionControlFactory {
            public QuestionControl CreateQuestionControl(Question question) {
               // Switches on Question.Type to produce the correct control
            }
        }
        

        用法:

        public void Page_Load(object sender, EventArgs args) {
            List<Question> questions = this.repository.GetQuestions();
            foreach(Question question in Questions) {
                this.Controls.Add(QuestionControlFactory.CreateQuestionControl(question));
                // ... Additional wiring etc.
            }
        }
        

        【讨论】:

        • 如果你把它添加到 UserType 中,就会为你创建控件。
        • 我对 NHibernate 的探索还不够,但是直接映射到 UI 控件听起来非常有趣。
        【解决方案10】:

        显而易见:您可能可以使用工厂方法来获取所需渲染类的实例并在其上调用 render 以获得所需的输出。

        【讨论】:

          【解决方案11】:

          我将采用的方法是为您希望使用其呈现问题的每种视觉样式创建一个单独的控件(如果您在 MVC 中,则为 HtmlHelper 方法)。这将问题表示为一个对象和在视觉上整齐地表示它的关注点分开了。

          然后您可以使用主控件(或方法)根据呈现给它的Question 实例的类型选择正确的渲染方法。

          【讨论】:

            猜你喜欢
            • 2010-09-08
            • 2010-09-06
            • 1970-01-01
            • 1970-01-01
            • 2013-01-25
            • 1970-01-01
            • 2010-10-28
            • 1970-01-01
            相关资源
            最近更新 更多