Quelle est la taille des bases de code non OO gérées?


27

Je vois toujours que l'abstraction est une fonctionnalité très utile que l'OO fournit pour gérer la base de code. Mais comment les grandes bases de code non OO sont-elles gérées? Ou est-ce que cela devient finalement une " grosse boule de boue "?

Mise à jour:
Il semble que tout le monde pense que «l'abstraction» n'est que de la modularisation ou de la dissimulation de données. Mais à mon humble avis, cela signifie également l'utilisation de «classes abstraites» ou «interfaces» qui est un must pour l'injection de dépendance et donc les tests. Comment les bases de code non OO gèrent-elles cela? Et aussi, à part l'abstraction, l'encapsulation aide également beaucoup à gérer de grandes bases de code car elle définit et restreint la relation entre les données et les fonctions.

Avec C, il est tout à fait possible d'écrire du code pseudo-OO. Je ne connais pas grand-chose aux autres langues non OO. Alors, est-ce LA façon de gérer de grandes bases de code C?


6
D'une manière indépendante du langage, veuillez décrire un objet. De quoi s'agit-il, comment est-il modifié, que doit-il hériter et que doit-il apporter? Le noyau Linux est plein de structures allouées avec beaucoup d'aide et de pointeurs de fonction, mais cela ne satisferait probablement pas la définition de l'objet orienté pour la plupart. Pourtant, c'est l'un des meilleurs exemples d'une base de code très bien entretenue. Pourquoi? Parce que chaque mainteneur de sous-système sait ce qui est dans sa zone de responsabilité.
Tim Post

D'une manière indépendante du langage, veuillez décrire comment vous voyez les bases de code gérées et ce que OO a à voir avec cela.
David Thornley

@Tim Post Je m'intéresse à la gestion du code source du noyau Linux. Pourriez-vous décrire davantage le système? Peut-être comme réponse avec un exemple?
Gulshan

7
Autrefois, nous utilisions des liens séparés pour les maquettes et les talons pour les tests unitaires. L'injection de dépendance n'est qu'une technique parmi plusieurs. La compilation conditionnelle en est un autre.
Macneil

Je pense qu'il est difficile de se référer à de grandes bases de code (OO ou autre) comme «gérées». Il serait bon d'avoir une meilleure définition du terme central dans votre question.
tottinge

Réponses:


43

Vous semblez penser que la POO est le seul moyen de réaliser l'abstraction.

Bien que la POO soit certainement très bonne pour le faire, ce n'est en aucun cas le seul moyen. Les grands projets peuvent également être gérés grâce à une modularisation sans compromis (il suffit de regarder Perl ou Python, qui ont tous deux excellé dans ce domaine, tout comme les langages fonctionnels comme ML et Haskell), et en utilisant des mécanismes tels que des modèles (en C ++).


27
+1 De plus, il est possible d'écrire une "grosse boule de boue" en utilisant la POO si vous ne savez pas ce que vous faites.
Larry Coleman

Qu'en est-il des bases de code C?
Gulshan

6
@Gulshan: De nombreuses grandes bases de code C sont des POO. Ce n'est pas parce que C n'a pas de classes que la POO ne peut pas être réalisée avec un peu d'effort. De plus, C permet une bonne modularisation en utilisant les en-têtes et l'idiome PIMPL. Pas aussi confortable ou puissant que les modules dans les langages modernes, mais encore une fois assez bon.
Konrad Rudolph

9
C permet la modularisation au niveau du fichier. L'interface va dans le fichier .h, les fonctions accessibles au public dans le fichier .c et les variables et fonctions privées obtiennent le staticmodificateur d'accès attaché.
David Thornley

1
@ Konrad: bien que je convienne que la POO n'est pas la seule façon de le faire, je pense que OP avait probablement strictement C en tête, qui n'est ni un langage fonctionnel ni un langage dynamique. Je doute donc que mentionner Perl et Haskell lui soit d'aucune utilité. En fait, je trouve votre commentaire plus pertinent et utile pour OP ( cela ne signifie pas que la POO ne peut pas être réalisée avec un peu d'effort ); vous pourriez envisager de l'ajouter en tant que réponse distincte avec des détails supplémentaires, peut-être pris en charge avec un extrait de code ou quelques liens. Cela gagnerait au moins mon vote, et très probablement les PO. :)
Groo

11

Modules, fonctions (externes / internes), sous-programmes ...

