rendimiento versus reutilización


8

¿Cómo puedo escribir funciones que sean reutilizables sin sacrificar el rendimiento? En repetidas ocasiones me encuentro con la situación en la que quiero escribir una función de manera que sea reutilizable (por ejemplo, no hace suposiciones sobre el entorno de datos), pero conociendo el flujo general del programa, sé que no es la más eficiente. método. Por ejemplo, si quiero escribir una función que valide un código de inventario pero sea reutilizable, no puedo asumir que el conjunto de registros está abierto. Sin embargo, si abro y cierro el conjunto de registros cada vez que se llama a la función, el rendimiento alcanzado al recorrer miles de filas podría ser enorme.

Entonces, para el rendimiento, podría tener:

Function IsValidStockRef(strStockRef, rstStockRecords)
    rstStockRecords.Find ("stockref='" & strStockRef & "'")
    IsValidStockRef = Not rstStockRecords.EOF
End Function

Pero para la reutilización necesitaría algo como lo siguiente:

Function IsValidStockRef(strStockRef)
    Dim rstStockRecords As ADODB.Recordset

    Set rstStockRecords = New ADODB.Recordset
    rstStockRecords.Open strTable, gconnADO

    rstStockRecords.Find ("stockref='" & strStockRef & "'")
    IsValidStockRef = Not rstStockRecords.EOF

    rstStockRecords.Close
    Set rstStockRecords = Nothing
End Function

Me preocupa que el impacto en el rendimiento de abrir y cerrar ese conjunto de registros cuando se llama desde un bucle en miles de filas / registros sería grave, pero el uso del primer método hace que la función sea menos reutilizable.

¿Qué tengo que hacer?


Respuestas:


13

Debe hacer lo que sea que produzca el mayor valor comercial en esta situación.

Escribir software siempre es una compensación. Casi nunca todos los objetivos válidos (mantenibilidad, rendimiento, claridad, concisión, seguridad, etc., etc.) están completamente alineados. No caigas en la trampa de esas personas miopes que consideran una de estas dimensiones como primordial y te dicen que sacrifiques todo por ella.

En cambio, comprenda qué riesgos y qué beneficios ofrece cada alternativa, cuantícelos y elija el que maximice el resultado. (No tiene que hacer estimaciones numéricas, por supuesto. Es suficiente sopesar factores como "usar esta clase significa encerrarnos en ese algoritmo hash, pero dado que no lo estamos usando para protegernos contra ataques maliciosos , solo para conveniencia, esta es tan buena que podemos ignorar la posibilidad de 1: 1,000,000,000 de una colisión accidental ".)

Lo más importante es recordar que son compensaciones; Ningún principio único justifica todo para satisfacer, y ninguna decisión, una vez tomada, debe mantenerse eternamente . Es posible que siempre tenga que revisar en retrospectiva cuando las circunstancias cambian de una manera que no previó. Es un dolor, pero no tan doloroso como tomar la misma decisión sin mirar atrás.


2
Si bien lo que usted dice es cierto en general, se debe decir algo para escribir código para proteger contra cualquier posible caso frente a tener requisitos previos. En mi humilde opinión, no hay nada de malo en simplemente esperar que el conjunto de registros ya esté abierto cuando se llama, dada la documentación suficiente. En todo caso, si este es un método en una biblioteca, realice una comprobación rápida si está abierto y, si no lo está, inicie una excepción. No es necesario "hacer que funcione" en ningún escenario posible.
Neil

6

Ninguno de estos parece más reutilizable que el otro. Simplemente parecen estar en diferentes niveles de abstracción . La primera es para llamar a un código que comprende el sistema de existencias lo suficientemente íntimo como para saber que validar una referencia de existencias significa mirar a través de Recordsetalgún tipo de consulta. El segundo es para llamar a un código que solo quiere saber si un código de acciones es válido o no y no tiene ningún interés en cómo se verificaría tal cosa.

Pero como con la mayoría de las abstracciones , esta es "permeable". En este caso, la abstracción se filtra a través de su rendimiento: el código de llamada no puede ignorar por completo cómo se implementa la validación porque si lo hiciera, podría invocar esa función miles de veces como lo describió y degradar seriamente el rendimiento general.

