À propos du style de codage

 

J'aborde ce sujet parce que j'ai une bonne expérience du codage et du recodage d'un produit écrit il y a longtemps à partir de zéro en MQL4 et je veux partager mon expérience.

Mon collègue, je ne doute pas de votre capacité à écrire rapidement un programme qui implémente un certain algorithme. Mais je me suis déjà assuré que si vous avez abandonné le projet pour une raison quelconque, que vous y êtes revenu un mois plus tard et que vous n'avez pas pu le comprendre immédiatement, vous n'êtes pas un bon écrivain. Dans ce qui suit, je vais vous parler de mes propres exigences en matière de style de codage. Le respect de ces exigences simplifie les modifications ultérieures.

0. Ne vous précipitez pas tout de suite pour écrire le programme. Faites comme les classiques le recommandent : consacrez quelques heures à la réflexion sur la structure du programme. Vous pouvez alors vous asseoir et écrire un programme rapidement, clairement et de manière concise. Ces quelques heures seront payantes en termes de vitesse d'écriture et de débogage ultérieur.

1. La longueur des fonctions ne doit pas dépasser significativement 20 lignes. Si vous ne pouvez pas le mettre en œuvre, c'est que vous n'avez pas suffisamment réfléchi à la logique et à la structure du code. De plus, c'est sur les fonctions les plus longues et leur relation avec les fonctions qu'elles appellent que l'on passe souvent le plus de temps à déboguer le code.

Par exemple, mon code actuel compte 629 lignes et contient 27 fonctions. Cela s'accompagne d'une description de la structure d'appel des fonctions (2-6 lignes) et d'un bref commentaire avant chaque fonction, ainsi que de séparateurs vides de 4-5 lignes entre les fonctions. De plus, je ne mets pas les crochets (parenthèses) avec parcimonie, c'est-à-dire que pour chacune de deux crochets, je consacre une ligne. Si je supprime tous les commentaires avant les fonctions, et que je réduis le nombre de séparateurs entre les fonctions, alors 27 fonctions prendront environ 400 lignes, c'est-à-dire que la longueur moyenne de mes fonctions est d'environ 15 lignes.

Il y a, bien sûr, des exceptions à cette règle - mais cela s'applique aux fonctions les plus simples ou aux fonctions de sortie. En règle générale, une fonction ne doit pas effectuer plus de 3 à 5 actions différentes sur le plan fonctionnel. Sinon, il y aura de la confusion. J'ai aussi l'habitude de mettre une ligne blanche entre les différentes actions fonctionnelles de la fonction.

J'ai pas mal de fonctions qui ne font que 4 lignes (c'est un minimum, avec une ligne dans le corps de la fonction, une par ligne de déclaration et deux par accolades) à 10 lignes. Je ne m'inquiète pas de la dégradation de la vitesse du code à cause de cela, car le code ne ralentit pas du tout à cause de cela, mais à cause des mains tordues.

2. Ne soyez pas avare de commentaires expliquant la signification des actions, des variables et des fonctions. La limite de 20 lignes pour la longueur de la fonction reste intacte dans ce cas. Si vous le cassez, réécrivez la fonction. Vous pouvez utiliser des commentaires d'une ou plusieurs lignes.

3. Mes fonctions sont structurées. Il existe des fonctions de niveau d'appel supérieur (zéro), 1er, 2ème, etc. Chaque fonction du niveau d'appel suivant est directement appelée par la fonction du niveau précédent. Par exemple, une fonction comme celle-ci :

// open
// .pairsToOpen
// .combineAndVerify( )
// Собирает из двух валют символ и выполняет все проверки, нужные для его открытия.
// Возвращает валидность пары для открытия.
// Последний аргумент - [...]
bool
combineAndVerify( string quoted, string base, double& fp1 )

- est une fonction de troisième niveau. Ici :

open() est une fonction de premier niveau,

pairsToOpen() est la deuxième fonction (elle est appelée par open()), et

combineAndVerify() - troisième (elle est appelée par la fonction pairsToOpen()).


Le code de chaque fonction du niveau suivant est indenté plus à gauche que le code de la fonction précédente. Il est ainsi plus facile de voir la structure de l'ensemble du programme.

