Wie verwalte ich einen sehr großen Satz von Regeln und magischen Zahlen in meinem Programm?


21

Ich bin etwas neu in der Programmierung (ich bin von Beruf Maschinenbauingenieur) und entwickle während meiner Ausfallzeit ein kleines Programm, das einen (Solidworks-) Teil auf der Grundlage von Eingaben verschiedener Mitarbeiter aus der gesamten Anlage generiert.

Basierend auf nur wenigen Eingaben (6 um genau zu sein) muss ich Hunderte von API-Aufrufen durchführen, die jeweils bis zu einem Dutzend Parameter aufnehmen können. Alles wird durch eine Reihe von Regeln generiert, die ich nach einem Interview mit allen Personen, die mit dem Teil befasst sind, gesammelt habe. Der Regel- und Parameterbereich meines Codes ist 250 Zeilen lang und wächst.

Was ist der beste Weg, um meinen Code lesbar und verwaltbar zu halten? Wie unterteile ich alle meine magischen Zahlen, alle Regeln, Algorithmen und prozeduralen Teile des Codes? Wie gehe ich mit einer sehr ausführlichen und detaillierten API um?

Mein Hauptziel ist es, jemandem meine Quelle zu geben und ihn verstehen zu lassen, was ich getan habe, ohne meinen Beitrag zu leisten.


7
Können Sie einige Beispiele für diese API-Aufrufe bereitstellen?
Robert Harvey


"Alle Probleme in der Informatik können durch eine andere Indirektionsebene gelöst werden" - David Wheeler
Phil Frost

... außer zu viele Ebenen der Indirektion :)
Dan Lyons

1
Es ist schwierig, Ihre Frage zu beantworten, ohne Ihren Code zu sehen. Sie können Ihren Code auf codereview.stackexchange.com veröffentlichen und sich von anderen Programmierern beraten lassen.
Gilbert Le Blanc

Antworten:


26

Basierend auf Ihrer Beschreibung werden Sie wahrscheinlich die wunderbare Welt der Datenbanken erkunden wollen. Es hört sich so an, als wären viele der von Ihnen beschriebenen magischen Zahlen - insbesondere wenn sie teileabhängig sind - wirklich Daten, kein Code. Sie werden viel mehr Glück haben und es auf lange Sicht viel einfacher finden, die Anwendung zu erweitern, wenn Sie kategorisieren können, wie sich die Daten auf die Teile beziehen, und eine Datenbankstruktur dafür definieren können.

Denken Sie daran, dass 'Datenbanken' nicht unbedingt MySQL oder MS-SQL bedeuten müssen. Wie Sie die Daten speichern, hängt stark davon ab, wie das Programm verwendet wird, wie Sie es schreiben usw. Es kann sich um eine SQL-Datenbank oder einfach um eine formatierte Textdatei handeln.


7
Ich bin damit einverstanden, die Daten in einer Datenbank zu kodieren, obwohl es den Anschein hat, dass er größere Probleme hat.
Robert Harvey

Wenn ich ein Programm erstellen würde, das völlig andere Teile enthält, wäre dies der richtige Weg. Es ist jedoch nur ein Teil mit vier leicht unterschiedlichen Konfigurationen. Es wird nie eine große Sache sein (es sei denn, sie beauftragen einen Entwickler damit, so etwas zu machen. In diesem Fall spielt es keine Rolle). Obwohl ich denke, es wäre eine großartige Lernerfahrung, nachdem ich fertig war und umgestalten wollte.
user2785724

1
Klingt nach Soft Coding . Datenbanken sind für den veränderlichen Zustand. Magische Zahlen sind per Definition nicht veränderbar.
Phil Frost

1
@PhilFrost: Sie können sie unveränderlich machen. Schreiben Sie ihnen einfach nach der ersten Tabellenerstellung nicht mehr.
Robert Harvey

1
@PhilFrost: Nun, ich habe jetzt die API gesehen , mit der er es zu tun hat. Es ist nur für seine bloße Größe bemerkenswert. Möglicherweise benötigt er überhaupt keine Datenbank, es sei denn, er benötigt eine.
Robert Harvey

14

Sofern Sie nicht damit rechnen, dies auf mehrere Teile auszudehnen, würde ich nur ungern eine Datenbank hinzufügen. Wenn Sie über eine Datenbank verfügen, müssen Sie eine Menge lernen und mehr installieren, damit sie für andere Benutzer funktioniert. Durch das Hinzufügen einer eingebetteten Datenbank bleibt die endgültige ausführbare Datei portabel, aber jemand mit Ihrem Quellcode muss jetzt noch eine Sache erledigen.

Ich denke, eine Liste klar benannter Konstanten und regelimplementierender Funktionen wird viel helfen. Wenn Sie alle natürlichen Namen vergeben und sich auf gebildete Programmiertechniken konzentrieren , sollten Sie in der Lage sein, ein lesbares Programm zu erstellen.

Im Idealfall erhalten Sie folgenden Code:

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

Abhängig davon, wie lokal die Konstanten sind, wäre ich versucht, sie in den Funktionen zu deklarieren, in denen sie verwendet werden, wo dies möglich ist. Das Wenden ist ganz nützlich:

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

in

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

Das gibt Ihnen weitgehend selbstdokumentierenden Code und ermutigt jeden, der den Code ändert, dem, was er hinzufügt, ähnlich aussagekräftige Namen zu geben. Durch das Starten von local wird es auch einfacher, mit der Gesamtzahl der Konstanten umzugehen, die Sie akkumulieren. Es wird etwas ärgerlich, wenn Sie eine lange Liste von Konstanten durchblättern müssen, um sicherzustellen, dass der Wert dem gewünschten Wert entspricht.

Ein Tipp für Namen: Schreiben Sie das wichtigste Wort nach links. Es liest sich vielleicht nicht ganz so gut, erleichtert aber das Auffinden. Meistens schauen Sie auf einen Sumpf und fragen sich, wo der Bolzen ist, nicht auf einen Bolzen und fragen sich, wo er sich befindet. Nennen Sie ihn also SumpBoltThreadPitch, nicht BoltThreadPitchSump. Sortieren Sie dann die Liste der Konstanten. Um später alle Gewindesteigungen zu extrahieren, können Sie die Liste in einem Texteditor abrufen und entweder die Suchfunktion verwenden oder ein Tool wie grep verwenden, um nur die Zeilen zurückzugeben, die "ThreadPitch" enthalten.


1
Ian,

Hier ist eine aktuelle Zeile aus meinem Code. Ist es sinnvoll, was hier vor sich geht (Argumente sind x1, y1, z1, x2, y2, z2 als double), wenn Sie wissen, was die Variablennamen bedeuten? .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

Sie können auch ctags mit Editor-Integration verwenden , um die Konstanten zu finden.
Phil Frost

3
@ user2785724 Das ist ein Durcheinander. Was macht es? Macht es eine Nut von einer bestimmten Länge und Tiefe? Dann könnten Sie eine Funktion erstellen, die aufgerufen wird createGroove(length, depth). Sie müssen Funktionen implementieren, die beschreiben, was Sie erreichen möchten, wie Sie es einem Maschinenbauer beschreiben würden. Darum geht es beim Lesen und Schreiben.
Phil Frost

Dies ist der API-Aufruf zum Zeichnen einer Linie im 3D-Raum. Jedes der 6 Argumente steht in einer anderen Zeile im Programm. Die ganze API ist verrückt. Ich wusste nicht, wo ich das Chaos anrichten sollte, also habe ich es dort gemacht. Wenn Sie den API-Aufruf und seine Argumente kennen, können Sie anhand der Ihnen vertrauten Parameter die Endpunkte ermitteln und auf das Teil zurückführen. Wenn Sie sich mit SolidWorks vertraut machen möchten, ist die API ein absolutes Labyrinth.
user2785724

4

Ich denke, Ihre Frage reduziert sich auf: Wie strukturiere ich eine Berechnung? Beachten Sie, dass Sie "eine Reihe von Regeln" verwalten möchten, bei denen es sich um Code handelt, und "eine Reihe von magischen Zahlen", bei denen es sich um Daten handelt. (Sie können sie als "in Ihren Code eingebettete Daten" betrachten, aber es handelt sich trotzdem um Daten).

