Come posso gestire un set molto grande di regole e numeri magici nel mio programma?


21

Sono un po 'nuovo nella programmazione (sono un ingegnere meccanico di professione) e sto sviluppando un piccolo programma durante i miei tempi di inattività che genera una parte (solidworks) basata sul contributo di varie persone provenienti da tutto l'impianto.

Sulla base di pochi input (6 per l'esattezza), devo effettuare centinaia di chiamate API che possono richiedere fino a una dozzina di parametri ciascuna; tutto generato da una serie di regole che ho raccolto dopo aver intervistato tutti coloro che gestiscono la parte. La sezione delle regole e dei parametri del mio codice è di 250 righe e in crescita.

Quindi, qual è il modo migliore per mantenere il mio codice leggibile e gestibile? Come posso compartimentare tutti i miei numeri magici, tutte le regole, gli algoritmi e le parti procedurali del codice? Come posso gestire un'API molto dettagliata e dettagliata?

Il mio obiettivo principale è quello di essere in grado di consegnare a qualcuno la mia fonte e fargli capire cosa stavo facendo, senza il mio contributo.


7
Potete fornire alcuni esempi di queste chiamate API?
Robert Harvey,


"Tutti i problemi di informatica possono essere risolti da un altro livello di riferimento indiretto" - David Wheeler
Phil Frost,

... tranne troppi livelli di riferimento indiretto :)
Dan Lyons,

1
È difficile rispondere alla tua domanda senza vedere il tuo codice. Puoi pubblicare il tuo codice su codereview.stackexchange.com e ottenere consigli da altri programmatori.
Gilbert Le Blanc,

Risposte:


26

Sulla base di ciò che descrivi, probabilmente vorrai esplorare il meraviglioso mondo dei database. Sembra che molti dei numeri magici che descrivi, in particolare se dipendono in parte, sono in realtà dati, non codice. Avrai molta più fortuna e troverai molto più facile estendere l'applicazione a lungo termine, se puoi classificare il modo in cui i dati si relazionano alle parti e definire una struttura di database per essa.

Tieni presente che "database" non significa necessariamente MySQL o MS-SQL. Il modo in cui archiviate i dati dipenderà molto da come viene utilizzato il programma, da come lo state scrivendo, ecc. Può significare un database di tipo SQL o può semplicemente significare un file di testo formattato.


7
Concordato con la codifica dei dati in un database, anche se sembra che abbia problemi più grandi.
Robert Harvey,

Se stessi creando un programma che crea parti completamente diverse, sì, questa sarebbe la strada da percorrere. Tuttavia, è solo una parte con quattro configurazioni leggermente diverse. Non sarà mai una cosa enorme (a meno che non assumano uno sviluppatore per fare qualcosa di simile, nel qual caso non importa). Anche se, immagino che sarebbe una grande esperienza di apprendimento dopo che ho finito e volevo fare il refactoring.
user2785724,

1
Sembra una codifica soft . I database sono per stato mutabile. I numeri magici non sono mutabili, per definizione.
Phil Frost,

1
@PhilFrost: puoi renderli immutabili. Basta non scrivere a loro dopo la creazione della tabella iniziale.
Robert Harvey,

1
@PhilFrost: Bene, ora ho visto l'API con cui ha a che fare. È notevole solo per le sue dimensioni pure. Potrebbe non aver bisogno di un database, a meno che non lo faccia.
Robert Harvey,

14

Se non prevedi di estenderlo a più parti, sarei riluttante ad aggiungere un database ancora. Avere un database significa una grande pila di cose da imparare per te e più cose da installare per farlo funzionare per altre persone. L'aggiunta di un database incorporato mantiene portatile l'eseguibile finale, ma ora qualcuno con il tuo codice sorgente ha ancora una cosa per iniziare a lavorare.

Penso che un elenco di costanti chiaramente definite e funzioni di implementazione delle regole aiuteranno molto. Se dai a tutto nomi naturali e ti concentri su tecniche di programmazione letteraria , dovresti essere in grado di creare un programma leggibile.

Idealmente, finirai con un codice che dice:

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

A seconda di quanto sono locali le costanti, sarei tentato di dichiararle nelle funzioni in cui sono usate, ove possibile. È abbastanza utile girare:

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

in

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

Questo ti dà in gran parte un codice auto-documentante e incoraggia anche chiunque modifichi il codice a dare nomi altrettanto significativi a ciò che aggiungono. L'avvio locale consente inoltre di gestire più facilmente il numero totale di costanti che accumulerai. Diventa un po 'fastidioso se devi continuare a scorrere un lungo elenco di costanti per assicurarti che il valore sia quello che desideri.