Il y a aussi des exceptions à cette règle (il existe des fonctions appelées par deux fonctions d'un niveau structurel supérieur), mais ce n'est pas courant. Cela indique généralement que le code n'est pas optimal, car la même action est effectuée dans plusieurs parties différentes du programme.
Il existe cependant des fonctions universelles qui peuvent être appelées de n'importe où. Ce sont les fonctions de sortie, et je les ai mises dans une catégorie spéciale.

3. Variables globales: il est préférable d'en avoir moins, mais il est également préférable de ne pas en abuser. Vous pouvez insérer toutes ces variables dans des paramètres de fonction formels, mais leurs appels seront alors trop lourds à écrire et leur signification sera obscure.

4. Séparer les actions de calculs et leur sortie (dans un fichier, à l'écran ou par SMS). Je conçois toutes les fonctions de sortie séparément, puis je colle les appels de ces fonctions dans le corps de la fonction appelante.

Cette règle, en plus d'améliorer la clarté et la lisibilité du code, a un autre effet secondaire : en procédant ainsi, vous pouvez très facilement couper toute la sortie du code et réduire considérablement le temps d'exécution du code : la sortie est souvent l'action la plus lente dans un programme.

5. Noms de variables : c'est clair. Chacun a son propre style, mais il est toujours souhaitable de les réaliser de manière à ce qu'ils expliquent facilement la signification des variables.

Je pense que c'est suffisant pour commencer. Si vous le souhaitez, vous pouvez en ajouter.
 
Mathemat >> :
Je pense que c'est suffisant pour commencer. Si vous le souhaitez, vous pouvez ajouter autre chose.

La question est la suivante. Quelle est la manière la plus judicieuse de construire un programme ?

1) Décrivez tout ce qui peut être fait dans la fonction START ?

2) Ou puis-je décrire toutes les actions comme des fonctions définies par l'utilisateur, puis les appeler à partir de la fonction START selon les besoins ?

//---------------------------------------------

Par exemple, le même chalut.

 

Le second est meilleur. C'est sur ce sujet que j'écris. Les fonctions de négociation doivent également être écrites comme des fonctions distinctes.

 

C'est presque la même chose pour moi.

Sauf :

1. Le nombre de lignes dans la fonction.

2. Le nombre de fonctions.

Je donne la priorité à la rapidité des calculs. Par conséquent, moins il y a de fonctions et moins on les appelle, plus le programme s'exécute rapidement.

Si je peux me débarrasser d'une fonction, je l'utiliserai.

Je n'ai échoué qu'une seule fois à le faire. Les métacitations imposaient une limite au nombre de blocs imbriqués.

J'ai obtenu une fonction de rendu d'interface de 710 lignes. Il comporte 51 paramètres. Il y en a 21. Voici donc ce que Metacquotes a réussi à faire... :-)))

En général, je pense que la fonction n'est nécessaire que si elle est appelée depuis différentes parties du programme et pas très souvent. Je préfère répéter le code dans chaque bloc pour des raisons de rapidité.

 
Zhunko >> :

Le résultat est une fonction de dessin d'interface de 710 lignes. Il comporte 51 paramètres. Il y en a 21.

Wow. Mais les fonctions de sortie, je l'ai déjà noté, représentent une exception. En ce qui concerne la vitesse d'exécution, je pense que le coût de l'appel d'une fonction au lieu d'écrire directement le bon bloc sans fonction n'est pas si important - surtout si la fonction est appelée dans une boucle. Rosh a montré quelque part la différence entre le code direct et le code d'appel de fonction.

 
Zhunko писал(а) >>

Je donne la priorité à la vitesse de calcul. Par conséquent, moins il y a de fonctions et moins on les appelle, plus le programme s'exécute rapidement.

S'il y a une opportunité de se débarrasser d'une fonction, j'en profite.

Je suis d'accord. Si une fonction est appelée moins de trois fois, il vaut mieux l'insérer dans le corps. Je le sais de première main. Je dois souvent modifier les programmes d'autres personnes. Si vous n'avez que des fonctions, vous devez ouvrir deux ou trois fenêtres pour être sûr de ne pas confondre ce qui se passe quand.

 
Mathemat >> :

