[C#] Interfaces & implémentation

J’ai rencontré aujourd’hui un problème et je me demande si je m’y prends bien pour le coup.

J’ai créé une interface (IPersistanceContext) qui présente une méthode CreateConnection() ayant un type de retour assez générique : System.Data.IDbConnection (c’est un exemple, j’ai d’autres méthodes qui retournent d’autres interfaces).

J’ai créé ensuite une classe (SqlPersistanceContext) implémentant cette interface, et fonctionnant pour SQL Server 2000. Le but de l’interface est que quelqu’un d’autre pourra créer la même chose pour Oracle par ex.

Dans le cas de SQL2k, je créé un objet SqlConnection et je le retourne (SqlConnection implémente l’interface IDbConnection).

[code]public interface IPersistanceContext
{
System.Data.IDbConnection CreateConnection();
}

public class SqlPersistanceContext : IPersistanceContext
{
public System.Data.IDbConnection CreateConnection()
{
System.Data.SqlClient.SqlConnection conn = new System.Data.SqlClient.SqlConnection(connectionString);

return conn;

}
}[/code]

J’utilise ma classe SqlPersistanceContext dans un premier temps au travers d’un objet Factory (du pattern du même nom) qui me retourne un IPersistanceContext.

IPersistanceContext context = Factory.GetContext(); IDBConnection connexion = context.CreateConnection();

Aucun souci donc, je m’attends bien à avoir un IDbConnection retourné par ma méthode.

Dans un second temps, étant donné que ma classe SqlPersistanceContext est utilisée par une autre classe prévue pour fonctionner avec SQL Server 2000 uniquement, elle appelle la méthode CreateConnection() en s’attendant bien à obtenir un SqlConnection.

SqlPersistanceContext context = new SqlPersistanceContext(); SqlConnection connexion = (SqlConnection)context.CreateConnection();

Avec ce code, je suis obligé de transtyper le IDbConnection retourné en SqlConnection.

Mais du coup, je me suis dit que je pourrais très bien retourner directement un objet SqlConnection, vu qu’il implémente l’interface IDbConnection, ma méthode retourne bien (indirectement) un IDbConnection.

Si je fais cela, le compilo me sors une erreur comme quoi SqlPersistanceContext n’implémente pas le membre d’interface CreateConnexion() (type de retour différent).

Code qui ne compile pas :

[code]public class SqlPersistanceContext : IPersistanceContext
{
public System.Data.SqlClient.SqlConnection CreateConnection()
{
System.Data.SqlClient.SqlConnection conn = new System.Data.SqlClient.SqlConnection(connectionString);

return conn;

}
}[/code]

Ce qui me chagrine, c’est que SqlConnection implémente bien l’interface IDbConnection.
Il semblerait qu’on ne puisse pas appliquer le « qui peut le plus peut le moins » : Définir une méthode retournant une classe implémentant une interface A ne signifie pas que l’on implémente une méthode retournant cette même interface A.

La seule solution semble être de passer par du boxing dans les endroits où je sais que j’obtiendrais un SqlConnection. N’y a-t-il pas d’autre solution ? Est-ce que j’ai fait une erreur dans la conception même de mes classes ? Quelle est la justification d’une telle limitation dans le compilateur C# (ou dans l’IDL ?) ?

J’espère que j’ai été suffisament clair :stuck_out_tongue:

Effectivement, tu dois transtyper le IDbConnection retourné en SqlConnection.

Ce n’est pas un défaut du C# en général, c’est la même chose en C++.
Le probléme, c’est que les fonctions virtuelles ne peuvent être surchargées qu’avec la même signature. Un IDbConnection étant différent d’un SqlConnection, le compilateur te signale que tu n’as pas surchargé celle qui retourne un IDbConnection. Il n’y a pas non plus de transtypage implicite de IDbConnection vers SqlConnection.

[quote name=‘dgallo’ date=’ 29 Dec 2004, 12:34’]Il n’y a pas non plus de transtypage implicite de IDbConnection vers SqlConnection.
[right][post=“317163”]<{POST_SNAPBACK}>[/post][/right][/quote]
Dans le cas où la signature est différente, c’est le transtypage implicite de SqlConnection vers IDbConnection qui semble ne pas pouvoir être fait (en tout cas pour la signature).

J’aurais voulu éviter le transtypage pour des raisons de performance (éviter au maximum les opérations de boxing/unboxing), tant pis.

[quote name=‘Styx31’ date=’ 29 Dec 2004, 12:49’]J’aurais voulu éviter le transtypage pour des raisons de performance (éviter au maximum les opérations de boxing/unboxing), tant pis.
[right][post=“317170”]<{POST_SNAPBACK}>[/post][/right][/quote]
Attention de ne pas confondre transtypage(cast) et boxing/unboxing, ce n’est pas du tout la même chose. Un cast n’implique pas un boxing quand il s’agit de deux références. C’est différent si tu caste un int en object(donc référence), dans ce cas il y a un boxing. Le cast entre références est moins coûteux qu’un boxing.

Très bien, je vais donc regarder de plus près les nuances autour de ça. Merci encore :stuck_out_tongue:

Exact, j’utilisais le terme boxing à mauvais escient vu qu’il concerne la transformation de variables de types valeurs en type référence et inversement, ce qui n’est pas le cas ici

Oui, et non :stuck_out_tongue:

En fait, l’interet de faire des interfaces, c’est d’avoir un type d’objet « générique », que tu vas pouvoir manipuler sans te soucier de comment il travaille en interne.

Ici, toutes tes classes de persistance vont implémenter l’interface « IPersistanceContext » qui doivent pouvoir créer une connexion, de type IDbConnection. Après tu ne manipule que des IDbConnections, peut importe leur type réel.

De même, tu ne devrait jamais avoir à faire un « SqlPersistanceContext context = new SqlPersistanceContext(); »
C’est plutôt ta permière version : « Factory.getContext() » qui doit créer lui-même une classe qui implémente ton IPersistanceContext. Ce factory ira voir certainement dans un fichier de config quelconque quelle classe « réelle » doit être créée.

Ce factory ne renvoit jamais que des IPersistanceContext, et toi tu n’as qu’a manipuler ces objets de ce type là.

Le but des interfaces est de définir le contrat « minimum » à remplir des classes qui seront utilisées, pour que toi dans ton code tu n’ais jamais à faire un seul « cast » . Parce que pour toi, tu as uniquement besoin d’un IPersistanceContext, le reste, le comment ça marche tu t’en balances.

Maintenant, si le type de connexion IDbPersistance ne suffit pas, il te faudra peut-être définir de nouveau une nouvelle interface qui défini les méthode utiles à ta persistance. Parce que pour l’instant tu n’as que 2 classes qui sont bien du type IDbPersistance, mais imaginons que demain tu aies besoin de gérer une persistance XML, il n’y a pas de classe définissant IDbPersistance permettant de manipuler du XML, et pourtant il faudra surement pouvoir faire un « open() », « load() », « save() » …

En espérant être clair :stuck_out_tongue:

Oui, suffisament clair :stuck_out_tongue:

Je sais bien quelles sont les rôles des interfaces et leur utilisation, et avant même de poser la question j’ai bien ressassé mon archi, et effectivement, je pense que le problème vient aussi de la manière dont j’utilise mes classes. En fait j’ai deux contextes d’utilisation :

  • un où je veux m’abstraire du type de BD (où factory joue très bien son rôle) : démarrer des transaction, lancer un contexte de persistance
  • l’autre ou je veux accéder à des fonctions spécifiques de mon sgbd : se connecter, lancer une requête.

Je n’avais pas encore identifié la distinction entre ces contextes et j’avais donc fait une seule interface pour ces deux ensembles de fonctionnalités, mais je vois bien qu’il faut que je sépare en deux ces notions pour éviter les problèmes que je suis en train de rencontrer…

Finalement il est en train de pas mal dériver ce thread :P"