comme l'a dit Konrad, la POO n'est pas le seul moyen de gérer de grandes bases de code. En fait, beaucoup de logiciels ont été écrits avant (avant C ++ *).


* Et oui, je sais que C ++ n'est pas le seul à prendre en charge la POO, mais c'est en quelque sorte à ce moment que cette approche a commencé à prendre de l'inertie.
Tour


6

De manière réaliste, soit des changements peu fréquents (pensez aux calculs de retraite de la sécurité sociale) et / ou des connaissances profondément enracinées parce que les personnes qui maintiennent un tel système le font depuis un certain temps (la prise cynique est la sécurité de l'emploi).

Les meilleures solutions sont la validation reproductible, par laquelle je veux dire le test automatisé (par exemple les tests unitaires) et les tests humains qui suivent les étapes proscrites (par exemple les tests de régression) "au lieu de cliquer et voir ce qui casse".

Pour commencer à évoluer vers une sorte de test automatisé avec une base de code existante, je recommande de lire Michael Feather Working Effectively with Legacy Code , qui détaille les approches pour amener les bases de code existantes jusqu'à une sorte de cadre de test reproductible OO ou non. Cela conduit au genre d'idées auxquelles d'autres ont répondu, comme la modularisation, mais le livre décrit la bonne approche pour le faire sans casser les choses.


+1 pour le livre de Michael Feather. Quand vous vous sentez déprimé par une grosse base de code moche, (re) -le lisez :)
Matthieu

5

Bien que l'injection de dépendances basée sur des interfaces ou des classes abstraites soit une très bonne façon de faire des tests, elle n'est pas nécessaire. N'oubliez pas que presque tous les langages ont un pointeur de fonction ou un eval, ce qui peut faire tout ce que vous pouvez faire avec une interface ou une classe abstraite (le problème est qu'ils peuvent faire plus , y compris beaucoup de mauvaises choses, et qu'ils ne font pas '' t en eux-mêmes fournissent des métadonnées). Un tel programme peut réellement réaliser l'injection de dépendance avec ces mécanismes.

J'ai trouvé que la rigueur avec les métadonnées était très utile. Dans les langages OO, les relations entre les bits de code sont définies (dans une certaine mesure) par la structure des classes, d'une manière suffisamment normalisée pour avoir des choses comme une API de réflexion. Dans les langages procéduraux, il peut être utile de les inventer vous-même.

J'ai également trouvé que la génération de code est beaucoup plus utile dans un langage procédural (par rapport à un langage orienté objet). Cela garantit que les métadonnées sont synchronisées avec le code (car elles sont utilisées pour le générer) et vous donne quelque chose comme les points de coupure de la programmation orientée aspect - un endroit où vous pouvez injecter du code quand vous en avez besoin. Parfois, c'est la seule façon de faire de la programmation DRY dans un tel environnement que je peux comprendre.


3

En fait, comme vous l'avez récemment découvert , les fonctions de premier ordre sont tout ce dont vous avez besoin pour l'inversion des dépendances.

C prend en charge les fonctions de premier ordre et même les fermetures dans une certaine mesure . Et les macros C sont une fonctionnalité puissante pour la programmation générique, si elles sont manipulées avec le soin nécessaire.

Tout est là. SGLIB est un assez bon exemple sur la façon dont C peut être utilisé pour écrire du code hautement réutilisable. Et je crois qu'il y en a beaucoup plus.


2

Même sans abstraction, la plupart des programmes sont divisés en sections. Ces sections se rapportent généralement à des tâches ou activités spécifiques et vous travaillez sur celles-ci de la même manière que vous travaillez sur les bits les plus spécifiques des programmes abstraits.

Dans les projets de petite à moyenne taille, il est parfois plus facile de le faire avec une implémentation OO puriste.


2

L'abstraction, les classes abstraites, l'injection de dépendances, l'encapsulation, les interfaces, etc. ne sont pas le seul moyen de contrôler de grandes bases de code; c'est juste et orienté objet.

Le principal secret est d'éviter de penser OOP lors du codage non-OOP.

La modularité est la clé dans les langages non OO. En C, cela est réalisé comme David Thornley vient de le mentionner dans un commentaire:

L'interface va dans le fichier .h, les fonctions accessibles au public dans le fichier .c et les variables et fonctions privées obtiennent le modificateur d'accès statique attaché.


1