Un consiglio per i nomi: metti la parola più importante a sinistra. Potrebbe non leggere altrettanto bene, ma rende le cose più facili da trovare. La maggior parte delle volte stai guardando un pozzetto e ti chiedi del bullone, non guardi un bullone e ti chiedi dove lo fa, quindi chiamalo SumpBoltThreadPitch non BoltThreadPitchSump. Quindi ordinare l'elenco delle costanti. Successivamente, per estrarre tutti i passi del thread è possibile ottenere l'elenco in un editor di testo e utilizzare la funzione find oppure utilizzare uno strumento come grep per restituire solo le righe che contengono "ThreadPitch".


1
considera anche la creazione di un'interfaccia fluida
Ian

Ecco una riga effettiva dal mio codice. Ha senso cosa sta succedendo qui (gli argomenti sono x1, y1, z1, x2, y2, z2 come doppio), se sapessi cosa significano i nomi delle variabili? .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,

Puoi anche usare i tag con l' integrazione dell'editor per trovare le costanti.
Phil Frost,

3
@ user2785724 È un casino. Cosa sta facendo? Sta creando un solco di una lunghezza e profondità particolare? Quindi è possibile creare una funzione chiamata createGroove(length, depth). È necessario implementare funzioni che descrivono ciò che si desidera realizzare, come si descriverebbe a un ingegnere meccanico. Ecco di cosa tratta la programmazione alfabetica.
Phil Frost,

Questa è la chiamata API per disegnare una linea nello spazio 3d. Ciascuno dei 6 argomenti si trova su righe diverse nel programma. L'intera API è pazza. Non sapevo dove fare il casino, quindi l'ho fatto lì. Se sapessi qual era la chiamata API e i suoi argomenti, vedresti quali erano gli endpoint, usando i parametri che ti erano familiari, e saresti in grado di collegarlo alla parte. Se si desidera acquisire familiarità con SolidWorks, l'API è assolutamente labirintica.
user2785724,

4

Penso che la tua domanda si riduca a: come strutturare un calcolo? Si noti che si desidera gestire "un insieme di regole", che sono codice, e "un insieme di numeri magici", che sono dati. (Puoi vederli come "dati incorporati nel tuo codice", ma sono comunque dati).

Inoltre, rendere il tuo codice "comprensibile agli altri" è in realtà l'obiettivo generale di tutti i paradigmi di programmazione (vedi ad esempio " Schemi di implementazione " di Kent Beck, o " Codice pulito " di Robert C. Martin per gli autori di software che affermano lo stesso obiettivo come te, per qualsiasi programma).

Tutti i suggerimenti in questi libri si applicheranno alla tua domanda. Permettetemi di estrarre alcuni suggerimenti specifici per "numeri magici" e "insiemi di regole":

  1. Usa costanti ed enumerazioni nominate per sostituire i numeri magici

    Esempio di costanti :

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

    deve essere sostituito con una costante denominata in modo che nessuna modifica successiva possa introdurre un errore di battitura e violare il codice, ad esempio modificando il primo 0.625ma non il secondo.

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

    Esempio di enumerazioni :

    Le enumerazioni possono aiutarti a mettere insieme i dati che appartengono insieme. Se stai usando Java, ricorda che gli Enum sono oggetti; i loro elementi possono contenere dati e puoi definire metodi che restituiscono tutti gli elementi o controllare alcune proprietà. Qui un Enum viene utilizzato nella costruzione di un altro 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
    }
    

    Il vantaggio è: ora nessuno può erroneamente definire un EnginePart che non sia fatto di acciaio o carbonio, e nessuno può introdurre un EnginePart chiamato "asdfasdf", come sarebbe se fosse una stringa che verrebbe verificata sul contenuto.

  2. Il modello di strategia e il metodo di metodo Factory descrivono come incapsulare "regole" e passarle a un altro oggetto che le utilizza (nel caso del modello Factory, l'utilizzo sta costruendo qualcosa; nel caso del modello Strategia, il l'utilizzo è quello che vuoi).

    Esempio di modello del metodo Factory :

    Immagina di avere due tipi di motori: uno in cui ogni parte deve essere collegata al compressore e una in cui ogni parte può essere liberamente connessa a qualunque altra parte. Adattato da 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();
        }
    }
    

    E poi in un'altra classe:

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

    La parte interessante è: ora il costruttore AssemblyLine è separato dal tipo di motore che sta gestendo. Forse i addEnginemetodi chiamano un'API remota ...

    Esempio di modello di strategia :

    Il modello di strategia descrive come introdurre una funzione in un oggetto per modificarne il comportamento. Immaginiamo che a volte vuoi lucidare una parte, a volte vuoi dipingerla e, di default, vuoi rivederne la qualità. Questo è un esempio di Python, adattato da 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"
    

    È possibile espandere questo per contenere un elenco di azioni che si desidera eseguire e quindi chiamarle a turno dal executemetodo. Forse questa generalizzazione potrebbe essere meglio descritta come un modello Builder , ma ehi, non vogliamo essere pignoli, vero? :)


