Comment gérer un très grand ensemble de règles et de nombres magiques dans mon programme?

21

Je suis un peu nouveau dans la programmation (je suis ingénieur en mécanique de métier), et je développe un petit programme pendant mon temps d'arrêt qui génère une pièce (solidworks) basée sur l'entrée de diverses personnes de partout dans l'usine.

Basé sur seulement quelques entrées (6 pour être exact), j'ai besoin de faire des centaines d'appels API qui peuvent prendre jusqu'à une douzaine de paramètres chacun; tous générés par un ensemble de règles que j'ai rassemblées après avoir interviewé tous ceux qui manipulent la pièce. La section des règles et paramètres de mon code est de 250 lignes et grandit.

Alors, quelle est la meilleure façon de garder mon code lisible et gérable? Comment compartimenter tous mes nombres magiques, toutes les règles, algorithmes et parties procédurales du code? Comment gérer une API très détaillée et granulaire?

Mon objectif principal est de pouvoir remettre à quelqu'un ma source et de lui faire comprendre ce que je faisais, sans ma contribution.

user2785724
la source
7
Pouvez-vous fournir quelques exemples de ces appels d'API?
Robert Harvey
"Tous les problèmes informatiques peuvent être résolus par un autre niveau d'indirection" - David Wheeler
Phil Frost
... sauf trop de niveaux d'indirection :)
Dan Lyons
1
Il est difficile de répondre à votre question sans voir votre code. Vous pouvez publier votre code sur codereview.stackexchange.com et obtenir des conseils d'autres programmeurs.
Gilbert Le Blanc

Réponses:

26

Sur la base de ce que vous décrivez, vous souhaiterez probablement explorer le merveilleux monde des bases de données. Il semble que bon nombre des nombres magiques que vous décrivez - en particulier s'ils dépendent d'une partie - sont vraiment des données, pas du code. Vous aurez beaucoup plus de chance et trouverez beaucoup plus facile d'étendre l'application à long terme, si vous pouvez catégoriser la façon dont les données sont liées aux pièces et définir une structure de base de données pour celle-ci.

Gardez à l'esprit que «bases de données» ne signifie pas nécessairement MySQL ou MS-SQL. La façon dont vous stockez les données dépendra beaucoup de la façon dont le programme est utilisé, de la façon dont vous les écrivez, etc. Cela peut signifier une base de données de type SQL ou simplement un fichier texte formaté.

GrandmasterB
la source
7
D'accord avec la codification des données dans une base de données, bien qu'il semble qu'il ait des problèmes plus importants.
Robert Harvey
Si je créais un programme qui fait des parties complètement différentes, oui, ce serait la voie à suivre. Ce n'est qu'une partie avec quatre configurations légèrement différentes, cependant. Ce ne sera jamais une chose énorme (à moins qu'ils n'engagent un développeur pour faire quelque chose comme ça, auquel cas cela n'a pas d'importance). Bien, je suppose que ce serait une grande expérience d'apprentissage après avoir terminé et voulu refactoriser.
user2785724
1
Cela ressemble à un codage doux . Les bases de données sont pour un état mutable. Les nombres magiques ne sont pas modifiables, par définition.
Phil Frost
1
@PhilFrost: Vous pouvez les rendre immuables. Il suffit de ne pas leur écrire après la création de la table initiale.
Robert Harvey
1
@PhilFrost: Eh bien, j'ai maintenant vu l'API avec laquelle il traite. Il n'est remarquable que par sa taille. Il n'a peut-être pas du tout besoin d'une base de données, sauf si c'est le cas.
Robert Harvey
14

À moins que vous ne prévoyiez d'étendre cela à plusieurs parties, je serais réticent à ajouter une base de données pour l'instant. Avoir une base de données signifie un gros tas de choses à apprendre pour vous, et plus de choses à installer pour le faire fonctionner pour d'autres personnes. L'ajout d'une base de données intégrée conserve l'exécutable final portable, mais quelqu'un avec votre code source a maintenant une chose de plus à faire fonctionner.

Je pense qu'une liste de constantes clairement nommées et de fonctions d'implémentation de règles sera très utile. Si vous donnez tout à des noms naturels et que vous vous concentrez sur des techniques de programmation alphabétisées , vous devriez pouvoir créer un programme lisible.

Idéalement, vous vous retrouverez avec un code qui dit:

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

Selon la façon dont les constantes sont locales, je serais tenté de les déclarer dans les fonctions dans lesquelles elles sont utilisées dans la mesure du possible. Il est très utile de tourner:

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

dans

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

Cela vous donne un code largement auto-documenté et encourage également toute personne qui modifie le code à donner des noms similaires à ce qu'ils ajoutent. Le démarrage local facilite également le traitement du nombre total de constantes que vous accumulerez. Cela devient un peu ennuyeux si vous devez continuer à faire défiler une longue liste de constantes pour vous assurer que la valeur est celle que vous souhaitez.