Une façon de gérer le code est de le décomposer en les types de code suivants, selon les principes de l'architecture MVC (model-view-controller).

  • Gestionnaires d'entrée - Ce code traite les périphériques d'entrée tels que la souris, le clavier, le port réseau ou les abstractions de niveau supérieur telles que les événements système.
  • Gestionnaires de sortie - Ce code traite de l'utilisation de données pour manipuler des périphériques externes tels que des moniteurs, des lumières, des ports réseau, etc.
  • Modèles - Ce code traite de la déclaration de la structure de vos données persistantes, des règles de validation des données persistantes et de l'enregistrement des données persistantes sur le disque (ou tout autre périphérique de données persistantes).
  • Vues - Ce code traite du formatage des données pour répondre aux exigences de diverses méthodes de visualisation telles que les navigateurs Web (HTML / CSS), l'interface graphique, la ligne de commande, les formats de données du protocole de communication (par exemple JSON, XML, ASN.1, etc.).
  • Algorithmes - Ce code transforme de manière répétitive un ensemble de données d'entrée en un ensemble de données de sortie aussi rapidement que possible.
  • Contrôleurs - Ce code prend les entrées via les gestionnaires d'entrée, analyse les entrées à l'aide d'algorithmes, puis transforme les données avec d'autres algorithmes en combinant éventuellement des entrées avec des données persistantes ou simplement en transformant les entrées, puis en enregistrant éventuellement les données transformées en persistant via le modèle logiciel, et éventuellement transformer les données via le logiciel de visualisation pour les restituer sur un périphérique de sortie.

Cette méthode d'organisation du code fonctionne bien pour les logiciels écrits dans n'importe quel langage OO ou non OO car les modèles de conception communs sont souvent communs à chacun des domaines. En outre, ces types de limites de code sont souvent les plus lâchement couplés, à l'exception des algorithmes car ils relient les formats de données des entrées au modèle, puis aux sorties.

Les évolutions du système prennent souvent la forme d'un logiciel qui gère plus de types d'entrées ou de types de sorties, mais les modèles et les vues sont les mêmes et les contrôleurs se comportent de manière très similaire. Ou un système peut au fil du temps avoir besoin de prendre en charge de plus en plus de types de sorties différents, même si les entrées, les modèles, les algorithmes sont les mêmes et que les contrôleurs et les vues sont similaires. Ou un système peut être augmenté pour ajouter de nouveaux modèles et algorithmes pour le même ensemble d'entrées, des sorties similaires et des vues similaires.

La programmation OO rend l'organisation du code difficile, car certaines classes sont profondément liées aux structures de données persistantes, et d'autres non. Si les structures de données persistantes sont intimement liées à des éléments tels que les relations en cascade 1: N ou les relations m: n, il est très difficile de décider des limites de classe jusqu'à ce que vous ayez codé une partie significative et significative de votre système avant de savoir que vous avez bien compris. . Toute classe liée aux structures de données persistantes sera difficile à faire évoluer lorsque le schéma des données persistantes change. Les classes qui gèrent les algorithmes, le formatage et l'analyse sont moins susceptibles d'être vulnérables aux modifications du schéma des structures de données persistantes. L'utilisation d'un type d'organisation de code MVC isole mieux les modifications de code les plus salissantes du code de modèle.


0

Lorsque vous travaillez dans des langages qui manquent de structure intégrée et de fonctionnalités d'organisation (par exemple, s'il n'a pas d'espaces de noms, de packages, d'assemblages, etc.) ou lorsque ceux-ci sont insuffisants pour garder une base de code de cette taille sous contrôle, la réponse naturelle est de développer nos propres stratégies pour organiser le code.

Cette stratégie d'organisation comprend probablement des normes relatives à l'emplacement où les différents fichiers doivent être conservés, les choses qui doivent se produire avant / après certains types d'opérations, et les conventions de dénomination et autres normes de codage, ainsi que beaucoup de "c'est ainsi qu'il est configuré - ne plaisante pas avec ça! " tapez des commentaires - qui sont valables tant qu'ils expliquent pourquoi!

Parce que la stratégie finira très probablement par être adaptée aux besoins spécifiques du projet (personnes, technologies, environnement, etc.), il est difficile de donner une solution universelle pour gérer de grandes bases de code.

Par conséquent, je crois que le meilleur conseil est d'adopter la stratégie spécifique au projet et de faire de sa gestion une priorité clé: documenter la structure, pourquoi c'est ainsi, les processus pour apporter des changements, l'auditer pour s'assurer qu'elle est respectée, et surtout: changez-le quand il a besoin de changer.

Nous sommes surtout familiers avec les classes et méthodes de refactorisation, mais avec une grande base de code dans un tel langage, c'est la stratégie d'organisation elle-même (avec documentation) qui doit être refactorisée au fur et à mesure des besoins.

Le raisonnement est le même que pour la refactorisation: vous développerez un blocage mental pour travailler sur de petites parties du système si vous sentez que l'organisation globale de celui-ci est un gâchis, et finira par lui permettre de se détériorer (du moins c'est mon point de vue sur il).

Les mises en garde sont également les mêmes: utilisez des tests de régression, assurez-vous que vous pouvez facilement revenir en arrière si le refactoring va mal, et concevez de manière à faciliter le refactoring en premier lieu (ou vous ne le ferez tout simplement pas!).

Je suis d'accord que c'est beaucoup plus délicat que de refactoriser le code direct, et il est plus difficile de valider / cacher le temps aux gestionnaires / clients qui pourraient ne pas comprendre pourquoi cela doit être fait, mais ce sont également les types de projets les plus sujets à la pourriture de logiciels causés par des conceptions de haut niveau inflexibles ...


0

Si vous vous interrogez sur la gestion d'une grande base de code, vous demandez comment garder votre base de code bien structurée à un niveau relativement grossier (bibliothèques / modules / construction de sous-systèmes / utilisation d'espaces de noms / avoir les bons documents aux bons endroits) etc.). Les principes OO, en particulier les «classes abstraites» ou les «interfaces», sont des principes pour garder votre code propre en interne, à un niveau très détaillé. Ainsi, les techniques de gestion d'une base de code volumineuse ne diffèrent pas pour le code OO ou non OO.


0

Comment cela est géré, c'est que vous découvrez les frontières des éléments que vous utilisez. Par exemple, les éléments suivants en C ++ ont une bordure claire et toutes les dépendances en dehors de la frontière doivent être soigneusement pensées:

  1. fonction libre
  2. fonction membre
  3. classe
  4. objet
  5. interface
  6. expression
  7. appel constructeur / création d'objets
  8. appel de fonction
  9. type de paramètre de modèle

En combinant ces éléments et en reconnaissant leurs frontières, vous pouvez créer presque tous les styles de programmation que vous souhaitez dans c ++.

Un exemple de ceci est pour une fonction serait de reconnaître qu'il est mauvais d'appeler d'autres fonctions à partir d'une fonction, car cela provoque une dépendance, à la place, vous ne devez appeler que les fonctions membres des paramètres de la fonction d'origine.


-1

Le plus grand défi technique est le problème de l'espace de noms. Une liaison partielle peut être utilisée pour contourner ce problème. La meilleure approche consiste à concevoir en utilisant des normes de codage. Sinon, tous les symboles deviennent un gâchis.


-2

Emacs en est un bon exemple:

Architecture d'Emacs

Composants Emacs

Les tests Emacs Lisp utilisent skip-unlesset let-bindfont des dispositifs de détection et de test de fonctionnalités:

Parfois, il n'est pas logique d'exécuter un test en raison de conditions préalables manquantes. Une fonctionnalité Emacs requise pourrait ne pas être compilée, la fonction à tester pourrait appeler un binaire externe qui pourrait ne pas être disponible sur la machine de test, vous l'appelez. Dans ce cas, la macro skip-unlesspeut être utilisée pour ignorer le test:

 (ert-deftest test-dbus ()
   "A test that checks D-BUS functionality."
   (skip-unless (featurep 'dbusbind))
   ...)

Le résultat de l'exécution d'un test ne doit pas dépendre de l'état actuel de l'environnement, et chaque test doit laisser son environnement dans le même état dans lequel il l'a trouvé. En particulier, un test ne doit dépendre d'aucune variable ou hook de personnalisation Emacs, et s'il doit apporter des modifications à l'état d'Emacs ou à l'état externe à Emacs (tel que le système de fichiers), il doit annuler ces modifications avant qu'il ne revienne, qu'il ait réussi ou échoué.

Les tests ne doivent pas dépendre de l'environnement car de telles dépendances peuvent rendre le test fragile ou conduire à des défaillances qui ne se produisent que dans certaines circonstances et sont difficiles à reproduire. Bien sûr, le code testé peut avoir des paramètres qui affectent son comportement. Dans ce cas, il est préférable de faire le test de let-bindtoutes ces variables de réglage pour mettre en place une configuration spécifique pour la durée du test. Le test peut également mettre en place un certain nombre de configurations différentes et exécuter le code sous test avec chacune.

Tout comme SQLite. Voici son design:

  1. sqlite3_open () → Ouvrir une connexion à une base de données SQLite nouvelle ou existante. Le constructeur de sqlite3.

  2. sqlite3 → L'objet de connexion à la base de données. Créé par sqlite3_open () et détruit par sqlite3_close ().

  3. sqlite3_stmt → L'objet d'instruction préparé. Créé par sqlite3_prepare () et détruit par sqlite3_finalize ().

  4. sqlite3_prepare () → Compiler du texte SQL en octet-code qui fera le travail d'interrogation ou de mise à jour de la base de données. Le constructeur de sqlite3_stmt.

  5. sqlite3_bind () → Stocker les données d'application dans les paramètres du SQL d'origine.

  6. sqlite3_step () → Avance un sqlite3_stmt à la ligne de résultat suivante ou à la fin.

  7. sqlite3_column () → Valeurs de colonne dans la ligne de résultat actuelle pour sqlite3_stmt.

  8. sqlite3_finalize () → Destructeur pour sqlite3_stmt.

  9. sqlite3_exec () → Fonction wrapper qui exécute sqlite3_prepare (), sqlite3_step (), sqlite3_column () et sqlite3_finalize () pour une chaîne d'une ou plusieurs instructions SQL.

  10. sqlite3_close () → Destructeur pour sqlite3.

architecture sqlite3

Les composants Tokenizer, Parser et Code Generator sont utilisés pour traiter les instructions SQL et les convertir en programmes exécutables dans un langage de machine virtuelle ou un code d'octet. En gros, ces trois couches supérieures implémentent sqlite3_prepare_v2 () . Le code d'octet généré par les trois couches supérieures est une instruction préparée. Le module Virtual Machine est responsable de l'exécution du code d'octet de l'instruction SQL. Le module B-Tree organise un fichier de base de données dans plusieurs magasins de clés / valeurs avec des clés ordonnées et des performances logarithmiques. Le module Pager est responsable du chargement des pages du fichier de base de données en mémoire, de l'implémentation et du contrôle des transactions, ainsi que de la création et de la maintenance des fichiers journaux qui empêchent la corruption de la base de données suite à un crash ou une panne de courant. L'interface du système d'exploitation est une abstraction fine qui fournit un ensemble commun de routines pour adapter SQLite pour qu'il s'exécute sur différents systèmes d'exploitation. En gros, les quatre couches inférieures implémentent sqlite3_step () .

table virtuelle sqlite3

Une table virtuelle est un objet qui est enregistré avec une connexion de base de données SQLite ouverte. Du point de vue d'une instruction SQL, l'objet de table virtuelle ressemble à n'importe quelle autre table ou vue. Mais en arrière-plan, les requêtes et les mises à jour sur une table virtuelle appellent des méthodes de rappel de l'objet table virtuelle au lieu de lire et d'écrire sur le fichier de base de données.

Une table virtuelle peut représenter une structure de données en mémoire. Ou il peut représenter une vue des données sur le disque qui ne sont pas au format SQLite. Ou l'application peut calculer le contenu de la table virtuelle à la demande.

Voici quelques utilisations existantes et postulées des tables virtuelles:

Une interface de recherche plein texte
Indices spatiaux utilisant des R-Trees
Introspection du contenu du disque d'un fichier de base de données SQLite (la table virtuelle dbstat)
Lire et / ou écrire le contenu d'un fichier de valeurs séparées par des virgules (CSV)
Accédez au système de fichiers de l'ordinateur hôte comme s'il s'agissait d'une table de base de données
Activation de la manipulation SQL des données dans des packages de statistiques comme R

SQLite utilise une variété de techniques de test, notamment:

Trois harnais de test développés indépendamment
Couverture de test à 100% dans une configuration telle que déployée
Des millions et des millions de cas de test
Tests de mémoire insuffisante
Tests d'erreur d'E / S
Tests de collision et de perte de puissance
Tests Fuzz
Tests de valeur limite
Tests d'optimisation désactivés
Tests de régression
Tests de base de données mal formés
Utilisation extensive des vérifications assert () et d'exécution
Analyse de Valgrind
Vérifications de comportement indéfinies
Listes de contrôle

Les références

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.