Jak zarządzać bardzo dużym zestawem reguł i magicznych liczb w moim programie?

21

Jestem nieco nowy w programowaniu (z zawodu jestem inżynierem mechanikiem) i podczas mojego przestoju opracowuję mały program, który generuje część (solidworks) na podstawie danych wejściowych od różnych osób z całego zakładu.

Opierając się tylko na kilku danych wejściowych (dokładnie 6), muszę wykonać setki wywołań API, z których każde może zająć nawet kilkanaście parametrów; wszystko wygenerowane przez zestaw reguł, które zebrałem po przeprowadzeniu wywiadu ze wszystkimi, którzy zajmują się tą częścią. Sekcja zasad i parametrów mojego kodu ma 250 linii i rośnie.

Więc jaki jest najlepszy sposób, aby mój kod był czytelny i zarządzalny? Jak podzielić na segmenty wszystkie moje magiczne liczby, wszystkie reguły, algorytmy i części proceduralne kodu? Jak radzić sobie z bardzo szczegółowym i szczegółowym interfejsem API?

Moim głównym celem jest przekazanie komuś mojego źródła i zrozumienie, co robię, bez mojego wkładu.

użytkownik2785724
źródło
7
Czy możesz podać przykłady takich wywołań API?
Robert Harvey
„Wszystkie problemy w informatyce można rozwiązać przez inny poziom pośredni” - David Wheeler
Phil Frost
... z wyjątkiem zbyt wielu poziomów pośrednich :)
Dan Lyons
1
Trudno jest odpowiedzieć na pytanie, nie widząc kodu. Możesz opublikować swój kod na codereview.stackexchange.com i uzyskać porady od innych programistów.
Gilbert Le Blanc

Odpowiedzi:

26

Na podstawie tego, co opisujesz, prawdopodobnie będziesz chciał odkryć wspaniały świat baz danych. Wygląda na to, że wiele magicznych liczb, które opisujesz - szczególnie jeśli są zależne od części - to tak naprawdę dane, a nie kod. Będziesz miał znacznie więcej szczęścia i znacznie łatwiej będzie ci rozszerzyć aplikację na dłuższą metę, jeśli możesz kategoryzować związek danych z częściami i zdefiniować dla niej strukturę bazy danych.

Pamiętaj, że „bazy danych” niekoniecznie oznaczają MySQL lub MS-SQL. Sposób przechowywania danych będzie w dużej mierze zależeć od sposobu korzystania z programu, sposobu pisania itp. Może to oznaczać bazę danych typu SQL lub po prostu sformatowany plik tekstowy.

Grandmaster B.
źródło
7
Zgodził się z kodyfikacją danych w bazie danych, choć wydaje się, że ma większe problemy.
Robert Harvey
Gdybym tworzył program, który składa się z zupełnie różnych części, tak, to byłaby właściwa droga. To tylko jedna część z czterema nieco odmiennymi konfiguracjami. Nigdy nie będzie to wielka sprawa (chyba że zatrudni programistę, aby coś takiego zrobił, w takim przypadku nie ma to znaczenia). Chociaż myślę, że byłoby to wspaniałe doświadczenie edukacyjne po tym, jak skończyłem i chciałem zreformować.
user2785724,
1
Brzmi jak kodowanie miękkie . Bazy danych służą do zmiany stanu. Liczby magiczne nie są z definicji zmienne.
Phil Frost
1
@PhilFrost: Możesz uczynić je niezmiennymi. Po prostu nie pisz do nich po początkowym utworzeniu tabeli.
Robert Harvey
1
@PhilFrost: Cóż, widziałem API, z którym on ma do czynienia. Jest to niezwykłe tylko ze względu na swój rozmiar. Może nie potrzebować bazy danych, chyba że będzie.
Robert Harvey
14

O ile nie spodziewacie się rozszerzenia tego na wiele części, nie chciałbym jeszcze dodawać bazy danych. Posiadanie bazy danych oznacza duży zbiór rzeczy do nauczenia się dla ciebie i więcej rzeczy do zainstalowania, aby działały dla innych osób. Dodanie wbudowanej bazy danych sprawia, że ​​końcowy plik wykonywalny jest przenośny, ale osoba z twoim kodem źródłowym ma teraz jeszcze jedną rzecz do zrobienia.

Myślę, że lista wyraźnie nazwanych stałych i funkcji implementujących reguły bardzo pomoże. Jeśli nadasz wszystkim naturalne nazwy i skupisz się na umiejętnościach programowania , powinieneś być w stanie stworzyć czytelny program.

Idealnie byłoby skończyć z kodem, który mówi:

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

W zależności od tego, jak lokalne są stałe, kusiłoby mnie, aby zadeklarować je w funkcjach, w których są używane, tam gdzie to możliwe. Przydatne jest włączenie:

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

w

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

Daje to w dużej mierze samokontrujący się kod, a także zachęca każdego, kto modyfikuje kod, do nadawania podobnie znaczących nazw dodawanym przez siebie treściom. Uruchomienie lokalne ułatwia także radzenie sobie z całkowitą liczbą stałych, które zgromadzisz. To staje się trochę denerwujące, jeśli musisz przewijać długą listę stałych, aby upewnić się, że wartość jest tą, którą chcesz.

