Question

Supposons que vous ayez une application divisée en 3 niveaux :Interface graphique, logique métier et accès aux données.Dans votre couche de logique métier, vous avez décrit vos objets métier :getters, setters, accesseurs, etc.vous avez eu l'idée.L'interface avec la couche de logique métier garantit une utilisation sûre de la logique métier, de sorte que toutes les méthodes et accesseurs que vous appelez valideront les entrées.

C'est génial lorsque vous écrivez pour la première fois le code de l'interface utilisateur, car vous disposez d'une interface parfaitement définie à laquelle vous pouvez faire confiance.

Mais voici la partie délicate : lorsque vous commencez à écrire la couche d'accès aux données, l'interface avec la logique métier ne répond pas à vos besoins.Vous devez disposer de plus d'accesseurs et de getters pour définir les champs qui sont/étaient masqués.Vous êtes désormais obligé d’éroder l’interface de votre logique métier ;il est désormais possible de définir des champs à partir de la couche UI, dont la couche UI n'a aucun paramètre métier.

En raison des changements nécessaires pour la couche d'accès aux données, l'interface avec la logique métier s'est érodée au point qu'il est même possible de définir la logique métier avec des données non valides.Ainsi, l’interface ne garantit plus une utilisation sécurisée.

J'espère avoir expliqué le problème assez clairement.Comment empêcher l’érosion de l’interface, maintenir le masquage et l’encapsulation des informations, tout en répondant aux différents besoins d’interface entre les différentes couches ?

Était-ce utile?

La solution

Si je comprends bien la question, vous avez créé un modèle de domaine et vous souhaitez écrire un mappeur objet-relationnel pour mapper entre les enregistrements de votre base de données et vos objets de domaine.Cependant, vous craignez de polluer votre modèle de domaine avec le code de « plomberie » qui serait nécessaire pour lire et écrire dans les champs de votre objet.

En prenant du recul, vous avez essentiellement deux choix quant à l'endroit où placer votre code de mappage de données : dans la classe de domaine elle-même ou dans une classe de mappage externe.La première option est souvent appelée modèle Active Record et présente l'avantage que chaque objet sait comment persister et dispose d'un accès suffisant à sa structure interne pour lui permettre d'effectuer le mappage sans avoir besoin d'exposer des champs non liés à l'activité.

Par exemple

public class User
{
    private string name;
    private AccountStatus status;

    private User()
    {
    }

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    public AccountStatus Status
    {
        get { return status; }
    }

    public void Activate()
    {
        status = AccountStatus.Active;
    }

    public void Suspend()
    {
        status = AccountStatus.Suspended;
    }

    public static User GetById(int id)
    {
        User fetchedUser = new User();

        // Lots of database and error-checking code
        // omitted for clarity
        // ...

        fetchedUser.name = (string) reader["Name"];
        fetchedUser.status = (int)reader["statusCode"] == 0 ? AccountStatus.Suspended : AccountStatus.Active;

        return fetchedUser;
    }

    public static void Save(User user)
    {
        // Code to save User's internal structure to database
        // ...
    }
}

Dans cet exemple, nous avons un objet qui représente un utilisateur avec un nom et un compte AccountStatus.Nous ne voulons pas permettre que le statut soit défini directement, peut-être parce que nous voulons vérifier que le changement est une transition de statut valide, nous n'avons donc pas de paramètre.Heureusement, le code de mappage dans les méthodes statiques GetById et Save a un accès complet aux champs de nom et d'état de l'objet.

