2012-05-14 16 views
7

Estoy escribiendo un cliente de aplicaciones JS. El usuario puede editar la lista/árbol de elementos de texto (por ejemplo, una lista de tareas pendientes o notas). Manipulo DOM con jQuery mucho.¿Cómo evitar el código duplicado cuando se opera en DOM en la dirección "arriba" y "abajo"?

El usuario puede navegar por la lista hacia arriba y hacia abajo usando el teclado (similar a las teclas J/K en GMail), y realizar varias otras operaciones. Muchas de estas operaciones tienen una funcionalidad espejo "arriba"/"abajo", p.

$.fn.moveItemUp = function() { 
    var prev = this.getPreviousItem(); 
    prev && this.insertBefore(prev); 
    // there's a bit more code in here, but the idea is pretty simple, 
    // i.e. move the item up if there's a previous item in the list 
} 

$.fn.moveItemDown = function() { 
    var next = this.getNextItem(); 
    next && this.insertAfter(next); 
    // .... 
} 

Ahora este patrón de tener dos funciones casi idénticas se repite en varios lugares en mi código, porque hay muchas operaciones en la lista/árbol de elementos que son más o menos simétrica.

PREGUNTA: ¿Cómo puedo refactorizar esto elegantemente para evitar la duplicación de código?

La forma trivial me ocurrió hasta ahora es utilizar .apply() ...

$.fn.moveItem = function(direction) { 
     var up = direction === 'up', 
      sibling = up ? this.getPreviousItem() : this.getNextItem(), 
      func = up ? $.fn.insertBefore : $.fn.insertAfter; 

     // ... 

     if (! sibling) { return false; } 

     func.apply(this, [ sibling ]); 

     // ... 
    }; 

El beneficio es un mantenimiento más fácil cuando la estructura de otros elementos del código requiere cambiar moveUp/moveDown. Ya he necesitado cambiar ligeramente el código varias veces, y siempre tengo que tener en cuenta que tengo que hacerlo en dos lugares ...

Pero no estoy contento con la versión "unificada", porque:

  • La implementación es más complicada que el simple moveUp/moveDown, por lo tanto, en el futuro puede que NO sea tan fácil de mantener después de todo. Me gusta el código simple Todos estos parámetros, operaciones ternarias, .aplicar() trucos en cada función que se supone que va "arriba" y "abajo" ...
  • No estoy seguro de si es seguro referirse a funciones jQuery.fn. * Explícitamente (después de todo se supone que deben utilizarse aplicándolos a jQuery object $ ('...'). *)

¿Cómo se resuelven esas situaciones de "código casi idéntico" cuando se trabaja con DOM o una estructura similar?

+1

BTW hugomg

Respuesta

4

Una cosa que puede hacer es usar cierres para ocultar el parámetro de configuración que pasa desde el usuario:

var mk_mover = function(direction){ 
    return function(){ 
     //stuff 
    }; 
}; 

var move_up = mk_mover("up"); 
var move_down = mk_mover("down"); 

Si desea continuar en esta dirección, se hace básicamente una cuestión de decidir cuál es la mejor manera de pasar los parámetros a la implementación común, y no hay una única mejor solución para esto.


Una posible dirección a seguir es utilizar un enfoque de estilo OO, pasando un objeto de "estrategia" de configuración a la función de implementación.

var mk_mover = function(args){ 
    var get_item = args.getItem, 
     ins_next = args.insNext; 
    return function(){ 
     var next = get_item.call(this); 
     next && ins_next.call(this, next); 
    }; 
}; 

var move_up = mk_mover({ 
    get_item : function(){ return this.getPrevious(); }, 
    get_next : function(x){ return this.insertBefore(x); } 
}); 

var move_down = mk_mover({/**/}); 

Prefiero hacer esto cuando la interfaz de estrategia (los métodos que difieren entre las direcciones) es pequeño y relativamente constante y cuando se quiere abrir la posibilidad de añadir nuevos tipos de dirección en el futuro.

También tiendo a usar esto cuando sé que ni el conjunto de instrucciones ni los tipos de métodos necesitarán cambiar mucho, ya que JS tiene mejor soporte para OO que para declaraciones de cambio.


La otra posible dirección para ir es el enfoque de enumeración que utilizó:

var mk_mover = function(direction){ 
    return function(){ 
     var next = (
      direction === 'up' ? this.getNextItem() : 
      direction === 'down' ? this.getPreviousItem() : 
      null); 

     if(next){ 
      if(direction === 'up'){ 
       this.insertAfter(next); 
      }else if(direction === 'down'){ 
       this.insertBefore(next); 
      } 
     } 

     //... 
    }; 
} 

A veces se puede utilizar objetos ordenados o matrices en lugar de instrucciones switch para hacer las cosas más bonitas:

var something = ({ 
    up : 17, 
    down: 42 
}[direction]); 

Aunque tengo que confesar que esto es un poco torpe, tiene la ventaja de que si su conjunto de instrucciones se soluciona desde el principio, ahora tiene mucha flexibilidad para agregar un nuevo if-else o switc h declaración siempre que lo necesite, de forma autónoma (sin necesidad de pasar añadir algunos métodos para el objeto estrategia en otro lugar ...)


Por cierto, creo que el enfoque que sugirió se encuentra en una incómodo terreno intermedio entre las dos versiones que sugerí.

Si todo lo que está haciendo es cambiar dentro de su función dependiendo de la etiqueta de dirección del valor aprobada, es mejor hacer los cambios directamente en los lugares que necesita sin tener que refaccionar las cosas en funciones separadas y luego tener que hacer muchas de molesto .call y .aplicar cosas.

Por otro lado, si se tomó la molestia de definir y separar las funciones, puede hacerlo para que las reciba directamente de la persona que llama (en forma de un objeto de estrategia) en lugar de hacer el envío por entrega tú mismo

+0

Thx para buenas sugerencias, especialmente la idea de un método de "fábrica". –

2

Me he encontrado con problemas similares antes, y no hay realmente un gran patrón que he encontrado para hacer tareas aparentemente similares a la inversa.

Si bien tiene sentido para usted como ser humano que sean similares, realmente solo está llamando a métodos arbitrarios en lo que respecta al compilador. La única duplicación obvia es el orden en que se llaman cosas:

  1. encontrar un elemento que nos gustaría insertar antes o después de
  2. cheque que el elemento existe
  3. insertar el elemento actual antes o después de es

Ya ha resuelto el problema de una manera que básicamente hace estas comprobaciones. Sin embargo, su solución es un poco ilegible. Así es como me gustaría que resolver:

$.fn.moveItem = function(dir){ 
    var index = dir == 'up' ? 0:1, 
     item = this['getPreviousItem','getNextItem'][index](); 
    if(item.length){ 
    this['insertBefore','insertAfter'][index].call(this,item); 
    } 
} 

Mediante el uso de un índice de matriz, podemos ver lo que se llama cuando está siendo llamado un poco más fácil que en su ejemplo. Además, el uso de apply es innecesario, ya que sabe cuántos argumentos está pasando, por lo que la llamada se adapta mejor a ese caso de uso.

No estoy seguro si eso es mejor, pero es un poco más corto (¿es necesario devolver el mensaje falso?) Y un OMI un poco más legible.

En mi opinión, la centralización de las acciones es algo más importante que la legibilidad: si desea optimizar, cambiar o anexar la acción de mover algo, solo tiene que hacerlo en un solo lugar. Siempre puede agregar comentarios para resolver el problema de legibilidad.

+0

apply vs call - g2k (más en: http://stackoverflow.com/questions/ 1986896/what-is-the-difference-between-call-and-apply). –

1

Creo que el enfoque de una función es el mejor, aún así lo separaría de esa cosa arriba/abajo. Haga una función para mover un elemento a un índice o antes de otro elemento (prefiero el último) y maneje todos los eventos de "movimiento" genéricamente allí, no dependiendo de una dirección.

function moveBefore(node) { 
    this.parentNode.insertBefore(this, node); // node may be null, then it equals append() 

    // do everything else you need to handle when moving items 
} 
function moveUp() { 
    var prev = this.previousSibling; 
    return !!prev && moveBefore.call(this, prev); 
} 
function moveDown() { 
    var next = this.nextSibling; 
    return !!next && moveBefore.call(this, next.nextSibling); 
} 
+0

¡Gracias por la buena sugerencia de separar lo común de lo diferente! –

Cuestiones relacionadas