En última instancia, si tiene que elegir entre un código mal abstraído y un rendimiento inaceptable, debe elegir el código mal abstraído. Pero primero, debe buscar una mejor solución: un compromiso que mantenga un rendimiento aceptable y presente una abstracción decente (si no ideal). Desafortunadamente, no conozco muy bien VBA, pero en un lenguaje OO, mi primer pensamiento sería dar una clase de código de llamada con métodos como:

BeginValidation()
IsValidStockRef(strStockRef)
EndValidation()

Aquí, sus métodos Begin...y End...hacen la gestión del ciclo de vida de una sola vez del conjunto de registros, que IsValidStockRefcoincide con su primera versión, pero utiliza este conjunto de registros del que la propia clase se ha responsabilizado, en lugar de pasarlo. El código de llamada llamaría al Begin...y End...métodos fuera del ciclo y el método de validación dentro.

Nota: Este es solo un ejemplo ilustrativo muy general, y podría considerarse un primer paso en la refactorización. Los nombres probablemente podrían usar ajustes, y dependiendo del lenguaje debería haber una forma más limpia o idiomática de hacerlo (C #, por ejemplo, podría usar el constructor para comenzar y Dispose()terminar). Idealmente, el código que solo quiere verificar si una referencia de stock es válida no debería tener que hacer ninguna gestión del ciclo de vida.

Esto representa una ligera degradación de la abstracción que estamos presentando: ahora el código de llamada necesita saber lo suficiente sobre la validación para comprender que es algo que requiere algún tipo de configuración y desmontaje. Pero a cambio de ese compromiso relativamente modesto, ahora tenemos métodos que se pueden usar fácilmente llamando al código, sin dañar nuestro rendimiento.


Votante: ¿Alguna razón en particular, fuera de interés?
Ben Aaronson

Yo no era el votante, pero supongo. BeginValidation, EndValidationy IsValidStockReftienen una relación especial entre ellos. El conocimiento de esa relación es más complejo que el conocimiento que se requeriría para manejar directamente a RecordSet. Y el conocimiento requerido para manejar un RecordSetes más ampliamente aplicable.
Keen

@Cory Estoy de acuerdo en cierta medida, y mi mano se vio un poco forzada por la falta de conocimiento sobre vba. Intenté señalar esto con la siguiente oración, pero tal vez mi redacción no era clara o lo suficientemente fuerte. He hecho una edición para tratar de aclarar esto un poco
Ben Aaronson

Una nota interesante, en C #, se espera que use la usinginstrucción para hacer este trabajo. En otros idiomas (aquellos que usan excepciones de todos modos), para hacer el mismo trabajo que using, necesitaría usar try {} finally {}para garantizar la eliminación adecuada, e incluso a veces es imposible envolver correctamente todo el código que podría throw. Este es un problema potencial con todas las soluciones mencionadas aquí, y tampoco estoy seguro de cómo debería resolverse esto en VBA.
Keen

@Cory: Y en C ++, simplemente usarías RAII.
Deduplicador

3

Durante mucho tiempo, solía implementar un sistema complicado de controles para poder usar las transacciones de la base de datos. La lógica de transacción es la siguiente: abrir una transacción, realizar las operaciones de la base de datos, revertir en caso de error o comprometerse con éxito. La complicación proviene de lo que sucede cuando desea que se realice una operación adicional dentro de la misma transacción. Debería escribir un segundo método por completo que realice ambas operaciones, o podría llamar a su método original desde un segundo, abrir una transacción solo si aún no se ha abierto y confirmar / deshacer los cambios solo si usted fuera el uno para abrir la transacción.

Por ejemplo:

public void method1() {
    boolean selfOpened = false;
    if(!transaction.isOpen()) {
        selfOpened = true;
        transaction.open();
    }

    try {
        performDbOperations();
        method2();

        if(selfOpened) 
            transaction.commit();
    } catch (SQLException e) {
        if(selfOpened) 
            transaction.rollback();
        throw e;
    }
}

public void method2() {
    boolean selfOpened = false;
    if(!transaction.isOpen()) { 
        selfOpened = true;
        transaction.open();
    }

    try {
        performMoreDbOperations();

        if(selfOpened) 
            transaction.commit();
    } catch (SQLException e) {
        if(selfOpened) 
            transaction.rollback();
        throw e;
    }
}

Tenga en cuenta que no estoy abogando por el código anterior de ninguna manera. ¡Esto debería servir como un ejemplo de lo que no se debe hacer!

Parecía una tontería crear un segundo método para realizar la misma lógica que el primero más algo extra, pero quería poder llamar a la sección API del programa de la base de datos y solucionar los problemas allí. Sin embargo, si bien esto resolvió parcialmente mi problema, cada método que escribí implicaba agregar esta lógica detallada de verificar si una transacción ya está abierta y confirmar / deshacer los cambios si mi método la abrió.

El problema fue conceptual. No debería haber intentado adoptar todos los escenarios posibles. El enfoque adecuado era alojar la lógica de transacción en un único método tomando un segundo método como parámetro que realizaría la lógica real de la base de datos. Esa lógica asume que la transacción está abierta y ni siquiera realiza una verificación. Estos métodos podrían llamarse en combinación para que estos métodos no estuvieran saturados de lógica de transacción innecesaria.

La razón por la que menciono esto es porque mi error fue asumir que necesitaba que mi método funcionara en cualquier situación. Al hacerlo, no solo mi método llamado verificaba si una transacción estaba abierta, sino también aquellos a los que llamaba. En este caso, no es un gran impacto en el rendimiento, pero si digo que necesitaba verificar la existencia de un registro en la base de datos antes de continuar, estaría verificando cada método que lo requiera cuando debería haber asumido todo el tiempo la persona que llama debe ser consciente de que el registro debe existir. Si se llama al método de todos modos, este es un comportamiento indefinido y no necesita preocuparse por lo que sucede.

En su lugar, debe proporcionar mucha documentación y escribir lo que espera que sea cierto antes de realizar una llamada a su método. Si es lo suficientemente importante, agréguelo como comentario antes de su método para que no haya ningún error (javadoc proporciona un buen soporte para este tipo de cosas en java).

¡Espero que eso ayude!


2

Podría tener dos funciones sobrecargadas. De esa manera, puede usar ambos según la situación.

Nunca se puede (nunca lo he visto suceder) optimizar para todo, así que tienes que conformarte con algo. Elige lo que creas que es más importante.


Desafortunadamente, estoy haciendo mucho en VBA y la sobrecarga no es una opción. Sin embargo, podría usar un Optionalparámetro para lograr un efecto similar.
Caltor

2

2 funciones: una abre el conjunto de registros y lo pasa a una función de análisis de datos.

El primero se puede omitir si ya tiene un conjunto de registros abierto. El segundo puede suponer que se le pasará un conjunto de registros abierto, ignorando de dónde vino, y procesará los datos.

¡Tienes tanto rendimiento como reutilización entonces!


No creo que sea necesario abrir el conjunto de registros para la persona que llama, pero de lo contrario estoy de acuerdo.
Neil

0

La optimización (además de la microoptimización) está directamente en desacuerdo con la modularidad.

La modularidad funciona al aislar el código de su contexto global, mientras que la optimización del rendimiento explota el contexto global para minimizar lo que el código tiene que hacer. La modularidad es el beneficio del acoplamiento bajo, mientras que (el potencial para) un rendimiento muy alto es el beneficio del acoplamiento alto.

La respuesta es arquitectónica. Considere las partes del código que va a querer reutilizar. Quizás sea el componente de cálculo de precios o la lógica de validación de la configuración.

Luego, debe escribir el código que interactúa con ese componente para su reutilización. Dentro de un componente en el que nunca puede usar solo una parte del código, puede optimizar el rendimiento ya que sabe que nadie más lo usará.

El truco es determinar cuáles son sus componentes.

tl; dr: entre los componentes escriben teniendo en cuenta la modularidad, dentro de los componentes escriben teniendo en cuenta el rendimiento.


La modularidad y la optimización no están necesariamente en desacuerdo. Los compiladores modernos pueden incluir casi cualquier cosa en cualquier lugar, por lo que no importa qué tan modular se escriba, siempre y cuando el compilador pueda unirlo a un "ejecutable no modular", no hay razón para que no sea tan rápido como el código escrito no modular en primer lugar. Por supuesto, no todos los compiladores pueden hacer esto muy bien, pero ...
Leftaroundabout

@leftaroundabout Bueno, quise decir a nivel de código fuente, pero tienes mucha razón. ¡No hay razón para que un compilador lo suficientemente inteligente no pueda reemplazar su tipo de burbuja con un tipo rápido!
Trineo
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.