¿Cómo administro un conjunto muy grande de reglas y números mágicos en mi programa?

21

Soy algo nuevo en la programación (soy ingeniero mecánico de oficio), y estoy desarrollando un pequeño programa durante mi tiempo de inactividad que genera una parte (solidworks) basada en el aporte de varias personas de toda la planta.

Basado en solo unas pocas entradas (6 para ser exactos), necesito hacer cientos de llamadas API que pueden tomar hasta una docena de parámetros cada una; todo generado por un conjunto de reglas que he reunido después de entrevistar a todos los que manejan la parte. La sección de reglas y parámetros de mi código es de 250 líneas y sigue creciendo.

Entonces, ¿cuál es la mejor manera de mantener mi código legible y manejable? ¿Cómo compartimento todos mis números mágicos, todas las reglas, algoritmos y partes procesales del código? ¿Cómo trato con una API muy detallada y granular?

Mi objetivo principal es poder entregar a alguien mi fuente y hacer que entiendan lo que estaba haciendo, sin mi opinión.

usuario2785724
fuente
77
¿Puedes proporcionar algunos ejemplos de estas llamadas a la API?
Robert Harvey
"Todos los problemas en informática pueden resolverse mediante otro nivel de indirección" - David Wheeler
Phil Frost
... excepto demasiados niveles de indirección :)
Dan Lyons
1
Es difícil responder a su pregunta sin ver su código. Puede publicar su código en codereview.stackexchange.com y obtener consejos de otros programadores.
Gilbert Le Blanc

Respuestas:

26

Según lo que describa, es probable que desee explorar el maravilloso mundo de las bases de datos. Parece que muchos de los números mágicos que usted describe, especialmente si dependen en parte, son realmente datos, no códigos. Tendrá mucha mejor suerte y le resultará mucho más fácil extender la aplicación a largo plazo, si puede clasificar cómo se relacionan los datos con las partes y definir una estructura de base de datos para ello.

Tenga en cuenta que las "bases de datos" no necesariamente significan MySQL o MS-SQL. La forma en que almacene los datos dependerá mucho de cómo se use el programa, cómo lo escriba, etc. Puede significar una base de datos de tipo SQL, o simplemente puede significar un archivo de texto formateado.

Gran maestro B
fuente
77
Estuvo de acuerdo en codificar los datos en una base de datos, aunque parece que tiene problemas más grandes.
Robert Harvey
Si estuviera creando un programa que comprenda partes completamente diferentes, sí, este sería el camino a seguir. Sin embargo, es solo una parte con cuatro configuraciones ligeramente diferentes. Nunca será una gran cosa (a menos que contraten a un desarrollador para hacer algo así, en cuyo caso no importa). Aunque, supongo que sería una gran experiencia de aprendizaje después de que termine y quiera refactorizar.
user2785724
1
Suena como una codificación suave . Las bases de datos son para estado mutable. Los números mágicos no son mutables, por definición.
Phil Frost
1
@PhilFrost: Puedes hacerlos inmutables. Simplemente no les escriba después de la creación de la tabla inicial.
Robert Harvey
1
@PhilFrost: Bueno, ahora he visto la API con la que está tratando. Es notable solo por su gran tamaño. Es posible que no necesite una base de datos, a menos que lo necesite.
Robert Harvey
14

A menos que anticipe extender esto a varias partes, aún no me gustaría agregar una base de datos. Tener una base de datos significa un montón de cosas que aprender para ti y más cosas que instalar para que funcione para otras personas. Agregar una base de datos incrustada mantiene el archivo ejecutable final portátil, pero alguien con su código fuente ahora tiene una cosa más para comenzar a trabajar.

Creo que una lista de constantes claramente nombradas y funciones de implementación de reglas ayudará mucho. Si le da a todo nombres naturales y se enfoca en técnicas de programación alfabetizadas , debería poder hacer un programa legible.

Idealmente, terminarás con un código que dice:

LeftBearingHoleDepth = BearingWidth + HoleDepthTolerance;
if (not CheckPartWidth(LeftBearingHoleDepth, {other parameters})
    {whatever you need to adjust}

Dependiendo de cuán locales sean las constantes, me vería tentado a declararlas en las funciones en las que se usan cuando sea posible. Es bastante útil convertir:

SomeAPICall(10,324.5, 1, 0.02, 6857);

dentro

const NumberOfOilDrainHoles = 10
const OilDrainHoleSpacing = 324.5
{etc}
SomeAPICall(NumberOfOilDrainHoles, OilDrainHoleSpacing, {etc}

Eso le proporciona en gran medida un código autodocumentado y también alienta a cualquier persona que modifique el código a dar nombres igualmente significativos a lo que agregan. Comenzar local también facilita el manejo del número total de constantes que acumulará. Se vuelve un poco molesto si tiene que seguir desplazándose por una larga lista de constantes para asegurarse de que el valor sea el que desea.

Un consejo para los nombres: ponga la palabra más importante a la izquierda. Puede que no se lea tan bien, pero hace que encontrar las cosas sea más fácil. La mayoría de las veces estás mirando un sumidero y te preguntas sobre el perno, no miras un perno y te preguntas dónde está, así que llámalo SumpBoltThreadPitch, no BoltThreadPitchSump. Luego ordena la lista de constantes. Más tarde, para extraer todos los tonos de hilo, puede obtener la lista en un editor de texto y usar la función de búsqueda, o usar una herramienta como grep para devolver solo las líneas que contienen "ThreadPitch".

Más
fuente
1
También considere crear una interfaz fluida
Ian
Aquí hay una línea real de mi código. ¿Tiene sentido lo que está pasando aquí (los argumentos son x1, y1, z1, x2, y2, z2 como dobles), si supieras lo que significan los nombres de las variables? .CreateLine(m_trunion_support_spacing / 2, -((m_flask_length / 2) + m_sand_ledge_width + m_wall_thickness), -m_flange_thickness, m_trunion_support_spacing / 2, -((m_flask_length / 2) + m_sand_ledge_width + m_wall_thickness), -m_flask_height + m_flange_thickness)
user2785724
También puede usar ctags con integración de editor para encontrar las constantes.
Phil Frost
3
@ user2785724 Eso es un desastre. ¿Qué está haciendo? ¿Está haciendo un surco de una longitud y profundidad particulares? Entonces podrías crear una función llamada createGroove(length, depth). Debe implementar funciones que describan lo que desea lograr como lo describiría a un ingeniero mecánico. De eso se trata la programación alfabetizada.
Phil Frost
Esa es la llamada API para dibujar una línea en el espacio 3d. Cada uno de los 6 argumentos están en diferentes líneas en el programa. Toda la API está loca. No sabía dónde hacer el desastre, así que lo hice allí. Si supiera cuál era la llamada API y sus argumentos, vería cuáles eran los puntos finales, utilizando parámetros que le son familiares, y podría relacionarlo con la parte. Si desea familiarizarse con SolidWorks, la API es absolutamente laberíntica.
user2785724
4

Creo que su pregunta se reduce a: ¿cómo estructuro un cálculo? Tenga en cuenta que desea administrar "un conjunto de reglas", que son códigos, y "un conjunto de números mágicos", que son datos. (Puede verlos como "datos incrustados en su código", pero de todos modos son datos).

Además, hacer que su código sea "comprensible para otros" es, de hecho, el objetivo general de todos los paradigmas de programación (consulte, por ejemplo, " Patrones de implementación " de Kent Beck, o " Código limpio " de Robert C. Martin para autores sobre software que declaran el mismo objetivo como tú, para cualquier programa).

Todas las sugerencias en estos libros se aplicarían a su pregunta. Permítanme extraer algunos consejos específicos para "números mágicos" y "conjuntos de reglas":

  1. Use constantes con nombre y enumeraciones para reemplazar los números mágicos

    Ejemplo de constantes :

    if (partWidth > 0.625) {
        // doSomeApiCall ...
    }
    return (partWidth - 0.625)
    

    debe reemplazarse con una constante con nombre para que ningún cambio posterior pueda introducir un error tipográfico y romper su código, por ejemplo, cambiando el primero 0.625pero no el segundo.

    const double MAX_PART_WIDTH = 0.625;
    
    if (partWidth > MAX_PART_WIDTH) {
        // doSomeApiCall ...
    }
    return (partWidth - MAX_PART_WIDTH)
    

    Ejemplo de enumeraciones :

    Las enumeraciones pueden ayudarlo a reunir datos que pertenecen juntos. Si está utilizando Java, recuerde que las enumeraciones son objetos; sus elementos pueden contener datos, y puede definir métodos que devuelvan todos los elementos o verificar alguna propiedad. Aquí se usa una Enum para construir otra Enum:

    public enum EnginePart {
        CYLINDER (100, Materials.STEEL),
        FLYWHEEL (120, Materials.STEEL),
        CRANKSHAFT (200, Materials.CARBON);
    
        private final double maxTemperature;
        private final Materials composition;
        private EnginePart(double maxTemperature, Materials composition) {
            this.maxTemperature = maxTemperature;
            this.composition = composition;
        }
    }
    
    public enum Materials {
        STEEL,
        CARBON
    }
    

    La ventaja es que ahora nadie puede definir erróneamente un EnginePart que no esté hecho de acero o carbono, y nadie puede introducir un EnginePart llamado "asdfasdf", como sería el caso si se tratara de una cadena que se verificaría en el contenido.

  2. El patrón de estrategia y el patrón de método Factory describen cómo encapsular "reglas" y pasarlas a otro objeto que las use (en el caso del patrón Factory, el uso es construir algo; en el caso del patrón Strategy, el el uso es lo que quieras).

    Ejemplo de patrón de método de fábrica :

    Imagine que tiene dos tipos de motores: uno donde cada parte tiene que estar conectada al Compresor, y uno donde cada parte puede conectarse libremente a cualquier otra parte. Adaptado de Wikipedia

    public class EngineAssemblyLine {
        public EngineAssemblyLine() {
            EnginePart enginePart1 = makeEnginePart();
            EnginePart enginePart2 = makeEnginePart();
            enginePart1.connect(enginePart2);
            this.addEngine(engine1);
            this.addEngine(engine2);
        }
    
        protected Room makeEngine() {
            return new NormalEngine();
        }
    }
    

    Y luego en otra clase:

    public class CompressedEngineAssemblyLine extends EngineAssemblyLine {
        @Override
        protected Room makeRoom() {
            return new CompressedEngine();
        }
    }
    

    La parte interesante es: ahora su constructor de la línea de ensamblaje está separado del tipo de motor que maneja. Tal vez los addEnginemétodos están llamando a una API remota ...

    Ejemplo de patrón de estrategia :

    El patrón de estrategia describe cómo introducir una función en un objeto para cambiar su comportamiento. Imaginemos que a veces desea pulir una pieza, a veces desea pintarla y, de forma predeterminada, desea revisar su calidad. Este es un ejemplo de Python, adaptado de Stack Overflow

    class PartWithStrategy:
    
        def __init__(self, func=None) :
            if func:
                self.execute = func
    
        def execute(self):
            # ... call API of quality review ...
            print "Part will be reviewed"
    
    
    def polish():
        # ... call API of polishing department ...
        print "Part will be polished"
    
    
    def paint():
        # ... call API of painting department ...
        print "Part will be painted"
    
    if __name__ == "__main__" :
        strat0 = PartWithStrategy()
        strat1 = PartWithStrategy(polish)
        strat2 = PartWithStrategy(paint)
    
        strat0.execute()  # output is "Part will be reviewed"
        strat1.execute()  # output is "Part will be polished"
        strat2.execute()  # output is "Part will be painted"
    

    Puede ampliar esto para mantener una lista de acciones que desea realizar y luego llamarlas a su vez desde el executemétodo. Tal vez esta generalización podría describirse mejor como un patrón de Constructor , pero bueno, no queremos ser exigentes, ¿verdad? :)

logc
fuente
2

Es posible que desee utilizar un motor de reglas. Un motor de reglas le proporciona un DSL (lenguaje específico de dominio) que está diseñado para modelar los criterios necesarios para un determinado resultado de una manera comprensible, como se explica en esta pregunta .

Dependiendo de la implementación del motor de reglas, las reglas pueden incluso cambiarse sin volver a compilar el código. Y debido a que las reglas están escritas en su propio lenguaje simple, los usuarios también pueden cambiarlas.

Si tiene suerte, hay un motor de reglas listo para usar para el lenguaje de programación que está utilizando.

La desventaja es que debes familiarizarte con un motor de reglas que puede ser difícil si eres un principiante en programación.

zilluss
fuente
1

Mi solución a este problema es bastante diferente: capas, configuraciones y LOP.

Primero envuelva la API en una capa. Encuentre secuencias de llamadas API que se usan juntas y combínelas en sus propias llamadas API. Eventualmente no debería haber llamadas directas a la API subyacente, solo llamadas a sus contenedores. Las llamadas de contenedor deberían comenzar a parecerse a un mini idioma.

Segundo, implemente un 'administrador de configuraciones'. Esta es una forma de asociar nombres con valores dinámicamente. Algo como esto. Otro mini idioma.

Baseplate.name="Base plate"
Baseplate.length=1032.5
Baseplate.width=587.3

Finalmente, implemente su propio mini lenguaje para expresar diseños (esto es programación orientada al lenguaje). Este lenguaje debe ser comprensible para los ingenieros y diseñadores que contribuyen con las reglas y la configuración. El primer ejemplo de tal producto que viene a la mente es Gnuplot, pero hay muchos otros. Podrías usar Python, aunque personalmente no lo haría.

Entiendo que este es un enfoque complejo, y puede ser excesivo para su problema o requerir habilidades que aún no ha adquirido. Así es como lo haría.

david.pfx
fuente
0

No estoy seguro de haber recibido la pregunta correctamente, pero parece que debería agrupar las cosas en algunas estructuras. Digamos que si está usando C ++, puede definir cosas como:

struct SomeParametersClass
{
    int   p1;  // this is for that
    float p2;  // this is a different parameter
    ...
    SomeParametersClass() // constructor, assigns default values
    {
        p1 = 42; // the best value that some guy told me
        p2 = 3.14; // looks like a know value, but isn't
    {
};

struct SomeOtherParametersClass
{
    int   v1;  // this is for ...
    float v2;  // this is for ...
    ...
    SomeOtherParametersClass() // constructor, assigns default values
    {
        v1 = 24; // the best value 
        v2 = 1.23; // also the best value
    }
};

Puede instanciar estos al comienzo del programa:

int main()
{
    SomeParametersClass params1;
    SomeOtherParametersClass params2;
    ...

Entonces sus llamadas a la API se verán así (suponiendo que no pueda cambiar la firma):

 SomeAPICall( params1.p1, params1.p2 );

Si puede cambiar la firma de la API, puede pasar toda la estructura:

 SomeAPICall( params1 );

También puede agrupar todos los parámetros en un contenedor más grande:

struct AllTheParameters
{
    SomeParametersClass      SPC;
    SomeOtherParametersClass SOPC;
};
kebs
fuente
0

Me sorprende que nadie más haya mencionado esto ...

Tu dijiste:

Mi objetivo principal es poder entregar a alguien mi fuente y hacer que entiendan lo que estaba haciendo, sin mi opinión.

Permítanme decir esto, la mayoría de las otras respuestas están en el camino correcto. Definitivamente creo que las bases de datos podrían ayudarte. Pero otra cosa que lo ayudará es hacer comentarios, buenos nombres de variables y una organización / separación adecuada de las preocupaciones.

Todas las otras respuestas tienen una base muy técnica, pero ignoran los fundamentos que la mayoría de los programadores aprenden. Dado que usted es un ingeniero mecánico por profesión, supongo que no está acostumbrado a este estilo de documentación.

Comentar y elegir nombres de variables buenos y concisos ayuda enormemente con la legibilidad. ¿Cuál es más fácil de entender?

var x = y + z;

O:

//Where bandwidth, which was previously defined is (1000 * Info Rate) / FEC Rate / Modulation * carrier spacing / 1000000
float endFrequency = centerFrequency + (1/2 bandwidth);

Este es un lenguaje bastante independiente. No importa con qué plataforma, IDE, idioma, etc., esté trabajando, la documentación adecuada es la forma más limpia y fácil de asegurarse de que alguien pueda entender su código.

Luego se trata de manejar esos números mágicos y toneladas de preocupaciones, pero creo que el comentario de GrandmasterB lo manejó bastante bien.

Dibujó
fuente