Un conseil pour les noms: mettez le mot le plus important à gauche. Il peut ne pas lire aussi bien, mais cela facilite la recherche des choses. La plupart du temps, vous regardez un puisard et vous vous interrogez sur le boulon, vous ne regardez pas un boulon et vous vous demandez où il se trouve, alors appelez-le SumpBoltThreadPitch et non BoltThreadPitchSump. Triez ensuite la liste des constantes. Plus tard, pour extraire tous les pas de thread, vous pouvez obtenir la liste dans un éditeur de texte et utiliser la fonction de recherche ou utiliser un outil comme grep pour renvoyer uniquement les lignes qui contiennent "ThreadPitch".

Móż
la source
1
envisagez également de créer une interface Fluent
Ian
Voici une ligne réelle de mon code. Est-ce que cela a du sens ce qui se passe ici (les arguments sont x1, y1, z1, x2, y2, z2 en tant que double), si vous saviez ce que les noms de variables signifiaient? .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
Vous pouvez également utiliser des ctags avec l' intégration de l'éditeur pour trouver les constantes.
Phil Frost
3
@ user2785724 C'est un gâchis. Qu'est-ce que ça fait? Fait-il une rainure d'une longueur et d'une profondeur particulières? Ensuite, vous pouvez créer une fonction appelée createGroove(length, depth). Vous devez implémenter des fonctions qui décrivent ce que vous voulez accomplir comme vous le feriez pour un ingénieur mécanicien. C'est à cela que sert la programmation alphabétisée.
Phil Frost
C'est l' appel de l' API pour tracer une ligne dans l'espace 3D. Chacun des 6 arguments est sur des lignes différentes dans le programme. L'API entière est folle. Je ne savais pas où faire le bordel, alors je l'ai fait là-bas. Si vous saviez ce qu'était l'appel API et ses arguments, vous verriez quels étaient les points de terminaison, en utilisant des paramètres qui vous sont familiers, et vous pourriez le relier à la pièce. Si vous voulez vous familiariser avec SolidWorks, l'API est absolument labyrinthique.
user2785724
4

Je pense que votre question se résume à: comment structurer un calcul? Veuillez noter que vous souhaitez gérer "un ensemble de règles", qui sont du code, et "un ensemble de nombres magiques", qui sont des données. (Vous pouvez les voir comme des "données intégrées dans votre code", mais ce sont néanmoins des données).

De plus, rendre votre code "compréhensible pour les autres" est en fait l'objectif général de tous les paradigmes de programmation (voir par exemple " Implementation Patterns " de Kent Beck, ou " Clean Code " de Robert C. Martin pour les auteurs de logiciels qui déclarent le même objectif comme vous, pour tout programme).

Toutes les astuces de ces livres s'appliqueraient à votre question. Permettez-moi d'extraire quelques indices spécifiquement pour les "nombres magiques" et les "ensembles de règles":

  1. Utilisez des constantes nommées et des énumérations pour remplacer les nombres magiques

    Exemple de constantes :

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

    doit être remplacé par une constante nommée afin qu'aucune modification ultérieure ne puisse introduire une faute de frappe et casser votre code, par exemple en changeant la première 0.625mais pas la seconde.

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

    Exemple d'énumérations :

    Les énumérations peuvent vous aider à rassembler des données qui vont ensemble. Si vous utilisez Java, n'oubliez pas que les énumérations sont des objets; leurs éléments peuvent contenir des données et vous pouvez définir des méthodes qui renvoient tous les éléments ou vérifier certaines propriétés. Ici, un Enum est utilisé pour construire un autre 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
    }
    

    L'avantage étant que désormais personne ne peut définir à tort un EnginePart qui n'est pas fait d'acier ou de carbone, et personne ne peut introduire un EnginePart appelé "asdfasdf", comme ce serait le cas s'il s'agissait d'une chaîne dont le contenu serait vérifié.

  2. Le modèle de stratégie et le modèle de méthode Factory décrivent comment encapsuler des "règles" et les transmettre à un autre objet qui les utilise (dans le cas du modèle Factory, l'utilisation crée quelque chose; dans le cas du modèle Strategy, le l'utilisation est ce que vous voulez).

    Exemple de modèle de méthode d'usine :

    Imaginez que vous ayez deux types de moteurs: un où chaque partie doit être connectée au compresseur, et un où chaque partie peut être librement connectée à n'importe quelle autre partie. Adapté 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();
        }
    }
    

    Et puis dans une autre classe:

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

    La partie intéressante est: maintenant, votre constructeur AssemblyLine est séparé du type de moteur qu'il gère. Peut-être que les addEngineméthodes appellent une API distante ...

    Exemple de modèle de stratégie :

    Le modèle de stratégie décrit comment introduire une fonction dans un objet afin de changer son comportement. Imaginons que vous souhaitiez parfois polir une pièce, parfois que vous souhaitiez la peindre et que, par défaut, vous souhaitiez vérifier sa qualité. Ceci est un exemple Python, adapté 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"
    

    Vous pouvez étendre cela à la tenue d'une liste d'actions que vous souhaitez effectuer, puis les appeler à tour de rôle à partir de la executeméthode. Peut-être que cette généralisation pourrait être mieux décrite comme un modèle Builder , mais bon, nous ne voulons pas devenir pointilleux, n'est-ce pas? :)

logc
la source
2

Vous voudrez peut-être utiliser un moteur de règles. Un moteur de règles vous donne un DSL (Domain Specific Language) qui est conçu pour modéliser les critères nécessaires pour un certain résultat d'une manière compréhensible, comme expliqué dans cette question .

Selon l'implémentation du moteur de règles, les règles peuvent même être modifiées sans recompiler le code. Et parce que les règles sont écrites dans leur propre langage simple, elles peuvent également être modifiées par les utilisateurs.

Si vous avez de la chance, il existe un moteur de règles prêt à l'emploi pour le langage de programmation que vous utilisez.

L'inconvénient est que vous devez vous familiariser avec un moteur de règles qui peut être difficile si vous êtes un débutant en programmation.

zilluss
la source
1

Ma solution à ce problème est assez différente: couches, paramètres et LOP.

Enveloppez d'abord l'API dans une couche. Recherchez des séquences d'appels d'API qui sont utilisées ensemble et combinez-les dans vos propres appels d'API. Finalement, il ne devrait y avoir aucun appel direct à l'API sous-jacente, juste des appels à vos wrappers. Les appels wrapper devraient commencer à ressembler à une mini-langue.

Deuxièmement, implémentez un «gestionnaire de paramètres». Il s'agit d'un moyen d'associer dynamiquement des noms à des valeurs. Quelque chose comme ça. Une autre mini langue.

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

Enfin, implémentez votre propre mini-langage dans lequel exprimer les conceptions (c'est la programmation orientée langage). Ce langage doit être compréhensible pour les ingénieurs et les concepteurs qui contribuent aux règles et paramètres. Le premier exemple d'un tel produit qui me vient à l'esprit est Gnuplot, mais il y en a beaucoup d'autres. Vous pouvez utiliser Python, mais personnellement je ne le ferais pas.

Je comprends que cette approche est complexe et peut être excessive pour votre problème ou nécessiter des compétences que vous n'avez pas encore acquises. C'est comme ça que je le ferais.

david.pfx
la source
0

Je ne suis pas sûr d'avoir bien saisi la question, mais il semble que vous devriez regrouper les choses dans certaines structures. Dites que si vous utilisez C ++, vous pouvez définir des choses comme:

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

Vous pouvez les instancier au début du programme:

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

Ensuite, vos appels API ressembleront (en supposant que vous ne pouvez pas modifier la signature):

 SomeAPICall( params1.p1, params1.p2 );

Si vous pouvez changer la signature de l'API, alors vous pouvez passer la structure entière:

 SomeAPICall( params1 );

Vous pouvez également regrouper tous les paramètres dans un wrapper plus grand:

struct AllTheParameters
{
    SomeParametersClass      SPC;
    SomeOtherParametersClass SOPC;
};
kebs
la source
0

Je suis surpris que personne d'autre n'ait mentionné cela ...

Tu as dit:

Mon objectif principal est de pouvoir remettre à quelqu'un ma source et de lui faire comprendre ce que je faisais, sans ma contribution.

Alors permettez-moi de dire ceci, la plupart des autres réponses sont sur la bonne voie. Je pense vraiment que les bases de données pourraient vous aider. Mais une autre chose qui vous aidera est le commentaire, les bons noms de variables et la bonne organisation / séparation des préoccupations.

Toutes les autres réponses sont fortement basées sur la technique, mais elles ignorent les principes fondamentaux que la plupart des programmeurs apprennent. Étant donné que vous êtes un ingénieur mécanicien de métier, je suppose que vous n'êtes pas habitué à ce style de documentation.

Commenter et choisir de bons noms de variables succincts aide énormément à la lisibilité. Qu'est-ce qui est plus facile à comprendre?

var x = y + z;

Ou:

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

C'est assez indépendant de la langue. Peu importe la plate-forme, l'IDE, la langue, etc. avec lesquels vous travaillez, une documentation appropriée est le moyen le plus propre et le plus simple de vous assurer que quelqu'un peut comprendre votre code.

Ensuite, il s'agit de gérer ces nombres magiques et ces tonnes de préoccupations, mais je pense que le commentaire de GrandmasterB a assez bien géré cela.

A dessiné
la source