Como gerencio um conjunto muito grande de regras e números mágicos no meu programa?


21

Sou um pouco novo em programação (sou engenheiro mecânico de profissão) e estou desenvolvendo um pequeno programa durante meu tempo de inatividade que gera uma peça (solidworks) com base nas informações de várias pessoas de toda a planta.

Com base em apenas algumas entradas (6 para ser exato), preciso fazer centenas de chamadas de API que podem levar até uma dúzia de parâmetros cada; tudo gerado por um conjunto de regras que reuni após entrevistar todos que lidam com a parte. A seção de regras e parâmetros do meu código tem 250 linhas e está crescendo.

Então, qual é a melhor maneira de manter meu código legível e gerenciável? Como compartimentalizo todos os meus números mágicos, todas as regras, algoritmos e partes processuais do código? Como faço para lidar com uma API muito detalhada e granular?

Meu principal objetivo é poder entregar a alguém a minha fonte e fazer com que ela entenda o que eu estava fazendo, sem a minha contribuição.


7
Você pode fornecer alguns exemplos dessas chamadas de API?
Robert Harvey


"Todos os problemas em ciência da computação podem ser resolvidos por outro nível de indireção" - David Wheeler
Phil Frost

... exceto demasiados níveis de engano :)
Dan Lyons

1
É difícil responder sua pergunta sem ver seu código. Você pode postar seu código em codereview.stackexchange.com e obter conselhos de outros programadores.
Gilbert Le Blanc

Respostas:


26

Com base no que você descreve, você provavelmente vai querer explorar o maravilhoso mundo dos bancos de dados. Parece que muitos dos números mágicos que você descreve - principalmente se dependem de parte - são realmente dados, não código. Você terá muito mais sorte e achará muito mais fácil estender o aplicativo a longo prazo, se puder categorizar como os dados se relacionam com as partes e definir uma estrutura de banco de dados para ele.

Lembre-se de que 'bancos de dados' não significam necessariamente MySQL ou MS-SQL. Como você armazena os dados dependerá muito de como o programa é usado, como você está gravando etc. Isso pode significar um banco de dados do tipo SQL ou simplesmente um arquivo de texto formatado.


7
Concordou em codificar os dados em um banco de dados, embora pareça que ele tenha problemas maiores.
Robert Harvey

Se eu estivesse criando um programa que cria partes completamente diferentes, sim, esse seria o caminho a seguir. É apenas uma parte com quatro configurações ligeiramente diferentes, no entanto. Nunca será uma coisa enorme (a menos que eles contratem um desenvolvedor para fazer algo parecido, caso em que não importa). Embora, eu acho que seria uma ótima experiência de aprendizado depois que eu terminasse e quisesse refatorar.
user2785724

1
Soa como codificação suave . Os bancos de dados são para estado mutável. Os números mágicos não são mutáveis, por definição.
Phil Frost

1
@ PhilFrost: Você pode torná-los imutáveis. Apenas não escreva para eles após a criação da tabela inicial.
Robert Harvey

1
@ PhilFrost: Bem, agora eu vi a API com a qual ele está lidando. É notável apenas por seu tamanho. Ele pode não precisar de um banco de dados, a menos que precise.
Robert Harvey

14

A menos que você preveja estender isso para várias partes, eu relutaria em adicionar um banco de dados ainda. Ter um banco de dados significa uma grande pilha de coisas para aprender e mais coisas para instalar para que funcione para outras pessoas. Adicionar um banco de dados incorporado mantém o executável final portátil, mas alguém com seu código-fonte agora tem mais uma coisa para começar a trabalhar.

Eu acho que uma lista de constantes claramente nomeadas e funções de implementação de regras ajudará muito. Se você der nomes naturais a tudo e se concentrar nas técnicas de programação alfabetizada, poderá criar um programa legível.

Idealmente, você terminará com o código que diz:

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

Dependendo de quão local são as constantes, ficaria tentado a declará-las nas funções em que são usadas sempre que possível. É bastante útil ativar:

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

para dentro

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

Isso fornece um código amplamente auto-documentado e também incentiva qualquer pessoa que modifica o código a atribuir nomes com significado semelhante ao que eles adicionam. Iniciar localmente também facilita lidar com o número total de constantes que você acumulará. Fica um pouco chato se você precisar continuar percorrendo uma longa lista de constantes para garantir que o valor seja o desejado.