2

Potresti voler usare un motore di regole. Un motore di regole ti fornisce un DSL (Domain Specific Language) progettato per modellare i criteri necessari per un determinato risultato in modo comprensibile, come spiegato in questa domanda .

A seconda dell'implementazione del motore delle regole, le regole possono anche essere modificate senza ricompilare il codice. E poiché le regole sono scritte nella loro lingua semplice e semplice, possono essere modificate anche dagli utenti.

Se sei fortunato c'è un motore di regole pronto per l'uso per il linguaggio di programmazione che stai utilizzando.

Il rovescio della medaglia è che devi conoscere un motore di regole che potrebbe essere difficile se sei un principiante della programmazione.


1

La mia soluzione a questo problema è abbastanza diversa: livelli, impostazioni e LOP.

Innanzitutto avvolgere l'API in un livello. Trova sequenze di chiamate API utilizzate insieme e combinale nelle tue chiamate API. Alla fine non dovrebbero esserci chiamate dirette all'API sottostante, ma solo chiamate ai wrapper. Le chiamate del wrapper dovrebbero iniziare ad apparire come una mini lingua.

In secondo luogo, implementare un "gestore delle impostazioni". Questo è un modo per associare nomi e valori in modo dinamico. Qualcosa come questo. Un'altra mini lingua.

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

Infine, implementa il tuo mini linguaggio in cui esprimere i progetti (questa è la programmazione orientata al linguaggio). Questo linguaggio dovrebbe essere comprensibile agli ingegneri e ai progettisti che contribuiscono alle regole e alle impostazioni. Il primo esempio di un prodotto simile che viene in mente è Gnuplot, ma ce ne sono molti altri. Potresti usare Python, anche se personalmente non lo farei.

Comprendo che si tratta di un approccio complesso e potrebbe essere eccessivo per il tuo problema o richiedere competenze che non hai ancora acquisito. È proprio come lo farei.


0

Non sono sicuro di aver ricevuto la domanda correttamente, ma sembra che dovresti raggruppare le cose in alcune strutture. Di 'se stai usando C ++, puoi definire cose come:

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

Puoi istanziarli all'inizio del programma:

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

Quindi le tue chiamate API appariranno (supponendo che non puoi cambiare la firma):

 SomeAPICall( params1.p1, params1.p2 );

Se puoi modificare la firma dell'API, puoi passare l'intera struttura:

 SomeAPICall( params1 );

Puoi anche raggruppare tutti i parametri in un wrapper più grande:

struct AllTheParameters
{
    SomeParametersClass      SPC;
    SomeOtherParametersClass SOPC;
};

0

Sono sorpreso che nessun altro abbia menzionato questo ...

Tu hai detto:

Il mio obiettivo principale è quello di essere in grado di consegnare a qualcuno la mia fonte e fargli capire cosa stavo facendo, senza il mio contributo.

Quindi, lasciatemelo dire, la maggior parte delle altre risposte sono sulla buona strada. Penso sicuramente che i database potrebbero aiutarti. Ma un'altra cosa che ti aiuterà è il commento, i nomi di variabili buone e la corretta organizzazione / separazione delle preoccupazioni.

Tutte le altre risposte sono fortemente tecniche, ma ignorano i fondamenti che la maggior parte dei programmatori impara. Dato che sei un mech engie di professione, suppongo che tu non sia abituato a questo stile di documentazione.

Commentare e scegliere nomi variabili buoni e concisi aiuta immensamente con la leggibilità. Quale è più facile da capire?

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

Questo è abbastanza indipendente dalla lingua. Indipendentemente dalla piattaforma, dall'IDE, dalla lingua, ecc. Con cui stai lavorando, una documentazione adeguata è il modo più semplice e pulito per assicurarti che qualcuno possa capire il tuo codice.

Successivamente si passa alla gestione di quei numeri magici e tonnellate di preoccupazioni, ma penso che il commento di GrandmasterB lo abbia gestito abbastanza bene.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.