Jedna wskazówka dla imion: umieść najważniejsze słowo po lewej stronie. Może nie jest tak dobrze czytany, ale ułatwia znajdowanie rzeczy. Przez większość czasu patrzysz na miskę i zastanawiasz się nad śrubą, nie patrzysz na śrubę i zastanawiasz się, gdzie ona jest, więc nazwij to SumpBoltThreadPitch, a nie BoltThreadPitchSump. Następnie posortuj listę stałych. Później, aby wyodrębnić wszystkie skoki wątków, możesz uzyskać listę w edytorze tekstu i albo użyć funkcji find, albo użyć narzędzia takiego jak grep, aby zwrócić tylko linie zawierające „ThreadPitch”.

Móż
źródło
1
rozważ także utworzenie płynnego interfejsu
Ian
Oto aktualny wiersz z mojego kodu. Czy ma sens to, co się tutaj dzieje (argumentami są x1, y1, z1, x2, y2, z2 jako podwójne), jeśli wiesz, co oznaczają nazwy zmiennych? .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,
Możesz również użyć tagów z integracją edytora, aby znaleźć stałe.
Phil Frost
3
@ user2785724 To bałagan. Co to robi? Czy tworzy rowek o określonej długości i głębokości? Następnie możesz utworzyć funkcję o nazwie createGroove(length, depth). Musisz zaimplementować funkcje opisujące to, co chcesz osiągnąć, tak jak opisałbyś je inżynierowi mechanicznemu. Na tym właśnie polega umiejętność programowania.
Phil Frost
To jest wywołanie API do narysowania jednej linii w przestrzeni 3D. Każdy z 6 argumentów znajduje się w różnych wierszach programu. Całe API jest szalone. Nie wiedziałam, gdzie zrobić bałagan, więc udało mi się. Gdybyś wiedział, co to było wywołanie interfejsu API i jego argumenty, zobaczyłbyś, jakie były punkty końcowe, używając parametrów, które znasz, i byłbyś w stanie odnieść je z powrotem do części. Jeśli chcesz się zapoznać z SolidWorks, interfejs API jest całkowicie labiryntowy.
user2785724,
4

Myślę, że twoje pytanie ogranicza się do: jak ustrukturyzować obliczenia? Zauważ, że chcesz zarządzać „zestawem reguł”, którymi są kod, oraz „zestawem magicznych liczb”, które są danymi. (Możesz zobaczyć je jako „dane osadzone w kodzie”, ale mimo to są danymi).

Co więcej, uczynienie twojego kodu „zrozumiałym dla innych” jest w rzeczywistości ogólnym celem wszystkich paradygmatów programowania (patrz np. „ Wzorce implementacji ” Kent Beck lub „ Clean Code ” Roberta C. Martina dla autorów oprogramowania, którzy mają ten sam cel jak ty, dla dowolnego programu).

Wszystkie wskazówki zawarte w tych książkach dotyczą twojego pytania. Pozwól mi wyciągnąć kilka wskazówek dotyczących „magicznych liczb” i „zbiorów reguł”:

  1. Użyj nazwanych stałych i wyliczeń, aby zastąpić magiczne liczby

    Przykład stałych :

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

    należy zastąpić nazwaną stałą, aby żadne późniejsze zmiany nie mogły wprowadzić literówki i zepsuć kod, np. poprzez zmianę pierwszego, 0.625ale nie drugiego.

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

    Przykład wyliczeń :

    Wyliczenia mogą pomóc ci zebrać dane, które do siebie należą. Jeśli używasz Java, pamiętaj, że Enums są obiektami; ich elementy mogą przechowywać dane i można zdefiniować metody zwracające wszystkie elementy lub sprawdzić niektóre właściwości. Tutaj Enum jest używane do budowy innego 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
    }
    

    Zaletą jest to, że teraz nikt nie może błędnie zdefiniować części silnika, która nie jest wykonana ze stali lub węgla, i nikt nie może wprowadzić elementu silnika o nazwie „asdfasdf”, tak jak byłoby w przypadku, gdyby był to ciąg znaków sprawdzany pod kątem zawartości.

  2. Wzór Strategia i metody wzorca Fabryka opisać jak hermetyzacji „zasady” i przekazać je do innego obiektu, który korzysta z nich (w przypadku wzorca Factory wykorzystanie jest coś budowę, w przypadku wzoru strategią, użycie jest czymkolwiek chcesz).

    Przykład wzorca metody fabrycznej :

    Wyobraź sobie, że masz dwa typy silników: jeden, w którym każda część musi być połączona ze sprężarką, i jeden, w którym każdą część można dowolnie połączyć z dowolnymi innymi częściami. Zaadaptowano z Wikipedii

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

    A potem w innej klasie:

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

    Interesująca część: teraz Twój konstruktor AssemblyLine jest oddzielony od obsługiwanego typu silnika. Może addEnginemetody wywołują zdalny interfejs API ...

    Przykład wzorca strategii :

    Wzorzec strategii opisuje, jak wprowadzić funkcję do obiektu w celu zmiany jego zachowania. Wyobraźmy sobie, że czasami chcesz wypolerować część, czasem chcesz ją pomalować, a domyślnie chcesz sprawdzić jej jakość. To jest przykład Pythona, zaadaptowany z 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"
    

    Możesz rozwinąć tę funkcję, trzymając listę Akcji, które chcesz wykonać, a następnie wywołując je kolejno z executemetody. Może to uogólnienie można lepiej opisać jako wzorzec Konstruktora , ale hej, nie chcemy być wybredni, prawda? :)

logc
źródło
2

Możesz użyć silnika reguł. Mechanizm reguł zapewnia DSL (język specyficzny dla domeny), który został zaprojektowany do modelowania kryteriów niezbędnych do określonego wyniku w zrozumiały sposób, jak wyjaśniono w tym pytaniu .

W zależności od implementacji silnika reguł reguły można nawet zmienić bez ponownej kompilacji kodu. A ponieważ reguły są napisane w ich własnym, prostym języku, mogą być zmieniane również przez użytkowników.

Jeśli masz szczęście, istnieje gotowy do użycia silnik reguł dla używanego języka programowania.

Minusem jest to, że musisz zapoznać się z silnikiem reguł, co może być trudne, jeśli jesteś początkującym programistą.

zilluss
źródło
1

Moje rozwiązanie tego problemu jest całkiem inne: warstwy, ustawienia i LOP.

Najpierw zawiń interfejs API w warstwie. Znajdź sekwencje wywołań API, które są używane razem i połącz je we własne wywołania API. W końcu nie powinno być żadnych bezpośrednich połączeń z bazowym interfejsem API, tylko połączenia z opakowaniami. Połączenia otoki powinny zacząć wyglądać jak mini-język.

Po drugie, zaimplementuj „menedżera ustawień”. Jest to sposób na dynamiczne kojarzenie nazw z wartościami. Coś takiego. Kolejny mini język.

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

Na koniec zaimplementuj swój własny mini język do wyrażania projektów (jest to programowanie zorientowane na język). Ten język powinien być zrozumiały dla inżynierów i projektantów, którzy wprowadzają zasady i ustawienia. Pierwszym przykładem takiego produktu, który przychodzi na myśl, jest Gnuplot, ale jest wiele innych. Możesz użyć Pythona, chociaż osobiście nie zrobiłbym tego.

Rozumiem, że jest to złożone podejście i może być przesadą w rozwiązywaniu problemów lub wymaganiem umiejętności, które musisz jeszcze zdobyć. Właśnie tak bym to zrobił.

david.pfx
źródło
0

Nie jestem pewien, czy poprawnie otrzymałem pytanie, ale wygląda na to, że powinieneś pogrupować rzeczy w niektórych strukturach. Powiedz, jeśli używasz C ++, możesz zdefiniować takie rzeczy jak:

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

Możesz je utworzyć na początku programu:

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

Wtedy będą wyglądały twoje wywołania API (zakładając, że nie możesz zmienić podpisu):

 SomeAPICall( params1.p1, params1.p2 );

Jeśli możesz zmienić podpis API, możesz przekazać całą strukturę:

 SomeAPICall( params1 );

Możesz również pogrupować wszystkie parametry w większe opakowanie:

struct AllTheParameters
{
    SomeParametersClass      SPC;
    SomeOtherParametersClass SOPC;
};
kebs
źródło
0

Dziwi mnie, że nikt inny nie wspomniał o tym ...

Powiedziałeś:

Moim głównym celem jest przekazanie komuś mojego źródła i zrozumienie, co robię, bez mojego wkładu.

Powiem więc tak, większość innych odpowiedzi jest na dobrej drodze. Zdecydowanie uważam, że bazy danych mogą ci pomóc. Ale kolejną rzeczą, która ci pomoże, jest komentowanie, dobre nazwy zmiennych oraz właściwa organizacja / separacja problemów.

Wszystkie pozostałe odpowiedzi są w dużej mierze oparte na zagadnieniach technicznych, ale ignorują podstawy, których uczy większość programistów. Ponieważ jesteś mechanikiem z zawodu, zgaduję, że nie jesteś przyzwyczajony do tego stylu dokumentacji.

Komentowanie i wybór dobrych, zwięzłych nazw zmiennych ogromnie pomaga w czytelności. Co jest łatwiejsze do zrozumienia?

var x = y + z;

Lub:

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

To jest dość niezależne od języka. Bez względu na platformę, IDE, język itp., Właściwa dokumentacja jest najczystszym i najłatwiejszym sposobem na upewnienie się, że ktoś zrozumie Twój kod.

Następnie poradzę sobie z zarządzaniem tymi magicznymi liczbami i mnóstwem obaw, ale myślę, że komentarz GrandmasterB poradził sobie z tym całkiem nieźle.

Drew
źródło