La deuxième option consiste à avoir une deuxième classe responsable du mappage.Cela présente l'avantage de séparer les différentes préoccupations de logique métier et de persistance, ce qui peut permettre à votre conception d'être plus testable et flexible.Le défi de cette méthode est de savoir comment exposer les champs de nom et de statut à la classe externe.Certaines options sont :1.Utilisez la réflexion (qui n'a pas de scrupules à creuser profondément dans les parties intimes de votre objet) 2.Fournissez des setters publics spécialement nommés (par ex.Préfixez-les avec le mot «privé») et espérez que personne ne les utilise accidentellement 3.Si votre langue le prend en charge, rendez les setters internes mais accordez l'accès à votre module de mappage de données.Par exemple.utilisez InternalsVisibleToAttribute dans .NET 2.0 ou des fonctions amies en C++

Pour plus d'informations, je recommanderais le livre classique de Martin Fowler « Patterns of Enterprise Architecture »

Cependant, à titre d'avertissement, avant de vous lancer dans l'écriture de vos propres mappeurs, je vous recommande fortement d'utiliser un outil de mappage relationnel d'objets (ORM) tiers tel que nHibernate ou Entity Framework de Microsoft.J'ai travaillé sur quatre projets différents dans lesquels, pour diverses raisons, nous avons écrit notre propre mappeur et il est très facile de perdre beaucoup de temps à maintenir et à étendre le mappeur au lieu d'écrire du code qui fournit de la valeur à l'utilisateur final.J'ai utilisé nHibernate sur un projet jusqu'à présent et, même si la courbe d'apprentissage est assez abrupte au départ, l'investissement que vous avez investi dès le début est considérablement rentable.

Autres conseils

Il s'agit d'un problème classique : séparer votre modèle de domaine de votre modèle de base de données.Il existe plusieurs façons de l’attaquer, cela dépend vraiment de la taille de votre projet à mon avis.Vous pouvez utiliser le modèle de référentiel comme d'autres l'ont dit.Si vous utilisez .net ou Java, vous pouvez utiliser NHiberner ou Hiberner.

Ce que je fais, c'est utiliser Développement piloté par les tests j'écris donc d'abord mes couches d'interface utilisateur et de modèle et la couche de données est simulée, de sorte que l'interface utilisateur et le modèle sont construits autour d'objets spécifiques au domaine, puis plus tard, je mappe ces objets à la technologie que j'utilise dans la couche de données.C'est une très mauvaise idée de laisser la base de données déterminer la conception de votre application, d'écrire d'abord l'application et de réfléchir aux données plus tard.

ps le titre de la question est un peu trompeur

@Ice^^Chaleur :

Que voulez-vous dire par là que le niveau données ne devrait pas être conscient du niveau logique métier ?Comment rempliriez-vous un objet métier avec des données ?

L'interface utilisateur demande un service à la ServiceClass du niveau métier, à savoir l'obtention d'une liste d'objets filtrés par un objet avec les données de paramètres nécessaires.
Ensuite, ServiceClass crée une instance de l'une des classes de référentiel dans la couche données et appelle GetList (filtres ParameterType).
Ensuite, le niveau données accède à la base de données, extrait les données et les mappe au format commun défini dans l'assembly « domaine ».
Le BL n'a plus de travail à faire avec ces données, il les envoie donc à l'interface utilisateur.

Ensuite, l'interface utilisateur souhaite modifier l'élément X.Il envoie l'élément (ou l'objet métier) au service du niveau Business.Le niveau métier valide l'objet et, s'il est OK, il l'envoie au niveau données pour stockage.

L'interface utilisateur connaît le service du niveau métier, qui connaît lui aussi le niveau données.

L'interface utilisateur est responsable du mappage des données utilisateur entrées vers et depuis les objets, et le niveau données est responsable du mappage des données de la base de données vers et depuis les objets.Le niveau Business reste purement professionnel.:)

Cela pourrait être une solution, car cela n’éroderait pas l’interface.Je suppose que vous pourriez avoir un cours comme celui-ci :

public class BusinessObjectRecord : BusinessObject
{
}

Je crée toujours un assembly séparé qui contient :

  • Beaucoup de petites interfaces (pensez à ICreateRepository, IReadRepository, IReadListRepsitory..la liste est longue et la plupart d'entre eux s'appuient fortement sur les génériques)
  • Beaucoup d'interfaces concrètes, comme un IPersonRepository, qui hérite de IReadRepository, vous comprenez.
    Tout ce que vous ne pouvez pas décrire avec des interfaces plus petites, vous l'insérez dans l'interface concrète.
    Tant que vous utilisez IPersonRepository pour déclarer votre objet, vous obtenez une interface claire et cohérente avec laquelle travailler.Mais le plus intéressant, c'est que vous pouvez également créer un cours qui prend du f.x.un ICreateRepository dans son constructeur, donc le code finira par être très facile à faire des choses vraiment géniales.Il existe également ici des interfaces pour les services du niveau entreprise.
  • Enfin, je colle tous les objets de domaine dans l'assembly supplémentaire, juste pour rendre la base de code elle-même un peu plus propre et moins couplée.Ces objets n'ont aucune logique, ils sont juste un moyen courant de décrire les données pour les 3+ couches.

D'ailleurs.Pourquoi définiriez-vous des méthodes dans le niveau logique métier pour s'adapter au niveau données ?
Le niveau données ne devrait même pas avoir de raison de savoir qu’il existe un niveau métier.

Que voulez-vous dire par là que le niveau données ne devrait pas être conscient du niveau logique métier ?Comment rempliriez-vous un objet métier avec des données ?

Je fais souvent ça :

namespace Data
{
    public class BusinessObjectDataManager
    {
         public void SaveObject(BusinessObject object)
         {
                // Exec stored procedure
         {
    }
}

Le problème est donc que la couche métier doit exposer davantage de fonctionnalités à la couche de données, et ajouter cette fonctionnalité signifie en exposer trop à la couche UI ?Si je comprends bien votre problème, il semble que vous essayiez de trop vous satisfaire avec une seule interface, ce qui ne fait que l'encombrer.Pourquoi ne pas avoir deux interfaces dans la couche métier ?L’une d’elles serait une interface simple et sûre pour la couche UI.L'autre serait une interface de niveau inférieur pour la couche de données.

Vous pouvez également appliquer cette approche à deux interfaces à tous les objets qui doivent être transmis à la fois à l'interface utilisateur et aux couches de données.

public class BusinessLayer : ISimpleBusiness
{}

public class Some3LayerObject : ISimpleSome3LayerObject
{}

Vous souhaiterez peut-être diviser vos interfaces en deux types, à savoir :

  • Afficher les interfaces : qui sont des interfaces qui spécifient vos interactions avec votre interface utilisateur, et
  • Interfaces de données – qui sont des interfaces qui vous permettront de spécifier les interactions avec vos données

Il est possible d'hériter et d'implémenter les deux ensembles d'interfaces tels que :

public class BusinessObject : IView, IData

De cette façon, dans votre couche de données, vous n'avez besoin que de voir l'implémentation de l'interface d'IData, tandis que dans votre interface utilisateur, vous n'avez besoin que de voir l'implémentation de l'interface d'IView.

Une autre stratégie que vous pourriez utiliser consiste à composer vos objets dans les couches d'interface utilisateur ou de données de manière à ce qu'ils soient simplement consommés par ces couches, par exemple :

public class BusinessObject : DomainObject

public class ViewManager<T> where T : DomainObject

public class DataManager<T> where T : DomainObject

Cela permet à votre objet métier de rester ignorant à la fois de la couche UI/View et de la couche de données.

Je vais continuer mon habitude d'aller à contre-courant et dire que vous devriez vous demander pourquoi vous construisez toutes ces couches d'objets horriblement complexes.

Je pense que de nombreux développeurs considèrent la base de données comme une simple couche de persistance pour leurs objets et ne se préoccupent que des opérations CRUD dont ces objets ont besoin.Trop d'efforts sont consacrés à « l'inadéquation d'impédance » entre les modèles objet et relationnel.Voici une idée :arrêter d'essayer.

Écrivez des procédures stockées pour encapsuler vos données.Utilisez les ensembles de résultats, DataSet, DataTable, SqlCommand (ou l'équivalent java/php/quel que soit) selon les besoins à partir du code pour interagir avec la base de données.Vous n'avez pas besoin de ces objets.Un excellent exemple consiste à intégrer un SqlDataSource dans une page .ASPX.

Vous ne devriez essayer de cacher vos données à personne.Les développeurs doivent comprendre exactement comment et quand ils interagissent avec le magasin de données physique.

Les mappeurs objet-relationnels sont le diable.Arrêtez de les utiliser.

La création d'applications d'entreprise est souvent un exercice de gestion de la complexité.Vous devez garder les choses aussi simples que possible, sinon vous aurez un système absolument impossible à maintenir.Si vous êtes prêt à autoriser un certain couplage (qui est de toute façon inhérent à toute application), vous pouvez supprimer à la fois votre couche de logique métier et votre couche d'accès aux données (en les remplaçant par des procédures stockées), et vous n'en aurez besoin d'aucune. interfaces.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top