Darüber hinaus ist es das allgemeine Ziel aller Programmierparadigmen, Ihren Code für andere "verständlich" zu machen (siehe z. B. " Implementation Patterns " von Kent Beck oder " Clean Code " von Robert C. Martin für Autoren von Software, die dasselbe Ziel angeben wie Sie, für jedes Programm).

Alle Hinweise in diesen Büchern gelten für Ihre Frage. Lassen Sie mich einige Hinweise speziell für "magische Zahlen" und "Regelsätze" extrahieren:

  1. Verwenden Sie benannte Konstanten und Aufzählungen, um magische Zahlen zu ersetzen

    Beispiel für Konstanten :

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

    sollte durch eine benannte Konstante ersetzt werden, damit keine späteren Änderungen einen Tippfehler verursachen und Ihren Code beschädigen können, z. B. durch Ändern der ersten, 0.625aber nicht der zweiten.

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

    Beispiel für Aufzählungen :

    Durch Aufzählungen können Sie zusammengehörige Daten zusammenstellen. Wenn Sie Java verwenden, denken Sie daran, dass Enums Objekte sind. Ihre Elemente können Daten enthalten, und Sie können Methoden definieren, die alle Elemente zurückgeben, oder einige Eigenschaften überprüfen. Hier wird eine Aufzählung verwendet, um eine andere Aufzählung zu erstellen:

    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
    }
    

    Der Vorteil ist: Jetzt kann niemand ein EnginePart falsch definieren, das nicht aus Stahl oder Kohlenstoff besteht, und niemand kann ein EnginePart mit dem Namen "asdfasdf" einführen, wie es der Fall wäre, wenn es eine Zeichenfolge wäre, die auf Inhalt überprüft würde.

  2. Das Strategy - Muster und das Factory - Methodenmuster beschreiben, wie "Regeln" gekapselt und an ein anderes Objekt übergeben werden, das sie verwendet (im Falle des Factory - Musters baut die Verwendung etwas auf; im Falle des Strategy - Musters das Nutzung ist was immer Sie wollen).

    Beispiel für ein Factory-Methodenmuster :

    Stellen Sie sich vor , Sie haben zwei Arten von Motoren: ein , wobei jeder Teil hat mit dem Kompressor verbunden ist, und eine , wo jeder Teil kann frei unabhängig von anderen Teilen verbunden werden. Aus Wikipedia übernommen

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

    Und dann in einer anderen Klasse:

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

    Der interessante Teil ist: Jetzt ist Ihr Fertigungslinienkonstruktor von der Art der zu bearbeitenden Engine getrennt. Vielleicht addEnginerufen die Methoden eine entfernte API auf ...

    Beispiel für ein Strategiemuster :

    Das Strategiemuster beschreibt, wie Sie eine Funktion in ein Objekt einfügen, um dessen Verhalten zu ändern. Stellen Sie sich vor, Sie möchten ein Teil manchmal polieren, manchmal malen und standardmäßig die Qualität überprüfen. Dies ist ein Python-Beispiel, das von Stack Overflow übernommen wurde

    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"
    

    Sie können dies erweitern, um eine Liste der Aktionen zu speichern, die Sie ausführen möchten, und diese dann nacheinander von der executeMethode aus aufrufen . Vielleicht könnte diese Verallgemeinerung besser als Builder-Muster beschrieben werden , aber hey, wir wollen nicht wählerisch werden, oder? :)


2

Möglicherweise möchten Sie eine Regelengine verwenden. Mit einer Regel-Engine erhalten Sie eine DSL (Domain Specific Language), mit der die für ein bestimmtes Ergebnis erforderlichen Kriterien auf verständliche Weise modelliert werden können, wie in dieser Frage erläutert .

Abhängig von der Implementierung der Regelengine können die Regeln sogar geändert werden, ohne den Code neu zu kompilieren. Und weil die Regeln in ihrer eigenen, einfachen Sprache geschrieben sind, können sie auch von den Benutzern geändert werden.

