Enregistrer un objet via une méthode propre ou via une autre classe?

38

Si je veux sauvegarder et récupérer un objet, dois-je créer une autre classe pour le gérer, ou vaudrait-il mieux le faire dans la classe elle-même? Ou peut-être mélanger les deux?

Qu'est-ce qui est recommandé selon le paradigme OOD?

Par exemple

Class Student
{
    public string Name {set; get;}
    ....
    public bool Save()
    {
        SqlConnection con = ...
        // Save the class in the db
    }
    public bool Retrieve()
    {
         // search the db for the student and fill the attributes
    }
    public List<Student> RetrieveAllStudents()
    {
         // this is such a method I have most problem with it 
         // that an object returns an array of objects of its own class!
    }
}

Contre. (Je sais que ce qui suit est recommandé, mais cela me semble un peu contre la cohésion de Studentclasse)

Class Student { /* */ }
Class DB {
  public bool AddStudent(Student s)
  {

  }
  public Student RetrieveStudent(Criteria)
  {
  } 
  public List<Student> RetrieveAllStudents()
  {
  }
}

Que diriez-vous de les mélanger?

   Class Student
    {
        public string Name {set; get;}
        ....
        public bool Save()
        {
            /// do some business logic!
            db.AddStudent(this);
        }
        public bool Retrieve()
        {
             // build the criteria 
             db.RetrieveStudent(criteria);
             // fill the attributes
        }
    }
Ahmad
la source
3
Vous devriez faire des recherches sur le modèle Active Record, comme cette question ou celle-ci
Eric King
@gnat, j'ai pensé demander quelle est la meilleure opinion basée sur l'opinion puis ajouté "pour et contre" et n'ai aucun problème pour la supprimer, mais en fait je veux dire en ce qui concerne le paradigme OOD qui est recommandé
Ahmad
3
Il n'y a pas de réponse correcte. Cela dépend de vos besoins, de vos préférences, de la manière dont votre classe doit être utilisée et de l’orientation de votre projet. La seule chose correcte pour OO est que vous pouvez enregistrer et récupérer en tant qu'objets.
Dunk

Réponses:

34

Principe de responsabilité unique , séparation des préoccupations et cohésion fonctionnelle . Si vous lisez ces concepts, vous obtiendrez la réponse suivante: séparez-les .

Une raison simple de séparer la Studentclasse "DB" (ou StudentRepository, pour suivre des conventions plus répandues) est de vous permettre de modifier vos "règles métier", présentes dans la Studentclasse, sans affecter le code responsable de la persistance, et vice-versa. vice versa.

Ce type de séparation est très important, non seulement entre les règles métier et la persistance, mais aussi entre les nombreuses préoccupations de votre système, pour vous permettre d'apporter des modifications avec un impact minimal sur les modules indépendants (minime car il est parfois inévitable). Cela permet de construire des systèmes plus robustes, faciles à entretenir et plus fiables en cas de changements constants.

En associant règles de persistance et règles métier, soit une classe unique, comme dans votre premier exemple, soit DBune dépendance de Student, vous associez deux préoccupations très différentes. Il peut sembler qu'ils vont bien ensemble. ils semblent cohésifs car ils utilisent les mêmes données. Mais voici la chose: la cohésion ne peut pas être mesurée uniquement par les données partagées entre les procédures, vous devez également prendre en compte le niveau d'abstraction auquel elles existent. En fait, le type de cohésion idéal est décrit comme suit:

La cohésion fonctionnelle se produit lorsque des parties d'un module sont regroupées, car elles contribuent toutes à une tâche bien définie du module.

Et évidemment, effectuer des validations Studentpendant un certain temps, même si cela persiste, ne constitue pas "une tâche unique bien définie". Encore une fois, les règles métier et les mécanismes de persistance sont deux aspects très différents du système, qui, selon de nombreux principes de bonne conception orientée objet, doivent être séparés.

Je vous recommande de lire sur l' architecture propre , de regarder ce qui suit sur le principe de responsabilité unique (dans lequel un exemple très similaire est utilisé) et de regarder également cette présentation sur l'architecture propre . Ces concepts décrivent les raisons de telles séparations.