Uma dica para nomes: coloque a palavra mais importante à esquerda. Pode não ser tão bom assim, mas facilita encontrar as coisas. Na maioria das vezes, você está olhando para um poço e se perguntando sobre o parafuso, sem olhar para um parafuso e se perguntando onde ele funciona. Então, chame-o de SumpBoltThreadPitch e não BoltThreadPitchSump. Em seguida, classifique a lista de constantes. Posteriormente, para extrair todos os arremessos de encadeamento, você pode obter a lista em um editor de texto e usar a função find ou usar uma ferramenta como grep para retornar somente as linhas que contêm "ThreadPitch".


1
também considerar a criação de uma interface fluente
Ian

Aqui está uma linha real do meu código. Faz sentido o que está acontecendo aqui (argumentos são x1, y1, z1, x2, y2, z2 como duplo), se você soubesse o que os nomes das variáveis ​​significavam? .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

Você também pode usar ctags com integração de editor para encontrar as constantes.
Phil Frost

3
@ user2785724 Isso é uma bagunça. O que isso está fazendo? Está fazendo um sulco de um comprimento e profundidade específicos? Então você pode criar uma função chamada createGroove(length, depth). Você precisa implementar funções que descrevam o que você deseja realizar como as descreveria a um engenheiro mecânico. É disso que trata a programação alfabetizada.
Phil Frost

Essa é a chamada da API para desenhar uma linha no espaço 3D. Cada um dos 6 argumentos está em linhas diferentes no programa. Toda a API é louca. Eu não sabia onde fazer a bagunça, então cheguei lá. Se você soubesse qual era a chamada da API e seus argumentos, veria quais eram os pontos de extremidade, usando parâmetros familiares e poderá relacioná-la de volta à peça. Se você deseja se familiarizar com o SolidWorks, a API é absolutamente labiríntica.
user2785724

4

Acho que sua pergunta se reduz a: como estruturar uma computação?Observe que você deseja gerenciar "um conjunto de regras", que são códigos, e "um conjunto de números mágicos", que são dados. (Você pode vê-los como "dados incorporados no seu código", mas, no entanto, são dados).

Além disso, tornar seu código "compreensível para os outros" é, de fato, o objetivo geral de todos os paradigmas de programação (veja, por exemplo, " Padrões de Implementação ", de Kent Beck, ou " Código Limpo ", de Robert C. Martin, para autores de software que afirmam o mesmo objetivo. como você, para qualquer programa).

Todas as dicas contidas nesses livros se aplicam à sua pergunta. Deixe-me extrair algumas dicas especificamente para "números mágicos" e "conjuntos de regras":

  1. Use constantes nomeadas e enumerações para substituir números mágicos

    Exemplo de constantes :

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

    deve ser substituído por uma constante nomeada para que nenhuma alteração posterior possa introduzir um erro de digitação e quebrar seu código, por exemplo, alterando a primeira, 0.625mas não a segunda.

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

    Exemplo de enumerações :

    As enumerações podem ajudá-lo a reunir dados que pertencem um ao outro. Se você estiver usando Java, lembre-se de que as enums são objetos; seus elementos podem conter dados e você pode definir métodos que retornam todos os elementos ou verificar alguma propriedade. Aqui um Enum é usado na construção de outro 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
    }
    

    A vantagem é que agora ninguém pode definir erroneamente um EnginePart que não seja feito de aço ou carbono e ninguém pode introduzir um EnginePart chamado "asdfasdf", como seria o caso se fosse uma string que seria verificada no conteúdo.

  2. O padrão de estratégia e o padrão de método de fábrica descrevem como encapsular "regras" e passá-las para outro objeto que as utilize (no caso do padrão de fábrica, o uso está criando algo; no caso do padrão de estratégia, o uso é o que você quiser).

    Exemplo de padrão de método de fábrica :

    Imagine que você tem dois tipos de motores: um em que cada parte deve ser conectada ao compressor e outro em que cada parte pode ser conectada livremente a quaisquer outras partes. Adaptado 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 então em outra aula:

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

    A parte interessante é: agora o construtor AssemblyLine está separado do tipo de mecanismo que ele está manipulando. Talvez os addEnginemétodos estejam chamando uma API remota ...

    Exemplo de padrão de estratégia :

    O padrão Estratégia descreve como introduzir uma função em um objeto para alterar seu comportamento. Vamos imaginar que às vezes você queira polir uma peça, às vezes deseje pintá-la e, por padrão, deseja revisar sua qualidade. Este é um exemplo de Python, adaptado do 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"
    

    Você pode expandir isso para manter uma lista de ações que deseja executar e, em seguida, chamá-las a partir do executemétodo. Talvez essa generalização possa ser melhor descrita como um padrão do Builder , mas, ei, não queremos ser exigentes, queremos? :)


