¿Usar subclases vacías para agregar a la semántica de una jerarquía de clases?

7

Me preguntaba si usar una clase derivada (casi) vacía para dar semántica adicional a una jerarquía de clases fue una buena idea (o práctica).

Debido a que un ejemplo (no tan corto) es siempre mejor que un discurso largo, supongamos que estamos escribiendo una jerarquía de clases para la generación de SQL. Desea representar un comando de manipulación de datos (INSERTAR, ACTUALIZAR o BORRAR), por lo que tiene dos alternativas:

Primera versión, con declaración de cambio

enum CommandType {
    Insert,
    Update,
    Delete
}

class SqlCommand {
    String TableName { get; }
    String SchemaName { get; }
    CommandType CommandType { get; }
    IEnumerable<string> Columns { get; }
    // ... and so on
}

class SqlGenerator  {
    string GenerateSQLFor(SqlCommand command)
    {
        switch (command.CommandType) 
        {
            case CommandType.Insert:
                return generateInsertCommand(command);
            case CommandType.Update:
                return generateUpdateCommand(command);
            case CommandType.Delete:
                return generateDeleteCommand(command);
            default:
                throw new NotSupportedException();
        }
    }
}

Segunda versión con VisitorPattern (Tenga en cuenta que la pregunta NO es sobre si se debe usar el patrón de Visitante o agregar nuevas operaciones)

abstract class SqlCommand {
    String TableName { get; }
    String SchemaName { get; }
    IEnumerable<string> Columns { get; }
    // ... and so on
    abstract void Accept(SqlCommandVisitor visitor);
}

class InsertCommand : SqlCommand 
{
    overrides void Accept(SqlCommandVisitor visitor) 
    {
        visitor.VisitInsertCommand(this);
    }
}

class DeleteCommand : SqlCommand 
{
    overrides void Accept(SqlCommandVisitor visitor) 
    {
        visitor.VisitDeleteCommand(this);
    }
}

class SqlCommandVisitor  {
    void InitStringBuffer() { ... }
    string GetStringBuffer() { ... }

    string GenerateSQLFor(SqlCommand command)
    {
        InitStringBuffer();
        command.Accept(this);
        return GetStringBuffer();
    }

    void Visit(SqlCommand command) { ... }
    void VisitInsertCommand(InsertCommand command) { ... }
    void VisitDeleteCommand(DeleteCommand command) { ... }
    void VisitUpdateCommand(UpdateCommand command) { ... }
}

Con ambos ejemplos logramos el mismo resultado, pero:

  • En la primera versión, mi código se siente más SECO, a pesar de que el polimorfismo es generalmente preferible a una declaración de cambio.
  • En la segunda versión, siento que estoy derivando SqlCommand innecesariamente solo para que el código tenga más significado.

Otro caso similar es derivar una clase de una clase genérica, solo para dar un significado adicional a la colección, por ejemplo:

class CustomerList : List<Customer> { }

Me quedé preguntándome si esta es una buena idea. ¿Hay alguna trampa para hacerlo?

Personalmente prefiero la segunda versión exactamente porque agrega significado al código y me ayuda cuando leo mi código más tarde ... Pero claramente no tengo la experiencia para ver algún inconveniente en este patrón.

    
pregunta T. Fabre 17.07.2012 - 12:50

6 respuestas

1

Su segundo ejemplo tiene la ventaja de que no hay ninguna declaración de caso, y no CommandType. Esto significa que puede un nuevo tipo de declaración sin tener que modificar una declaración de caso. Una declaración de caso debería hacer que un programador OOP se detenga un momento y considere si se debe cambiar el diseño para deshacerse de la declaración del caso. Una variable que denota el tipo de una cosa debería hacer que el programador se detenga un buen rato.

Sin embargo, el patrón de visitantes no parece ser necesario aquí. Más sencillo sería utilizar una jerarquía de clases, como lo hizo, pero luego colocar la generación de SQL dentro de esa jerarquía en lugar de tener una clase separada:

class SqlCommand {
  abstract String sql;
}

class InsertCommand : SqlCommand {
  overrides String sql {...};
}

class DeleteCommand : SqlCommand {
  overrides String sql {...};
}

El código dentro de las funciones sql probablemente resultará tener un comportamiento compartido, o suficiente complejidad, que mover el comportamiento y la complejidad compartidos a una clase SqlBuilder separada sería bueno:

class InsertCommand : SqlCommand {
  overrides String sql {
    builder = SqlBuilder.new
    builder.add("insert into")
    builder.add_identifier(table_name)
    buidler.add("values")
    builder.add_value_list(column_names)
    ...
    return builder.sql
  };
}

SqlCommand controla el proceso de creación del sql, insertando sus detalles en SqlBuilder. Esto mantiene a SqlCommand y SqlBuilder desacoplados, ya que SqlCommands no tiene que exponer todo su estado para que lo use el constructor. Esto tiene los beneficios de su segundo ejemplo, pero sin la complejidad del patrón de visitantes.

Si resulta que a veces necesita que el sql se construya de una manera y otras (quizás quiera admitir más de un back-end de SQL), entonces abstrae la clase SqlBuilder, de esta manera:

class SqlBuilder {
  abstract void add(String s);
  abstract void add_identifier(String identifier);
  abstract void add_value_list(String[] value_list);
  ...
}

class PostgresSqlBuilder : SqlBuilder {
  void add(String s) {...}
  void add_identifier(String identifier) {...}
  void add_value_list(String[] value_list) {...}
  ...
}

class MysqlSqlBuilder : SqlBuilder {
  void add(String s) {...}
  void add_identifier(String identifier) {...}
  void add_value_list(String[] value_list) {...}
  ...
}

Luego, las funciones sql usan el generador que se les pasa, en lugar de crear una:

class InsertCommand : SqlCommand {
  overrides String sql (SqlBuilder builder) {
    builder.add("insert into")
    builder.add_identifier(table_name)
    buidler.add("values")
    builder.add_value_list(column_names)
    ...
    return builder.sql
  };
}
    
respondido por el Wayne Conrad 25.08.2012 - 16:19
6

Prefiero el primero, pero tal vez sea solo yo. Me gusta DRY, y me gusta mantener las cosas simples, y no hay nada más simple que una declaración de cambio para esto. (Siempre pienso que, a menos que tenga una buena razón para refactorizar el cambio en una clase polimórfica, esta refactorización es principalmente para mostrar qué tan inteligente puede ser el código).

Usted está escribiendo mucho más código para lograr el mismo resultado, y eso requiere un montón de cortar y pegar, y creo que la codificación c & p es un lugar donde es más probable que aparezcan los errores. También creo que tratar de leer el código es más difícil ya que omite todas esas clases y métodos. Pero en última instancia, es solo de sabor: ambos hacen lo mismo.

Para su segunda pregunta ... depende, una vez que se derive de una clase, usted es responsable de verificar todos los trabajos de plomería. Por ejemplo, no se deriva de las clases de contenedor stl porque no tienen destructores virtuales y, por lo tanto, no están diseñados para herencia. A menos que sepa los peligros potenciales de la clase de la que está heredando, es mucho más seguro contener la clase base.

La otra razón es solo semántica, una lista de clientes no es una lista de clientes, por lo que alguien que lea su código tendría que pensar "¿esto es lo mismo que una lista < > o han modificado la funcionalidad?" Si no realiza ninguna modificación, entonces déjelo como el tipo subyacente, o use una declaración typedef / using que al menos simplemente diga "He cambiado el nombre de este solo para facilitar la lectura".

    
respondido por el gbjbaanb 17.07.2012 - 13:35
5

La desventaja de tener más clases en la jerarquía es que cuando tienes que depurar cosas es mucho más complicado averiguar qué está pasando. Cuanto más profunda sea la jerarquía de su clase, y cuantos más niveles tengan el código real, más difícil será entender el código al depurar (porque siempre debe tener en cuenta dónde se encuentra en la cadena de llamadas en cualquier momento y qué objeto real son, etc, etc).

La verdadera pregunta es: ¿Esto hace que su código en general sea más legible? Si solo tienes esa declaración de cambio en un solo lugar, entonces diría que es una victoria. Mientras que, de lo contrario, terminaría duplicando esa declaración de cambio en todo el lugar, entonces la segunda forma es probablemente una victoria.

    
respondido por el Michael Kohne 17.07.2012 - 13:47
1

El uso del sistema de tipos (en un lenguaje de tipo estático) para expresar semántica es exactamente para lo que está destinado. Considere un tipo de ID de base de datos que usted subclasifique para cada tabla, digamos un CustomerId, un OrderId y así sucesivamente. El beneficio es que nunca confunde un CustomerId con un OrderId. Pero son claramente subclases vacías. Consulte la charla de Google IO sobre GWT por Ray Ryan alrededor de las 7:00 para ver un ejemplo.

    
respondido por el scarfridge 25.08.2012 - 17:00
0

Me gusta más el segundo enfoque, ya que cuando uno mira a InsertCommand , esto se lee como "HAY un comando de inserción", lo que significa que debería haber otros comandos. Cuando lea a continuación, verá estos comandos y comprenderá totalmente la idea.

El primer enfoque dice: "HAY un comando ... con algunos detalles ahí ...".

    
respondido por el Andrey Agibalov 17.07.2012 - 15:47
-2

No, no es una buena práctica en general.

Cada vez que codificas información en el sistema de tipos, lo estás haciendo mal (en lenguajes comunes como C ++, java y C #). Luego requiere que utilice un patrón de visitante o una pila de verificaciones de tipos para extraer esa información que es, en el mejor de los casos, frágil.

Además, en los idiomas principales, es muy difícil almacenar / cambiar esa información. Si, por ejemplo, desea ejecutar un comando ingresado por el usuario en algún IDE de SQL.

En resumen, si está almacenando información, solo use una variable; para eso están ellos.

    
respondido por el Telastyn 17.07.2012 - 13:07

Lea otras preguntas en las etiquetas