MichelHenrich
la source
11
Ah, mais la Studentclasse devrait être correctement encapsulée, oui? Comment une classe externe peut-elle alors gérer la persistance d'un état privé? L'encapsulation doit être comparée au SoC et au SRP, mais choisir simplement l'un ou l'autre plutôt que l'autre sans peser avec soin les compromis est probablement une erreur. Une solution possible à cette énigme consiste à utiliser des accesseurs privés de paquets pour le code de persistance à utiliser.
amon
1
@amon Je conviens que ce n'est pas si simple, mais je pense que c'est une question de couplage et non d'encapsulation. Par exemple, vous pouvez rendre la classe Student abstraite, avec des méthodes abstraites pour toutes les données nécessaires à l'entreprise, qui sont ensuite implémentées par le référentiel. De cette façon, la structure de données de la base de données est complètement cachée derrière l'implémentation du référentiel, elle est donc même encapsulée à partir de la classe Student. Mais, dans le même temps, le référentiel est profondément couplé à la classe Student. Si des méthodes abstraites sont modifiées, la mise en œuvre doit également changer. Tout est une question de compromis. :)
MichelHenrich
4
L'affirmation selon laquelle SRP facilite la maintenance des programmes est pour le moins douteuse. Le problème créé par SRP est qu'au lieu d'avoir une collection de classes bien nommées, où le nom dit tout ce que la classe peut et ne peut pas faire (en d'autres termes, facile à comprendre et facile à maintenir), vous vous retrouvez avec des centaines / des milliers de classes que les gens devaient utiliser un thésaurus pour choisir un nom unique, de nombreux noms sont similaires mais pas tout à fait et trier tout ce que la paille est une corvée. IOW, pas maintenable du tout, IMO.
Dunk
3
@Dunk, vous parlez comme si les classes étaient la seule unité de modularisation disponible. J'ai vu et écrit de nombreux projets à la suite de SRP, et je conviens que votre exemple se concrétise, mais uniquement si vous n'utilisez pas correctement les packages et les bibliothèques. Si vous séparez les responsabilités en packages, puisque le contexte est impliqué par celui-ci, vous pouvez nommer les classes librement. Ensuite, pour maintenir un système comme celui-ci, il suffit de forer dans le bon paquet avant de rechercher la classe à modifier. :)
MichelHenrich
5
Les principes de @Dunk OOD peuvent être énoncés comme suit: "En principe, il est préférable de procéder ainsi à cause de X, Y et Z". Cela ne signifie pas qu'il devrait toujours être appliqué; les gens doivent être pragmatiques et peser les compromis. Dans les grands projets, les avantages sont tels qu’ils dépassent généralement la courbe d’apprentissage dont vous parlez - mais bien sûr, ce n’est pas la réalité pour la plupart des gens et il ne devrait donc pas s’appliquer aussi lourdement. Cependant, même dans les applications allégées SRP, je pense que nous pouvons tous deux convenir que séparer la persistance des règles métier produit toujours des avantages supérieurs à son coût (sauf peut-être pour le code jetable).
MichelHenrich
10

Les deux approches violent le principe de responsabilité unique. Votre première version donne à la Studentclasse de nombreuses responsabilités et la lie à une technologie d’accès à la base de données spécifique. La seconde mène à une DBclasse énorme qui sera responsable non seulement des étudiants, mais de tout autre type d’objet de données de votre programme. EDIT: votre troisième approche est la pire car elle crée une dépendance cyclique entre la classe DB et la Studentclasse.

Donc, si vous n'écrivez pas un programme de jouets, n'en utilisez aucun. Au lieu de cela, utilisez une classe différente, telle que celle StudentRepositorypour fournir une API pour le chargement et la sauvegarde, en supposant que vous allez implémenter le code CRUD vous-même. Vous pouvez également envisager d'utiliser un cadre ORM , qui peut effectuer le travail difficile pour vous (et le cadre appliquera généralement certaines décisions lorsque les opérations de chargement et d'enregistrement doivent être placées).

Doc Brown
la source
Merci, j'ai modifié ma réponse, qu'en est-il de la troisième approche? Quoi qu'il en soit, je pense ici qu'il est important de créer des couches distinctes
Ahmad
Quelque chose me suggère également d’utiliser StudentRepository au lieu de DB (je ne sais pas pourquoi! Peut-être encore une seule responsabilité) mais je ne peux toujours pas comprendre pourquoi un référentiel différent pour chaque classe? si c'est juste une couche d'accès aux données, alors il y a plus de fonctions utilitaires statiques et peuvent être ensemble, non? juste alors peut-être que la classe deviendrait si sale et encombrée (désolé pour mon anglais)
Ahmad
1
@Ahmad: il existe plusieurs raisons et quelques avantages et inconvénients à prendre en compte. Cela pourrait remplir un livre entier. Le raisonnement derrière cela se résume à la testabilité, à la maintenabilité et à l'évolutivité à long terme de vos programmes, en particulier lorsqu'ils ont un cycle de vie long.
Doc Brown
Quelqu'un a-t-il déjà travaillé sur un projet dans lequel la technologie de la base de données a considérablement évolué? (sans une réécriture massive?) Je n'ai pas. Dans l’affirmative, le fait de suivre le PRS, comme suggéré ici, pour éviter d’être lié à ce DB, a-t-il grandement aidé?
user949300
@ user949300: Je pense que je ne me suis pas exprimé clairement. La classe d'étudiant n'est pas liée à une technologie de base de données spécifique, mais à une technologie d'accès à une base de données spécifique. Elle attend donc quelque chose comme une base de données SQL pour la persistance. Garder la classe complètement ignorante du mécanisme de persistance permet une réutilisation beaucoup plus facile de la classe dans différents contextes. Par exemple, dans un contexte de test ou dans un contexte où les objets sont stockés dans un fichier. Et c’est quelque chose que j’ai fait très souvent dans le passé. Nul besoin de changer toute la pile de bases de données de votre système pour en tirer profit.
Doc Brown
3