2

Você pode querer usar um mecanismo de regras. Um mecanismo de regras fornece uma DSL (Linguagem Específica de Domínio) projetada para modelar os critérios necessários para um determinado resultado de uma maneira compreensível, conforme explicado nesta pergunta .

Dependendo da implementação do mecanismo de regras, as regras podem até ser alteradas sem recompilar o código. E como as regras são escritas em linguagem própria e simples, elas também podem ser alteradas pelos usuários.

Se você tiver sorte, existe um mecanismo de regras pronto para uso para a linguagem de programação que você está usando.

A desvantagem é que você precisa se familiarizar com um mecanismo de regras que pode ser difícil se você é iniciante em programação.


1

Minha solução para esse problema é bem diferente: camadas, configurações e LOP.

Primeiro, envolva a API em uma camada. Encontre sequências de chamadas de API usadas em conjunto e combine-as com suas próprias chamadas de API. Eventualmente, não deve haver chamadas diretas para a API subjacente, apenas chamadas para seus wrappers. As chamadas do wrapper devem começar a parecer um mini idioma.

Segundo, implemente um 'gerenciador de configurações'. Essa é uma maneira de associar nomes a valores dinamicamente. Algo assim. Outra mini linguagem.

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

Por fim, implemente sua própria mini linguagem para expressar designs (isso é Programação Orientada a Idioma). Esse idioma deve ser compreensível para os engenheiros e designers que contribuem com as regras e configurações. O primeiro exemplo desse produto que vem à mente é o Gnuplot, mas existem muitos outros. Você poderia usar Python, embora pessoalmente eu não.

Entendo que essa é uma abordagem complexa e pode ser um exagero para o seu problema ou exigir habilidades que você ainda não adquiriu. É assim que eu faria.


0

Não tenho certeza se entendi a pergunta corretamente, mas parece que você deve agrupar as coisas em algumas estruturas. Digamos que se você estiver usando C ++, você pode definir coisas 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
    }
};

Você pode instanciar estes no início do programa:

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

Em seguida, suas chamadas à API terão a aparência (supondo que você não possa alterar a assinatura):

 SomeAPICall( params1.p1, params1.p2 );

Se você pode alterar a assinatura da API, pode passar toda a estrutura:

 SomeAPICall( params1 );

Você também pode agrupar todos os parâmetros em um invólucro maior:

struct AllTheParameters
{
    SomeParametersClass      SPC;
    SomeOtherParametersClass SOPC;
};

0

Estou surpreso que ninguém mais tenha mencionado isso ...

Você disse:

Meu principal objetivo é poder entregar a alguém a minha fonte e fazer com que ela entenda o que eu estava fazendo, sem a minha contribuição.

Então deixe-me dizer isso, a maioria das outras respostas está no caminho certo. Eu definitivamente acho que os bancos de dados podem ajudá-lo. Mas outra coisa que o ajudará é comentar, bons nomes de variáveis ​​e organização / separação apropriada de preocupações.

Todas as outras respostas são altamente técnicas, mas estão ignorando os fundamentos aprendidos pela maioria dos programadores. Como você é um engenheiro mecânico por profissão, acho que você não está acostumado a esse estilo de documentação.

Comentar e escolher nomes de variáveis ​​bons e sucintos ajuda imensamente com a legibilidade. O que é mais fácil de entender?

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

Isso é bastante independente da linguagem. Independentemente de qual plataforma, IDE, idioma etc. você esteja trabalhando, a documentação adequada é a maneira mais limpa e fácil de garantir que alguém possa entender seu código.

Em seguida, vamos gerenciar esses números mágicos e toneladas de preocupações, mas acho que o comentário do GrandmasterB lidou com isso muito bem.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.