2009-10-30 10 views
12

Tengo una clase llamada Pregunta que tiene una propiedad llamada Tipo. Basado en este tipo, quiero presentar la pregunta a html de una manera específica (opción múltiple = botones de opción, respuesta múltiple = casillas de verificación, etc.). Empecé con un único método RenderHtml que llamaba submétodos según el tipo de pregunta, pero estoy pensando en separar la lógica de representación en clases individuales que implementan una interfaz. Sin embargo, como esta clase se conserva en la base de datos utilizando NHibernate y la implementación de la interfaz depende de una propiedad, no estoy seguro de cómo diseñar la clase.Pregunta de diseño de clase .NET

la clase en cuestión:

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; } 
} 

Sobre la base de la propiedad QuestionType enumeración, me gustaría hacer el siguiente (es un ejemplo):

<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> 

Actualmente, tengo una gran cambiar instrucción en una función llamada RenderHtml() que hace el trabajo sucio, pero me gustaría moverlo a algo más limpio. No estoy seguro de cómo.

¿Alguna idea?

EDIT: Gracias a todos por las respuestas!

Terminé yendo con el patrón de estrategia utilizando la interfaz siguiente:

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

Y la siguiente implementación:

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 
} 

La clase de preguntas modificado utiliza un diccionario interno de este modo:

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); 
    } 
} 

Parece bastante limpio para mí. :)

+1

Las instrucciones de cambio son muy sucias. El 95 por ciento de las veces hay una mejor manera de hacerlo. –

+0

Esa sería la razón por la que estaba buscando una mejor manera. :) – Chris

Respuesta

11

Por ejemplo, puede utilizar the strategy pattern:

  1. hace todas sus procesadores HTML implementar una interfaz común, por ejemplo IQuestionRenderer, con un nombre de método Render(Question).

  2. Tenga una instancia de Dictionary<QuestionType, IQuestionRenderer> en su aplicación. Rellenar en el momento de la inicialización, tal vez en función de un archivo de configuración.

  3. para una instancia determinada de una pregunta, hacer: renderers[question.Type].Render(question)

O, usted podría tener métodos llamado RenderXXX donde XXX es el tipo de pregunta, e invocar a ellos mediante la reflexión.

+0

# 1 es exactamente el camino que he comenzado mientras espero respuestas. Parece que podría estar en el buen camino ... – Chris

+0

http://www.dofactory.com/Patterns/PatternStrategy.aspx Para su referencia – Pratik

1

¿Por qué no tener una clase QuestionRenderer (en realidad, será un control) que expone un Question como una propiedad que puede establecer.

En el método de representación, puede decidir qué representar según el tipo de pregunta.

+1

En realidad estaba pensando algo similar con las interfaces, donde cada implementación de la interfaz manejaría un tipo de pregunta específico – Chris

1

No me gusta la idea de que los detalles estén en la misma clase que los datos.

Por lo tanto, una opción sería hacer que su método de representación simplemente genere uno de los controles de usuario que maneja la representación HTML real.

Otro sería tener una clase separada QuestionRenderer que tendría las distintas subclases para tipos de preguntas (cada una de las cuales representaría el HTML correcto).

3

Es una buena idea separar la lógica de representación en su propia clase. No desea que la lógica de representación esté integrada en la lógica comercial de su aplicación.

Crearía una clase llamada QuestionRenderer que toma una Pregunta, lee su tipo y muestra la representación en consecuencia. Si está utilizando ASP.NET podría generar controles web, o podría hacer un control de servidor que emita HTML.

12

Hablando en general, siempre que vea que se enciende un Tipo o Enum, significa que puede sustituir en objetos como el "Tipo" - dicho de otra manera, un caso para polymorphism.

Lo que esto significa en la práctica es que creará una clase diferente para cada tipo de pregunta y anulará la función RenderHTML(). Cada objeto de la pregunta será responsable de saber qué tipo de entrada debe emitir.

Los beneficios son que elimina la declaración del interruptor y también produce un buen código basado en OO. Las desventajas son que agrega una clase para cada tipo de Pregunta (en este caso, impacto mínimo).

+2

+1 para polimorfismo sobre instrucción de conmutación. También promueve la responsabilidad individual. –

+1

@Dave: tienes que cambiar a alguna parte ... con herencia, simplemente muévelo a instanciación. Prefiero la composición (patrón de estrategia como en la respuesta de Konamiman a continuación) –

+0

La respuesta de Konamiman está ARRIBA ahora, porque es la correcta ... dice Chris. –

0

Enunciando lo obvio: Probablemente podría usar un método de fábrica para obtener la instancia de la clase renderizada requerida y llamar a render en ese para obtener la salida requerida

5

Este es un caso clásico para usar la herencia de objetos para lograr lo que desea. Cada vez que vea una gran declaración de conmutación activando el tipo de objeto, debería considerar alguna forma de subclasificación.

Veo dos enfoques, dependiendo de lo "común" estos tipos de preguntas son en realidad y si la representación es la única diferencia entre ellos:

Opción 1 - subclase de la clase de preguntas

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 
    } 
} 

Opción 2 - Cree una interfaz de renderizado, y haga que una propiedad en su clase de pregunta

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 
    } 
} 

En este caso, creará una instancia del procesador en su constructor en función del tipo de pregunta.

La opción 1 es probablemente preferible si los tipos de preguntas difieren en más formas que en el procesamiento. Si la representación es la única diferencia, considere la opción 2.

+0

Pensé en hacer una subclase de la pregunta en sí, pero no estoy seguro de cómo hacer que NHibernate devuelva un subtipo específico según el tipo de propiedad. Idealmente, no quiero hacer un montón de casting después de haber recuperado el objeto de la base de datos. – Chris

+0

En ese caso, el patrón de estrategia (opción 2) sería ideal. Puede mantener el Tipo de pregunta como propiedad interna o privada (o pública si aún lo necesita) y crear una instancia de IRenderer en función de ese valor. –

0

El enfoque que tomaría es crear un Control separado (o un método HtmlHelper si está en MVC) para cada estilo visual con el que desea que se representen sus preguntas. Esto separa las preocupaciones de representar la pregunta como un objeto y representarla visualmente pulcramente.

Luego puede usar un Control maestro (o método) para elegir el método de representación correcto según el tipo de instancia Question que se le presente.

1

La prestación es definitivamente una preocupación interfaz de usuario, por lo que habría que separar de la clase Question y añadir una fábrica para aislar la lógica de conmutación (la clase base de QuestionControl hereda de WebControl y contendría la mayor parte de la lógica de representación):

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 
    } 
} 

Uso:

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. 
    } 
} 
+0

Si agrega esto al UserType, se crea el control para usted. – AndrewB

+0

Todavía no he explorado suficiente NHibernate, pero el mapeo directo a los controles de la interfaz de usuario suena extremadamente interesante. –

1

creo que lo que quiere es un IUserType que convierten la propiedad de la asignación de hibernación con el tipo correcto control a través de alguna fábrica de preguntas.

Un ejemplo del uso de un IuserType se puede encontrar aquí: NHibernate IUserType

en el ejemplo que convierte una burbuja a una imagen para su uso en el lado del cliente, pero con la misma idea que puede hacer su página creada con el QuestionType.

1

Puede utilizar el patrón de estrategia (Wikipedia) y una fábrica en combinación.

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 
    } 
} 

Solo se deben incluir las propiedades públicas en NHibernate.

+0

Maldición, parece que escribo muy lento :-) – spa