Il existe de nombreux modèles pouvant être utilisés pour la persistance des données. Il existe un modèle d' unité de travail, un modèle de référentiel , d'autres modèles pouvant être utilisés, tels que la façade distante, et ainsi de suite.

La plupart de ceux-ci ont leurs fans et leurs critiques. Souvent, il s'agit de choisir ce qui semble être le mieux adapté à l'application et de s'y tenir (avec tous ses avantages et inconvénients, c'est-à-dire de ne pas utiliser les deux modèles en même temps ... sauf si vous en êtes vraiment sûr).

Remarque: dans votre exemple, RetrieveStudent, AddStudent devrait être une méthode statique (car elle ne dépend pas d'une instance).

Une autre façon d'avoir des méthodes de sauvegarde / chargement en classe est la suivante:

class Animal{
    public string Name {get; set;}
    public void Save(){...}
    public static Animal Load(string name){....}
    public static string[] GetAllNames(){...} // if you just want to list them
    public static Animal[] GetAllAnimals(){...} // if you actually need to retrieve them all
}

Personnellement, je n'utiliserais cette approche que dans des applications relativement petites, éventuellement des outils à usage personnel ou pour lesquels je peux prédire de manière fiable que les cas d'utilisation ne seront pas plus compliqués que la simple sauvegarde ou le chargement d'objets.

Aussi personnellement, voir le modèle d'unité de travail. Lorsque vous apprenez à le connaître, il est vraiment bon dans les petites et les grandes affaires. Et il est supporté par beaucoup de frameworks / apis, nommer EntityFramework ou RavenDB par exemple.

Gerino
la source
2
À propos de votre note: Bien que, d’un point de vue conceptuel, il n’existe qu’une seule base de données pendant l’exécution de l’application, cela ne signifie pas que vous devriez rendre les méthodes RetriveStudent et AddStudent statiques. Les référentiels sont généralement constitués d'interfaces afin de permettre aux développeurs de changer d'implémentation sans affecter le reste de l'application. Cela peut même vous permettre de distribuer votre application avec un support pour différentes bases de données, par exemple. Bien entendu, cette même logique s’applique à de nombreux autres domaines que la persistance, comme par exemple l’UI.
MichelHenrich
Vous avez certainement raison, j'ai formulé beaucoup d'hypothèses basées sur la question initiale.
Gerino
merci, je pense que la meilleure approche consiste à utiliser les couches mentionnées dans le modèle de référentiel
Ahmad
1

S'il s'agit d'une application très simple dans laquelle l'objet est plus ou moins lié au magasin de données et inversement (c'est-à-dire qu'il peut être considéré comme une propriété du magasin de données), il peut être judicieux d'utiliser une méthode .save () pour cette classe.

Mais je pense que ce serait assez exceptionnel.

Il est généralement préférable de laisser la classe gérer ses données et ses fonctionnalités (comme un bon citoyen OO) et d'externaliser le mécanisme de persistance vers une autre classe, ou un ensemble de classes.

L'autre option consiste à utiliser un cadre de persistance qui définit la persistance de manière déclarative (comme avec les annotations), mais en externalisant la persistance.

Rob
la source
-1
  • Définir une méthode sur cette classe qui sérialise l'objet et renvoie une séquence d'octets que vous pouvez mettre dans une base de données, un fichier ou autre
  • Avoir un constructeur pour cet objet qui prend une telle séquence d'octets en entrée

En ce qui RetrieveAllStudents()concerne la méthode, vous avez raison, elle est probablement mal placée, car vous pourriez avoir plusieurs listes d’élèves distinctes. Pourquoi ne pas simplement garder les listes en dehors de la Studentclasse?

philfr
la source
Vous savez que j'ai vu une telle tendance dans certains frameworks PHP, par exemple Laravel si vous le connaissez. Le modèle est lié à la base de données et peut même renvoyer une série d'objets.
Ahmad