Wenn Sie Glück haben, gibt es eine einsatzbereite Regel-Engine für die von Ihnen verwendete Programmiersprache.

Der Nachteil ist, dass Sie sich mit einer Regel-Engine vertraut machen müssen, die für Programmieranfänger schwierig sein kann.


1

Meine Lösung für dieses Problem ist ganz anders: Ebenen, Einstellungen und LOP.

Wickeln Sie zuerst die API in eine Ebene. Suchen Sie nach Sequenzen von API-Aufrufen, die zusammen verwendet werden, und kombinieren Sie sie zu Ihren eigenen API-Aufrufen. Schließlich sollten keine direkten Aufrufe an die zugrunde liegende API erfolgen, sondern nur Aufrufe an Ihre Wrapper. Die Wrapper-Aufrufe sollten wie eine Mini-Sprache aussehen.

Zweitens implementieren Sie einen "Einstellungsmanager". Auf diese Weise können Namen dynamisch mit Werten verknüpft werden. Etwas wie das. Eine weitere Mini-Sprache.

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

Implementieren Sie schließlich Ihre eigene Minisprache, in der Sie Designs ausdrücken können (dies ist sprachorientierte Programmierung). Diese Sprache sollte für die Ingenieure und Designer verständlich sein, die die Regeln und Einstellungen beitragen. Das erste Beispiel für ein solches Produkt ist Gnuplot, aber es gibt viele andere. Sie könnten Python verwenden, obwohl ich persönlich nicht würde.

Ich verstehe, dass dies ein komplexer Ansatz ist und möglicherweise zu viel für Ihr Problem bedeutet oder Fähigkeiten erfordert, die Sie noch nicht erworben haben. Es ist nur so, wie ich es machen würde.


0

Ich bin mir nicht sicher, ob ich die Frage richtig verstanden habe, aber es hört sich so an, als ob Sie die Dinge in einigen Strukturen gruppieren sollten. Angenommen, Sie verwenden C ++ und können Folgendes definieren:

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

Sie können diese zu Beginn des Programms instanziieren:

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

Dann sehen Ihre API-Aufrufe wie folgt aus (vorausgesetzt, Sie können die Signatur nicht ändern):

 SomeAPICall( params1.p1, params1.p2 );

Wenn Sie die Signatur der API ändern können, können Sie die gesamte Struktur übergeben:

 SomeAPICall( params1 );

Sie können auch alle Parameter in einem größeren Wrapper gruppieren:

struct AllTheParameters
{
    SomeParametersClass      SPC;
    SomeOtherParametersClass SOPC;
};

0

Ich bin überrascht, dass das noch niemand erwähnt hat ...

Du sagtest:

Mein Hauptziel ist es, jemandem meine Quelle zu geben und ihn verstehen zu lassen, was ich getan habe, ohne meinen Beitrag zu leisten.

Lassen Sie mich das sagen, die meisten anderen Antworten sind auf dem richtigen Weg. Ich denke definitiv, dass Datenbanken Ihnen helfen könnten. Aber eine andere Sache, die Ihnen helfen wird, ist das Kommentieren, gute Variablennamen und die richtige Organisation / Trennung von Bedenken.

Alle anderen Antworten basieren stark auf technischen Aspekten, ignorieren jedoch die Grundlagen, die die meisten Programmierer lernen. Da Sie von Beruf ein Mech-Ingenieur sind, sind Sie an diese Art der Dokumentation vermutlich nicht gewöhnt.

Das Kommentieren und Auswählen guter, prägnanter Variablennamen trägt immens zur Lesbarkeit bei. Was ist leichter zu verstehen?

var x = y + z;

Oder:

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

Das ist ziemlich sprachunabhängig. Unabhängig davon, mit welcher Plattform, IDE, Sprache usw. Sie arbeiten, ist eine ordnungsgemäße Dokumentation der sauberste und einfachste Weg, um sicherzustellen, dass jemand Ihren Code verstehen kann.

Als nächstes geht es darum, diese magischen Zahlen und Unmengen von Bedenken in den Griff zu bekommen, aber ich denke, GrandmasterBs Kommentar hat das ziemlich gut gemeistert.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.