Wow. Mais les fonctions de sortie, je l'ai déjà noté, représentent une exception. En ce qui concerne la vitesse d'exécution, je pense que le coût de l'appel d'une fonction au lieu d'écrire directement le bloc requis sans fonction n'est pas si important - surtout si la fonction est appelée dans une boucle. Quelque part, Rosh a montré la différence entre le code direct et le code avec appel de fonction.

Alexei, tu as peut-être raison, je n'ai pas vérifié dernièrement, mais... !

À l'époque, il devait y avoir des problèmes avec le gestionnaire de mémoire de MT4 pour Metakvot. Ainsi, après avoir supprimé toutes les fonctions utilisées pour calculer les indices, j'ai été très surpris du résultat .... La vitesse de calcul a été multipliée par 5 et la consommation de mémoire a été réduite de 3 fois. ! !!!

 
Zhunko писал(а) >>

Alexey, tu as peut-être raison, je n'ai pas vérifié dernièrement, MAIS ... !

Il devait y avoir des problèmes à l'époque avec le gestionnaire de mémoire de MT4 pour Metakvot. Ainsi, après avoir supprimé toutes les fonctions utilisées pour calculer les indices, j'ai été très surpris par le résultat .... La vitesse de calcul a été multipliée par 5 et la consommation de mémoire a été réduite de 3 fois. ! !!!

Tous les tableaux déclarés dans les fonctions sont statiques. Cela signifie que ces tableaux sont créés une seule fois (lors du premier appel de la fonction) et qu'ils sont stockés en mémoire. Par conséquent, j'essaie de rendre les tableaux globaux. Ce qui n'est pas bon.

 
Sur la taille de la fonction. J'essaie de faire tenir la fonction sur un seul écran. Pour que vous puissiez voir l'ensemble.
 

Oui, Vadim, l'impact est là. J'ai décidé de vérifier. Voici les résultats :

1. Cycle de sommation simple (500 millions d'itérations) :


int start()
{
double sum = 0;
double d;
int st = GetTickCount();
for( int i = 0; i < 500000000; i ++ )
{
add( sum );


// sum += 3.14159265;

}
int timeTotal = GetTickCount() - st;
Print( "Time = " + timeTotal );
return(0);
}
//+------------------------------------------------------------------+


double add( double sum )
{
return( sum + 3.14159265 );
}//+------------------------------------------------------------------+


Temps de calcul en secondes : 4,42 - sans appeler add(), 36,7 avec.


2. Une boucle avec des calculs plus complexes (les mêmes 500 millions d'itérations) :


int start()
{
double sum = 0;
double d;
int st = GetTickCount();
for( int i = 0; i < 500000000; i ++ )
{
add( i, sum, d );


// d = MathTan( i ) + MathLog( i );
// sum += MathSin( 3.14159265 );
}
int timeTotal = GetTickCount() - st;
Print( "Time = " + timeTotal );
return(0);
}//+------------------------------------------------------------------+


double add( int i, double sum, double& d )
{
d = MathTan( i ) + MathLog( i );
return( sum + MathSin( 3.14159265 ) );
}//+------------------------------------------------------------------+


Temps de calcul en secondes : 100.6 sans add(), 142.1 avec add().


Voici des blocs commentés avec des calculs directs dans la boucle que nous transformons en fonction pour la comparaison. Comme nous pouvons le constater, il y a une différence dans tous les cas, mais elle est très différente.

Quelles sont les conclusions ? Si nous formons quelque chose de très simple en une fonction, les coûts d'appel de la fonction jouent un rôle important, voire très important. En d'autres termes, il peut être bien plus élevé que le coût des calculs dans le corps de la fonction. Si les calculs sont plus complexes, la différence entre la présence de la fonction et son absence est fortement réduite.

Il est donc préférable de ne concevoir que des blocs comportant des calculs plus ou moins importants dans les fonctions. Je vais essayer d'en tenir compte lors du codage. Mais dans tous les cas, la différence de temps n'est significative que lorsque la boucle a beaucoup d'itérations : le coût de l'appel de fonction est ici d'environ 10^(-7) secondes.

Raison: