IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

C# Discussion :

Problème de design, comment dériver une méthode static ?


Sujet :

C#

  1. #1
    Expert éminent
    Avatar de StringBuilder
    Homme Profil pro
    Chef de projets
    Inscrit en
    Février 2010
    Messages
    4 166
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 45
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Chef de projets
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2010
    Messages : 4 166
    Points : 7 418
    Points
    7 418
    Billets dans le blog
    1
    Par défaut Problème de design, comment dériver une méthode static ?
    Bonjour,

    J'ai un problème de design, car visiblement C# ne permet pas de répondre à mon problème.
    La notion d'héritage et de polymorphisme ne concerne que les "instances" pas les "types" directement.

    Ainsi, mon idée de départ d'avoir une méthode statique abstraite dans ma classe de base, dérivée ensuite dans chaque classe dérivée ne fonctionne pas.

    Et du coup je cherche une solution élégante pour contourner le problème proprement.

    Le besoin initial :

    J'ai deux classes "Customer" et "Project".
    Ces deux classes correspondent à des données en base.

    Je vais notamment faire des opérations de CRUD dessus.

    J'ai donc créé une classe de base "Crud" qui permet d'appeler des procédures stockées de Creation, Retrieve, Update et Delete.

    J'ai ensuite hérité de cette classe pour implémenter aisément la couche d'accès aux données dans mes classes Customer et Project.

    Tout marche plutôt pas mal quand je suis sur une instance (donc Creation, Update et Delete ne posent pas de problème quand il s'agit de faire une ligne à ligne) mais lorsque je travaille sur une méthode statique, c'est tout autre-chose : pas moyen de rériver une méthode statique.

    Malheureusement, "Retrieve" retourne par essence un List<T> et non une unique instance de T.
    Et quand on cherche quelque chose, généralement c'est qu'on en a pas déjà sous la main... Donc créer une instance "coquille vide" juste pour pouvoir charger la liste des éléments recherches me pose clairement un problème de principe.

    Du coup, j'ai dû feinter : ma classe de base implémente une méthode statique "protected".
    Et mes classes filles implémentent une méthode statique "public" qui appelle la méthode de base (après avoir fait quelques bidouilles) :

    Code csharp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
     
    public abstract class Crud<T>
        public Crud()
        {
     
        }
     
        public static List<T> LoadAllFromDatabase(string procedure)
        {
            List<T> items = new List<T>();
     
            using (SqlConnection cnx = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["MyCnx"].ConnectionString))
            {
                cnx.Open();
     
                using (SqlCommand cmd = cnx.CreateCommand())
                {
                    cmd.CommandText = procedure;
                    cmd.CommandType = CommandType.StoredProcedure;
                    SqlDataReader dr = cmd.ExecuteReader();
                    while (dr.Read())
                    {
                        object[] values = new object[dr.FieldCount];
                        dr.GetValues(values);
                        items.Add((T)typeof(T).GetMethod("FromDatabase").Invoke(null, new object[] { values }));
                    }
                    dr.Close();
                }
     
                cnx.Close();
            }
     
            return items;
        }
    }
     
    public class Customer : Crud<Customer>
    {
        public int ID { get; private set; }
        public string Name { get; set; }
     
        public Customer()
        {
        }
     
        public static Customer FromDatabase(object[] values)
        {
            return new Customer() { ID = (int)values[0], Name = (string)values[1] };
        }
     
        public static List<Customer> LoadAllFromDatabase()
        {
            return LoadAllFromDatabase("GetAllCustomers");
        }
     
        public static int Create(Customer customer)
        {
            throw new NotImplementedException();
        }
     
        public void Update()
        {
            throw new NotImplementedException();
        }
     
        public void Delete()
        {
            throw new NotImplementedException();
        }
    }

    Ce code me pose deux problèmes :
    1/ Je n'ai pas de "contrat" pour imposer que toutes les classes dérivées de Crud implémentent la méthode statique List<T> LoadAllFromDatabase() afin de garantir un code cohérent d'une classe dérivée à l'autre (et pouvoir utiliser mes instances dérivées dans d'autre classes qui manipulent des Crud sans connaître le type de base).
    2/ Je n'ai pas non plus de "contrat" pour imposer que toutes les classes dérivées de Crud implémentent la méthode statique <T> FromDatabase() alors que cette méthode est appelée en dur dans ma classe de base via réflexion. Et accessoirement, la réflexion me donne des boutons : à la base, je vois ça comme un contournement d'un mauvais design... Sans oublier que je ne suis pas certain que ça ne soit pas une usine à gaz à exécuter.

    Comment gèreriez-vous le problème, vous ?
    On ne jouit bien que de ce qu’on partage.

  2. #2
    Membre chevronné
    Homme Profil pro
    edi
    Inscrit en
    Juin 2007
    Messages
    904
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Gironde (Aquitaine)

    Informations professionnelles :
    Activité : edi

    Informations forums :
    Inscription : Juin 2007
    Messages : 904
    Points : 1 922
    Points
    1 922
    Par défaut
    J'ai l'impression que tu essaies de mélanger 2 concepts :
    - des classes d'entités qui portent les données provenant de la source de données (Customer, Project, Product...) ;
    - des classes de dépôts qui vont interroger la sources de données (Crud, Repository ou quel que soit le terme que tu emploies).

    Peux-tu nous en dire plus sur ce que tu cherches à faire ou sur ton architecture ?

  3. #3
    Membre chevronné
    Homme Profil pro
    edi
    Inscrit en
    Juin 2007
    Messages
    904
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Gironde (Aquitaine)

    Informations professionnelles :
    Activité : edi

    Informations forums :
    Inscription : Juin 2007
    Messages : 904
    Points : 1 922
    Points
    1 922
    Par défaut
    Je me suis livré à quelques expérimentations, que je livre un peu brut de décoffrage. Je précise que je n'ai rien testé, je n'ai en outre pas l'habitude d'utiliser ADO alors j'ai pu faire des erreurs flagrantes et ce n'est absolument pas canonique, mais peut-être que ça peut t'inspirer, rejette ce qui ne te plaît pas, garde ce qui répond à ta problématique ; au besoin je justifierai mes choix.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    public class Entity
        {
            public Guid Id { get; set; }
        }
     
        public class Product : Entity
        {
            public virtual string Reference { get; set; }
            public virtual string Name { get; set; }
            public virtual string Description { get; set; }
        }
     
    public interface IRepository<T> where T : Entity
        {
            bool Create(T entity);
            T Read(Guid id);
            bool Update(T entity);
            bool Delete(Guid id);
        }
     
        public abstract class SqlRepositoryBase<T> : IDisposable, IRepository<T> where T : Entity, new()
        {
            protected SqlConnection Connection { get; }
     
            protected SqlRepositoryBase(SqlConnection connection)
            {
                Connection = connection;
                connection.Open();
            }
     
            #region Default CRUD implementation
            // Uses reflection to automap class name and property names to table name and column names respectively
            // These may be overided with more appropriate versions
     
            public virtual bool Create(T entity)
            {
                if (entity.Id == Guid.Empty) entity.Id = Guid.NewGuid();
     
                var mapping = ReflectionHelper.SelectMappingProperties<T>().ToList();
                var parameters = mapping.Select(p => $"{p.Name} = @{p.Name}").ToArray();
                var values = string.Join(", ", parameters);
                var sql = $"INSERT INTO {typeof(T).Name} VALUES ({values})";
     
                SqlTransaction tr = null;
                try
                {
                    var command = Connection.CreateCommand();
     
                    command.CommandType = CommandType.Text;
                    command.CommandText = sql;
                    mapping.ForEach(p => command.Parameters.AddWithValue("@" + p.Name, p.GetValue(entity)));
     
                    tr = Connection.BeginTransaction();
                    command.Transaction = tr;
                    var result = command.ExecuteNonQuery();
                    tr.Commit();
                    return result > 0;
                }
                catch
                {
                    tr?.Rollback();
                    return false;
                }
                finally { tr?.Dispose(); }
            }
     
            public virtual bool Delete(Guid id)
            {
                throw new NotImplementedException();
            }
     
            public virtual T Read(Guid id)
            {
                if (id == Guid.Empty) return null;
                var sql = $"SELECT * FROM {typeof(T).Name} WHERE Id = @Id";
                var command = Connection.CreateCommand();
                command.CommandType = CommandType.Text;
                command.CommandText = sql;
                var reader = command.ExecuteReader();
                if (!reader.NextResult()) return null;
                var entity = new T();
                ReflectionHelper.SelectMappingProperties<T>().ToList().ForEach(p => p.SetValue(entity, reader[p.Name]));
                return entity;
            }
     
            public virtual bool Update(T entity)
            {
                throw new NotImplementedException();
            }
     
            #endregion
     
            #region IDisposable
     
            private bool _disposed = false;
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }
     
            protected virtual void Dispose(bool disposing)
            {
                if (_disposed) return;
                if (disposing) { Connection.Dispose(); }
                _disposed = true;
            }
     
            ~SqlRepositoryBase() { Dispose(false); }
     
            #endregion
        }
     
        public interface IProductRepository : IRepository<Product>
        {
            Product FindByReference(string reference);
        }
     
        public class ProductSqlRepository : SqlRepositoryBase<Product>, IProductRepository
        {
            public ProductSqlRepository(SqlConnection connection) : base(connection) { }
            public Product FindByReference(string reference)
            {
                var sql = "SELECT Id FROM Product WHERE Reference = @Reference";
                var command = Connection.CreateCommand();
                command.CommandType = CommandType.Text;
                command.CommandText = sql;
                command.Parameters.AddWithValue("@Reference", reference);
     
                var reader = command.ExecuteReader();
                if (!reader.NextResult()) return null;
                var id = (Guid)reader["Id"];
                return Read(id);
            }
        }
     
        public static class ReflectionHelper
        {
            public static IEnumerable<PropertyInfo> SelectMappingProperties<T>()
            {
                return SelectMappingProperties(typeof(T));
            }
            public static IEnumerable<PropertyInfo> SelectMappingProperties(Type t)
            {
                return t.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.CanRead && p.CanWrite).ToArray();
            }
        }

  4. #4
    Expert éminent sénior

    Avatar de François DORIN
    Homme Profil pro
    Consultant informatique
    Inscrit en
    Juillet 2016
    Messages
    2 761
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France, Charente Maritime (Poitou Charente)

    Informations professionnelles :
    Activité : Consultant informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Juillet 2016
    Messages : 2 761
    Points : 10 543
    Points
    10 543
    Billets dans le blog
    21
    Par défaut
    Citation Envoyé par StringBuilder Voir le message
    Comment gèreriez-vous le problème, vous ?
    C'est une véritable problématique à laquelle il n'est pas toujours aisé de répondre.

    On peut malgré tout s'en tirer d'une manière que je trouve assez propre en mélangeant quelques patrons de conceptions (notamment Fabrique et Singleton) couplé au polymorphisme.

    Voici le principe (code pas testé donc soumis aux erreurs de frappe et autre )

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
     
    public static class DatabaseManager
    {
       private static CustomerManager _customer = new CustomerManger();
     
       public static CustomerManager Customer {get {return _customer;}}
    }
     
    public abstract class CrudManager<T>
    {
        protected List<T> LoadAllFromDatabase(string procedure)
        {
            List<T> items = new List<T>();
     
            using (SqlConnection cnx = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["MyCnx"].ConnectionString))
            {
                cnx.Open();
     
                using (SqlCommand cmd = cnx.CreateCommand())
                {
                    cmd.CommandText = procedure;
                    cmd.CommandType = CommandType.StoredProcedure;
                    SqlDataReader dr = cmd.ExecuteReader();
                    while (dr.Read())
                    {
                        object[] values = new object[dr.FieldCount];
                        dr.GetValues(values);
                        items.Add((T)typeof(T).GetMethod("FromDatabase").Invoke(null, new object[] { values }));
                    }
                    dr.Close();
                }
     
                cnx.Close();
            }
     
            return items;
        }
     
        public abstract List<T> LoadAllFromDatabase();
    }
     
    public class CustomerManager : CrudManager<Customer>
    {
       public override List<Customer> LoadAllFromDatabase()
       {
          return LoadAllFromDatabase("GetAllCustomers");
       }
    }
    Et après, dans le code, il suffit d'appeler DatabaseManager.Customer.LoaddAllFromDatabase()La classe abstraite CrudManager contient les méthodes statiques initialement définies dans la classe Crud classique. Mais les méthodes ne sont plus statiques, mais d'instance (du coup, le polymorphisme est utilisable). On définit ainsi une fabrique.

    Enfin, le pattern Singleton au niveau de la classe DatabaseManager permet de faciliter l'accès aux différentes Fabrique sans avoir besoin de les instancier soi-même.

    L'architecture est adaptable en fonction des constraintes. Par exemple, on peut se passer de la classe DatabaseManager, mais dans ce cas, il faut à chaque fois instancer une fabrique pour pouvoir accéder aux différents objets. On peut également jouer sur la visibilité, notamment des constructeurs, pour autoriser/interdir l'instanciation directe des fabriques.
    François DORIN
    Consultant informatique : conception, modélisation, développement (C#/.Net et SQL Server)
    Site internet | Profils Viadéo & LinkedIn
    ---------
    Page de cours : fdorin.developpez.com
    ---------
    N'oubliez pas de consulter la FAQ C# ainsi que les cours et tutoriels

  5. #5
    Expert éminent
    Avatar de StringBuilder
    Homme Profil pro
    Chef de projets
    Inscrit en
    Février 2010
    Messages
    4 166
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 45
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Chef de projets
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2010
    Messages : 4 166
    Points : 7 418
    Points
    7 418
    Billets dans le blog
    1
    Par défaut
    Bonjour,

    Merci Noxen pour votre retour et le temps que vous avez passé à proposer votre solution.
    Seulement, vous êtes dans une logique de développement en couches (entity ou mvc), ce que je ne souhaite pas mettre en place pour diverses raisons.

    Merci François Dorin pour votre retour qui ressemble exactement à ce que j'avais en tête... et que j'espérais éviter d'avoir à mettre en place

    En effet, j'avais deux options :
    -> Soit être obligé d'instancier un "Customer" vide afin de pouvoir appeler la méthode d'instance LoadAllFromDatabase() en utilisant l'héritage classique, mais ça me gênait de devoir manipuler une coquille vide de la sorte qu'on risque de vouloir ensuite utiliser dans le code sans raison en tout planter
    -> Soit être obligé de créer une classe faisant doublon "Manager", "Factory", "LeTrucQuiVaBien" pour chaque classe dérivée, ce qui rend le code plus lourd et moins aisé à comprendre (et plus long à écrire)

    Mais je vais retenir votre solution, car c'est effectivement celle qui me semble la plus propre.

    Je vais voir si j'arrive à déclarer cette classe "Manager" en tant que sous-classe de ma classe dérivée : ça me donnera l'impression que c'est moins éparpillé
    On ne jouit bien que de ce qu’on partage.

  6. #6
    Expert éminent sénior

    Avatar de François DORIN
    Homme Profil pro
    Consultant informatique
    Inscrit en
    Juillet 2016
    Messages
    2 761
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France, Charente Maritime (Poitou Charente)

    Informations professionnelles :
    Activité : Consultant informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Juillet 2016
    Messages : 2 761
    Points : 10 543
    Points
    10 543
    Billets dans le blog
    21
    Par défaut
    Citation Envoyé par StringBuilder Voir le message
    En effet, j'avais deux options :
    -> Soit être obligé d'instancier un "Customer" vide afin de pouvoir appeler la méthode d'instance LoadAllFromDatabase() en utilisant l'héritage classique, mais ça me gênait de devoir manipuler une coquille vide de la sorte qu'on risque de vouloir ensuite utiliser dans le code sans raison en tout planter
    -> Soit être obligé de créer une classe faisant doublon "Manager", "Factory", "LeTrucQuiVaBien" pour chaque classe dérivée, ce qui rend le code plus lourd et moins aisé à comprendre (et plus long à écrire)
    Une variante de la première option : être obligé d'insstancier un "Customer" vide, mais faire en sorte que ce customer vide soit disponible via un attribut de classe (=static). Ainsi :
    • il n'est pas utile à l'appelant de l'instancier (on peut imaginer que c'est le constructeur static de la classe qui le fait, ou alors déclarer l'attribut avec une initialisation) ;
    • l'appelant à beaucoup moins l'impression qu'il s'agit d'une instance vide ;
    • ne nécessite pas la création d'une autre classe.


    Bien sûr, il y a des inconvenients : que se passe-t-il si on fait un Save ou Update sur cette instance par défaut ? Rien n'oblige le créateur de la classe à respecter ainsi le "contrat" lors de son implémentation, etc. Mais il peut s'agir d'une alternative.

    Citation Envoyé par StringBuilder Voir le message
    Mais je vais retenir votre solution, car c'est effectivement celle qui me semble la plus propre.
    C'est également l'approche qui me semble la plus propre

    Citation Envoyé par StringBuilder Voir le message
    Je vais voir si j'arrive à déclarer cette classe "Manager" en tant que sous-classe de ma classe dérivée : ça me donnera l'impression que c'est moins éparpillé
    Rien ne s'y oppose.
    François DORIN
    Consultant informatique : conception, modélisation, développement (C#/.Net et SQL Server)
    Site internet | Profils Viadéo & LinkedIn
    ---------
    Page de cours : fdorin.developpez.com
    ---------
    N'oubliez pas de consulter la FAQ C# ainsi que les cours et tutoriels

  7. #7
    Expert confirmé
    Homme Profil pro
    Développeur .NET
    Inscrit en
    Novembre 2009
    Messages
    2 027
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 35
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Développeur .NET

    Informations forums :
    Inscription : Novembre 2009
    Messages : 2 027
    Points : 5 468
    Points
    5 468
    Par défaut
    Moi je te propose de passer par une interface interne avec implémentation explicite pour camoufler un peu ces méthodes.
    Le FromDatabase devient une transformation de l'objet en cours plutot qu'une création.
    Pareil j'ai pas testé sous VS, mais dans ma tete j'ai réglé tout tes problèmes du départ sans trop modifier le code. Peut etre que ca marche pas du tout
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    public abstract class Crud<T> where T : IDBLoader<T> 
    {
     
         public static List<T> LoadAllFromDatabase()
        {
            List<T> items = new List<T>();
     
            using (SqlConnection cnx = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["MyCnx"].ConnectionString))
            {
                cnx.Open();
                T entity= (T)Activator.CreateInstance(typeof(T));
                using (SqlCommand cmd = cnx.CreateCommand())
                {
                    cmd.CommandText = (entity as IDBLoader).GetProcedureNameAllEntity();
                    cmd.CommandType = CommandType.StoredProcedure;
                    SqlDataReader dr = cmd.ExecuteReader();
                    while (dr.Read())
                    {
                        object[] values = new object[dr.FieldCount];
                        dr.GetValues(values);
                         (entity as IDBLoader).FromDatabase(values);
                        items.Add(T);
                        entity= (T)Activator.CreateInstance(typeof(T));
                    }
                    dr.Close();
                }
     
                cnx.Close();
            }
     
            return items;
        }
    }
     
    internal interface IDBLoader
    {
              FromDatabase():
              GetProcedureNameAllEntity();
    }
    public class Customer : Crud<Customer>,IDBLoader<T>
    { ....
        public IDBLoader.FromDatabase(object[] values)  //Explicit interface
        {
            ID = (int)values[0];
            Name = (string)values[1] };
        }
     
        public string IDBLoader.GetProcedureNameAllEntity() //explicit interface
        {
                  return     "GetAllCustomers";
        }
     
    ....
    }

  8. #8
    Membre chevronné
    Homme Profil pro
    edi
    Inscrit en
    Juin 2007
    Messages
    904
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Gironde (Aquitaine)

    Informations professionnelles :
    Activité : edi

    Informations forums :
    Inscription : Juin 2007
    Messages : 904
    Points : 1 922
    Points
    1 922
    Par défaut
    Citation Envoyé par StringBuilder Voir le message
    Seulement, vous êtes dans une logique de développement en couches (entity ou mvc), ce que je ne souhaite pas mettre en place pour diverses raisons.
    Qu'est-ce-qui pose problème exactement, vous avez des contraintes sur l'architecture ?

  9. #9
    Expert éminent
    Avatar de StringBuilder
    Homme Profil pro
    Chef de projets
    Inscrit en
    Février 2010
    Messages
    4 166
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 45
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Chef de projets
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2010
    Messages : 4 166
    Points : 7 418
    Points
    7 418
    Billets dans le blog
    1
    Par défaut
    Citation Envoyé par Noxen Voir le message
    Qu'est-ce-qui pose problème exactement, vous avez des contraintes sur l'architecture ?
    Pour la principale raison que je ne veux pas différencier les traitements dans la base de la couche métier : la plupart du code fonctionnel sera dans la base, qui n'est pas ici un simple moyen de sérialiser des données volatiles.

    En gros, avec une approche classique, on charge les données en mémoire, on en crée des nouvelles, on vérifie les règles de gestion, puis on sérialise dans la base le résultat.
    Moi à l'inverse, je lance un traitement dans la base, et c'est lui qui s'occupe de savoir ce qu'il doit créer, quelles règles vérifier, et ne renvoie à l'application que ce qui est strictement nécessaire (tout s'est bien passé par exemple).

    Le but étant de limiter au maximum :
    - Les échanges avec le serveur (car au final, j'aurai une version de mon application qui n'utilisera pas une base, mais des web services à travers un réseau GSM, donc le moins j'y fait appel et le mieux je me porte)
    - Les validations de cohérence et calcul décorrélés de la mémoire (car au final j'aurai beaucoup de traitements concurrents, et c'est pas au moment d'enregistrer l'état de mon objet qu'il faudra que je me rende compte d'une incohérence au niveau des données)
    - Et surtout parce que les architectures en couche ne m'ont jamais séduites, car elles provoquent généralement une large sous-utilisation des capacités de la base de données, et une surconsommation des ressources du serveur d'application qui se retrouve à faire le boulot du SGBD
    On ne jouit bien que de ce qu’on partage.

  10. #10
    Expert éminent sénior

    Avatar de François DORIN
    Homme Profil pro
    Consultant informatique
    Inscrit en
    Juillet 2016
    Messages
    2 761
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France, Charente Maritime (Poitou Charente)

    Informations professionnelles :
    Activité : Consultant informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Juillet 2016
    Messages : 2 761
    Points : 10 543
    Points
    10 543
    Billets dans le blog
    21
    Par défaut
    Citation Envoyé par StringBuilder Voir le message
    Pour la principale raison que je ne veux pas différencier les traitements dans la base de la couche métier : la plupart du code fonctionnel sera dans la base, qui n'est pas ici un simple moyen de sérialiser des données volatiles.

    En gros, avec une approche classique, on charge les données en mémoire, on en crée des nouvelles, on vérifie les règles de gestion, puis on sérialise dans la base le résultat.
    Moi à l'inverse, je lance un traitement dans la base, et c'est lui qui s'occupe de savoir ce qu'il doit créer, quelles règles vérifier, et ne renvoie à l'application que ce qui est strictement nécessaire (tout s'est bien passé par exemple).

    Le but étant de limiter au maximum :
    - Les échanges avec le serveur (car au final, j'aurai une version de mon application qui n'utilisera pas une base, mais des web services à travers un réseau GSM, donc le moins j'y fait appel et le mieux je me porte)
    - Les validations de cohérence et calcul décorrélés de la mémoire (car au final j'aurai beaucoup de traitements concurrents, et c'est pas au moment d'enregistrer l'état de mon objet qu'il faudra que je me rende compte d'une incohérence au niveau des données)
    - Et surtout parce que les architectures en couche ne m'ont jamais séduites, car elles provoquent généralement une large sous-utilisation des capacités de la base de données, et une surconsommation des ressources du serveur d'application qui se retrouve à faire le boulot du SGBD
    J'ai envie de dire, un gros +1.

    L'approche par couche a ses avantages, mais également ses inconvénients. Si une approche par couche est très bien d'un point de vue test unitaire et pour les grandes équipes de dev, elle a le principal inconvénient d'impacter, parfois grandement, les performances, notamment lorsqu'il y a une base de données derrière.

    Par expérience, mon approche par couche se limite à définir une couche métier, avec la définition d'interfaces et ensuite leur implémentation. Et non une couche métier + une couche de transfert de données + une couche de stockage en BD. La majorité de mes règles métiers sont au sein même du SGBD. L'inconvénient majeur est que l'on "fixe" le SGBD utilisé et qu'en changer demande un gros travail de réimplémentation. En pratique, hormis le cadre d'un projet opensource où on souhaite supporter de nombreuses bases de données, on a rarement besoin de supporter plusieurs SGBD.

    Ensuite, considérer le SGBD comme partie intégrante de la couche métier et non pas seulement comme un espace de stockage à de nombreux avantages :
    • la réalisation d'une transaction complexe (plusieurs requêtes) est bien plus rapide au sein du SGBD qu'une transaction logicielle nécessitant de nombreux aller/retour entre la couche métier et le SGBD (car oui, les temps de transfert ne sont pas négligeables !) ;
    • l'ajout de contrainte au sein même du SGBD pour s'assurer de la cohérence des données lui permet de trouver de meilleures optimisations lors de l'évaluation de requêtes ;
    • la conception d'une base de données reprend réellement du sens, dans la mesure où le modèle conceptuel de données et le modèle logique de données ne sont plus une simple une seule et même chose ;
    • la maintenance est également facilitée.


    Pour donner un exemple, dans le cadre d'un projet que j'ai repris, le SGBD était simplement un moyen de persistance et toute la logique métier se trouvait dans une couche logiciel. La base grossissant, la récupération d'une liste de réunions était devenue très longue, au point d'atteindre des timeout (+de 30s sur un serveur web). Avec la déportation de la logique métier au sein même du SGBD, la requête prenait moins de 50ms. Ce qui prenait du temps ? Les objets étant complexes, une fois récupéré, il fallait refaire des requêtes supplémentaires pour récupérer tous les objets nécessaires : le responsable de la réunion, le lieu de la réunion, etc...

    Une petite astuce d'ailleurs pour celles et ceux qui souhaiteraient récupérer des informations complexes en une seule requête SQL : retourner un XML ! SQL Server gère très bien cela par exemple !

    L'inconvénient, c'est qu'il faut avoir de bonnes bases en SGBD, en modélisation de base de données et en SQL pour éviter de faire n'importe quoi. En plus de connaître son langage de programmation favori
    François DORIN
    Consultant informatique : conception, modélisation, développement (C#/.Net et SQL Server)
    Site internet | Profils Viadéo & LinkedIn
    ---------
    Page de cours : fdorin.developpez.com
    ---------
    N'oubliez pas de consulter la FAQ C# ainsi que les cours et tutoriels

  11. #11
    Membre chevronné
    Homme Profil pro
    edi
    Inscrit en
    Juin 2007
    Messages
    904
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Gironde (Aquitaine)

    Informations professionnelles :
    Activité : edi

    Informations forums :
    Inscription : Juin 2007
    Messages : 904
    Points : 1 922
    Points
    1 922
    Par défaut
    D'accord, avec le contexte c'est plus clair. Ceci-dit j'ai une vision plus abstraite de l'architecture en couche. La couche métier doit valider la cohérence sur le sens métier des traitements, mais qu'il s'agisse d'une DLL unique, d'une dll qui fait appel à des WebServices ou bien encore qu'elle soit répartie entre des traitements sur un serveur d'application et des traitements par le SGBD est à mes yeux un détail d'implémentation (qui peut bien sûr avoir une incidence sur les performances). Ce qui importe surtout c'est de répartir clairement les responsabilités pour savoir qui fait quoi. Sur ce point mes points de références sont assez concrets : combien de temps faut-il à une personne nouvelle pour s'y retrouver ? combien de temps faut-il pour localiser l'implémentation d'une fonctionnalité ? combien de temps faut-il pour incorporer un changement ? si je modifie quelque chose, à quel point suis-je certain de ne pas introduire de régression ?

  12. #12
    Expert éminent
    Avatar de StringBuilder
    Homme Profil pro
    Chef de projets
    Inscrit en
    Février 2010
    Messages
    4 166
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 45
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Chef de projets
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2010
    Messages : 4 166
    Points : 7 418
    Points
    7 418
    Billets dans le blog
    1
    Par défaut
    C'est notamment pour répondre à toutes les dernières questions que j'ai fait le choix de me baser uniquement sur des procédures stockées pour accéder aux données.

    Pour moi, la manipulation (CRUD) et la validation (cohérence, calcules, règles fonctionnelles) des données doit se faire uniquement dans le SGBD. La couche "logicielle" ne doit être là que pour gérer les problématiques de transport (web services, client/serveur, etc.) et de mise en page (ASP.NET, WinForms, WPF, etc.) et d'ergonomie (enchaînement d'écrans, regroupement de données au sein d'une même vue, etc.) mais à aucun moment avoir des règles du genre "calculer le montant de la TVA en fonction de l'age du capitaine et de la couleur du bateau" ou "calcul du taux de remise en fonction du nombre de petits pois dans la jardinière" qui ne devrait être selon moi jamais être sorties du SGBD (pour la simple et bonne raison que que ce soit en ASP.NET, WinForms ou autre application EDI sans GUI, les calculs doivent être les mêmes, donc ne doivent pas être portés par les "applications" mais bien par la base).
    On ne jouit bien que de ce qu’on partage.

Discussions similaires

  1. Réponses: 16
    Dernier message: 26/10/2006, 16h17
  2. une méthode static
    Par adilo dans le forum Langage
    Réponses: 5
    Dernier message: 23/06/2006, 21h10
  3. Réponses: 3
    Dernier message: 15/12/2005, 22h04
  4. Réponses: 2
    Dernier message: 31/08/2005, 16h12

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo