Mon utilisation de l'opérateur de casting explicite est-elle raisonnable ou un mauvais hack?

24

J'ai un gros objet:

class BigObject{
    public int Id {get;set;}
    public string FieldA {get;set;}
    // ...
    public string FieldZ {get;set;}
}

et un objet spécialisé de type DTO:

class SmallObject{
    public int Id {get;set;}
    public EnumType Type {get;set;}
    public string FieldC {get;set;}
    public string FieldN {get;set;}
}

Personnellement, je trouve très intuitif et lisible un concept de conversion explicite de BigObject en SmallObject - sachant qu'il s'agit d'une opération unidirectionnelle avec perte de données.

var small = (SmallObject) bigOne;
passSmallObjectToSomeone(small);

Il est implémenté à l'aide d'un opérateur explicite:

public static explicit operator SmallObject(BigObject big){
    return new SmallObject{
        Id = big.Id,
        FieldC = big.FieldC,
        FieldN = big.FieldN,
        EnumType = MyEnum.BigObjectSpecific
    };
}

Maintenant, je pourrais créer une SmallObjectFactoryclasse avec FromBigObject(BigObject big)méthode, qui ferait la même chose, l'ajouter à l'injection de dépendances et l'appeler en cas de besoin ... mais pour moi, cela semble encore plus compliqué et inutile.

PS Je ne sais pas si cela est pertinent, mais il y en aura OtherBigObjectqui pourront également être convertis en SmallObjectparamètres différents EnumType.

Gerino
la source
4
Pourquoi pas un constructeur?
edc65
2
Ou une méthode d'usine statique?
Brian Gordon
Pourquoi auriez-vous besoin d'une classe d'usine ou d'une injection de dépendance? Vous y avez fait une fausse dichotomie.
user253751
1
@immibis - parce que je n'ai pas pensé à ce que @Telastyn a proposé: .ToSmallObject()méthode (ou GetSmallObject()). Un laps de temps momentané - je savais que quelque chose n'allait pas dans ma façon de penser, alors je vous ai demandé les gars :)
Gerino
3
Cela ressemble à un cas d'utilisation parfait pour une interface ISmallObject qui n'est implémentée que par BigObject comme moyen de fournir l'accès à un ensemble limité de ses données / comportements étendus. Surtout lorsqu'il est combiné avec l'idée de @ Telastyn d'une ToSmallObjectméthode.
Marjan Venema

Réponses:

0

Aucune des autres réponses ne me convient à mon humble avis. Dans cette question de stackoverflow, la réponse ayant obtenu le vote le plus élevé fait valoir que le code de mappage doit être conservé hors du domaine. Pour répondre à votre question, non - votre utilisation de l'opérateur de distribution n'est pas excellente. Je vous conseille de créer un service de cartographie qui se situe entre votre DTO et votre objet de domaine, ou vous pouvez utiliser automapper pour cela.

Esben Skov Pedersen
la source
C’est une idée formidable. J'ai déjà Automapper en place, donc ce sera un jeu d'enfant. Seul problème que j'ai avec: ne devrait-il pas y avoir une trace que BigObject et SmallObject sont en quelque sorte liés?
Gerino
1
Non, je ne vois aucun avantage à coupler BigObject et SmallObject plus ensemble que le service de mappage.
Esben Skov Pedersen
7
Vraiment? Un automappeur est votre solution aux problèmes de conception?
Telastyn
1
Le BigObject est mappable à SmallObject, ils ne sont pas vraiment liés les uns aux autres au sens classique de la POO, et le code le reflète (les deux objets existent dans le domaine, la capacité de mappage est définie dans la config de mappage avec beaucoup d'autres). Il supprime le code douteux (mon malheureux remplacement de l'opérateur), il laisse les modèles propres (sans méthodes), alors oui, cela semble être une solution.
Gerino
2
@EsbenSkovPedersen Cette solution est comme utiliser un bulldozer pour creuser un trou pour installer votre boîte aux lettres. Heureusement, le PO voulait creuser la cour de toute façon, donc un bulldozer fonctionne dans ce cas. Cependant, je ne recommanderais pas cette solution en général .
Neil
81

C'est ... Pas génial. J'ai travaillé avec du code qui a fait cette astuce astucieuse et cela a semé la confusion. Après tout, vous vous attendez à pouvoir simplement affecter le BigObjectdans une SmallObjectvariable si les objets sont suffisamment liés pour les convertir . Cela ne fonctionne pas cependant - vous obtenez des erreurs de compilation si vous essayez, car en ce qui concerne le système de type, elles ne sont pas liées. Il est également légèrement désagréable pour l'opérateur de moulage de fabriquer de nouveaux objets.

Je recommanderais .ToSmallObject()plutôt une méthode. Il est plus clair de ce qui se passe réellement et à peu près aussi verbeux.

Telastyn
la source
18
Doh ... ToSmallObject () semble être le choix le plus évident. Parfois le plus évident est le plus insaisissable;)
Gerino
6
mildly distastefulest un euphémisme. Il est regrettable que la langue permette à ce genre de chose de ressembler à un transtypage. Personne ne devinerait que c'était une transformation d'objet réelle à moins qu'ils ne l'écrivent eux-mêmes. Dans une équipe d'une personne, très bien. Si vous collaborez avec quelqu'un, dans le meilleur des cas, c'est une perte de temps car vous devez vous arrêter et déterminer si c'est vraiment un casting, ou si c'est l'une de ces transformations folles.
Kent A.
3
@Telastyn a convenu que ce n'est pas l'odeur de code la plus flagrante. Mais la création cachée d'un nouvel objet à partir d'une opération que la plupart des programmeurs comprennent comme une instruction au compilateur pour traiter ce même objet comme un type différent, est méchante pour quiconque doit travailler avec votre code après vous. :)
Kent A.
4
+1 pour .ToSmallObject(). Vous ne devez presque jamais remplacer les opérateurs.
ytoledano
6
@dorus - au moins en .NET, Getimplique le retour d'une chose existante. Sauf si vous avez outrepassé les opérations sur le petit objet, deux Getappels retourneraient des objets inégaux, provoquant confusion / bugs / wtfs.
Telastyn
11

Bien que je puisse voir pourquoi vous devriez en avoir un SmallObject, j'aborderais le problème différemment. Mon approche de ce type de problème consiste à utiliser une façade . Son seul but est d'encapsuler BigObjectet de ne mettre à disposition que des membres spécifiques. De cette façon, il s'agit d'une nouvelle interface sur la même instance, et non d'une copie. Bien sûr, vous pouvez également effectuer une copie, mais je vous recommande de le faire via une méthode créée à cet effet en combinaison avec la façade (par exemple return new SmallObject(instance.Clone())).

La façade présente un certain nombre d'autres avantages, à savoir garantir que certaines sections de votre programme ne peuvent utiliser que les membres mis à disposition via votre façade, garantissant ainsi qu'elle ne peut pas utiliser ce qu'elle ne devrait pas savoir. En plus de cela, il a également l'énorme avantage que vous avez plus de flexibilité pour changer BigObjectdans la future maintenance sans avoir à vous soucier trop de la façon dont il est utilisé tout au long de votre programme. Tant que vous pouvez émuler l'ancien comportement sous une forme ou une autre, vous pouvez faire en sorte que le SmallObjecttravail soit le même qu'avant sans avoir à changer votre programme partout où BigObjectil aurait été utilisé.

Notez que ce moyen BigObjectne dépend pas SmallObjectmais plutôt l'inverse (comme il devrait l'être à mon humble avis).

Neil
la source
Le seul avantage que vous avez mentionné qu'une façade a sur la copie de champs dans une nouvelle classe est d'éviter la copie (ce qui n'est probablement pas un problème à moins que les objets aient une quantité absurde de champs). En revanche, il présente l'inconvénient que vous devez modifier la classe d'origine à chaque fois que vous devez effectuer une conversion vers une nouvelle classe, contrairement à une méthode de conversion statique.
Doval
@Doval Je suppose que c'est le point. Vous ne le convertiriez pas en une nouvelle classe. Vous créeriez une autre façade si c'est ce dont vous avez besoin. Les modifications apportées à BigObject doivent uniquement être appliquées à la classe Facade et pas partout où il est utilisé.
Neil
Une distinction intéressante entre cette approche et la réponse de Telastyn est de savoir si la responsabilité de générer SmallObjectincombe à SmallObjectou BigObject. Par défaut, cette approche oblige SmallObjectà éviter les dépendances sur les membres privés / protégés de BigObject. Nous pouvons aller plus loin et éviter les dépendances sur les membres privés / protégés SmallObjecten utilisant une ToSmallObjectméthode d'extension.
Brian
@Brian Vous risquez d'encombrer de BigObjectcette façon. Si vous vouliez faire quelque chose de similaire, vous préférez créer une ToAnotherObjectméthode d'extension à l'intérieur BigObject? Telles ne devraient pas être les préoccupations de BigObjectpuisque, vraisemblablement, il est déjà assez grand tel quel. Il vous permet également de vous séparer BigObjectde la création de ses dépendances, ce qui signifie que vous pouvez utiliser des usines et autres. L'autre approche couple fortement BigObjectet SmallObject. Cela peut être bien dans ce cas particulier, mais ce n'est pas la meilleure pratique à mon humble avis.
Neil
1
@Neil En fait, Brian mal expliqué, mais il est juste - les méthodes d'extension ne se débarrasser de l'accouplement. Il n'est plus BigObjectcouplé à SmallObject, c'est juste une méthode statique quelque part qui prend un argument de BigObjectet retourne SmallObject. Les méthodes d'extension ne sont vraiment que du sucre syntaxique pour appeler les méthodes statiques d'une manière plus agréable. La méthode d'extension n'est pas une partie de BigObject, il est une méthode statique complètement séparée. C'est en fait une assez bonne utilisation des méthodes d'extension, et très pratique pour les conversions DTO en particulier.
Luaan
6

Il existe une très forte convention selon laquelle les transtypages sur les types de référence mutables préservent l'identité. Étant donné que le système n'autorise généralement pas les opérateurs de conversion définis par l'utilisateur dans les situations où un objet du type source pourrait être affecté à une référence du type de destination, il n'y a que quelques cas où les opérations de conversion définies par l'utilisateur seraient raisonnables pour une référence mutable les types.

Je suggérerais comme exigence que, étant donné x=(SomeType)foo;suivi quelque temps plus tard y=(SomeType)foo;, les deux transpositions étant appliquées au même objet, x.Equals(y)devrait toujours et à jamais être vrai, même si l'objet en question a été modifié entre les deux transtypages. Une telle situation pourrait s'appliquer si, par exemple, l'un avait une paire d'objets de types différents, chacun détenant une référence immuable à l'autre, et le fait de convertir l'un ou l'autre objet en l'autre type retournerait son instance appariée. Il pourrait également s'appliquer aux types qui servent de wrappers aux objets mutables, à condition que les identités des objets en train d'être encapsulés soient immuables et que deux wrappers du même type se rapportent comme égaux s'ils encapsulent la même collection.

Votre exemple particulier utilise des classes mutables, mais ne préserve aucune forme d'identité; en tant que tel, je dirais que ce n'est pas une utilisation appropriée d'un opérateur de casting.

supercat
la source
1

Ça pourrait aller.

Un problème avec votre exemple est que vous utilisez de tels noms d'exemple. Considérer:

SomeMethod(long longNum)
{
  int num = (int)longNum;
  /* ... */

Maintenant, lorsque vous avez une bonne idée de ce que signifie un long et un int , la distribution implicite de intto longet la distribution explicite de longto intsont tout à fait compréhensibles. Il est également compréhensible comment 3devient 3et est juste une autre façon de travailler avec 3. Il est compréhensible que cela échoue int.MaxValue + 1dans un contexte vérifié. Même la façon dont cela fonctionnera int.MaxValue + 1dans un contexte non contrôlé pour aboutir int.MinValuen'est pas la chose la plus difficile à juger.

De même, lorsque vous effectuez un cast implicite vers un type de base ou explicitement vers un type dérivé, il est compréhensible pour quiconque sait comment l'héritage fonctionne, ce qui se passe et quel sera le résultat (ou comment il pourrait échouer).

Maintenant, avec BigObject et SmallObject, je ne sais pas comment fonctionne cette relation. Si vos types réels sont tels que la relation de casting est évidente, le casting peut en effet être une bonne idée, bien que la plupart du temps, peut-être la grande majorité, si tel est le cas, cela devrait se refléter dans la hiérarchie des classes et un lancer normal basé sur l'héritage suffira.

Jon Hanna
la source
En fait, ils ne sont pas beaucoup plus que ce qui est fourni en question - mais par exemple, BigObjectpourrait décrire un Employee {Name, Vacation Days, Bank details, Access to different building floors etc.}, et SmallObjectpourrait être un MoneyTransferRecepient {Name, Bank details}. Il y a une traduction simple de Employeeà MoneyTransferRecepient, et il n'y a aucune raison d'envoyer à l'application bancaire plus de données que nécessaire.
Gerino