Czy moje użycie jawnego operatora rzutowania jest rozsądne, czy zły hack?

24

Mam duży przedmiot:

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

oraz specjalistyczny obiekt podobny do DTO:

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

Osobiście uważam koncepcję jawnego przeniesienia BigObject do SmallObject - wiedząc, że jest to operacja jednokierunkowa, polegająca na utracie danych - bardzo intuicyjna i czytelna:

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

Jest realizowany za pomocą jawnego operatora:

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

Teraz mógłbym stworzyć SmallObjectFactoryklasę z FromBigObject(BigObject big)metodą, która zrobiłaby to samo, dodać ją do wstrzykiwania zależności i wywołać w razie potrzeby ... ale dla mnie wydaje się to jeszcze bardziej skomplikowane i niepotrzebne.

PS Nie jestem pewien, czy to jest istotne, ale będzie też to, OtherBigObjectco będzie można przekonwertować SmallObject, ustawiając inaczej EnumType.

Gerino
źródło
4
Dlaczego nie konstruktor?
edc65
2
A może statyczna metoda fabryczna?
Brian Gordon
Dlaczego miałbyś potrzebować zastrzyku klasy fabrycznej lub zastrzyku zależności? Dokonałeś tam fałszywej dychotomii.
user253751
1
@immibis - bo jakoś nie myślałem o tym, co zaproponował @Telastyn: .ToSmallObject()metoda (lub GetSmallObject()). Chwilowy brak rozumu - wiedziałem, że coś jest nie tak z moim myśleniem, więc poprosiłem was :)
Gerino
3
To brzmi jak idealny przypadek użycia interfejsu ISmallObject, który jest implementowany tylko przez BigObject jako środek zapewniający dostęp do ograniczonego zestawu jego obszernych danych / zachowania. Zwłaszcza w połączeniu z pomysłem @ Telastyn ToSmallObject.
Marjan Venema

Odpowiedzi:

0

Żadna z pozostałych odpowiedzi nie ma racji w mojej skromnej opinii. W tym pytaniu dotyczącym przepełnienia stosu odpowiedź o najwyższym głosowaniu dowodzi, że kod mapowania powinien być trzymany poza domeną. Aby odpowiedzieć na twoje pytanie, nie - twoje użycie operatora rzutowania nie jest świetne. Radziłbym stworzyć usługę mapowania, która znajduje się pomiędzy twoim DTO a twoim obiektem domeny, lub możesz użyć do tego automappera.

Esben Skov Pedersen
źródło
To wspaniały pomysł. Mam już Automapper na miejscu, więc będzie bardzo łatwo. Mam tylko z tym problem: czy nie powinno być śladu, że BigObject i SmallObject są w jakiś sposób powiązane?
Gerino
1
Nie. Nie widzę żadnej korzyści, łącząc BigObject i SmallObject dalej razem, oprócz usługi mapowania.
Esben Skov Pedersen
7
Naprawdę? Automapper to Twoje rozwiązanie problemów projektowych?
Telastyn
1
BigObject można zmapować do SmallObject, nie są one tak naprawdę ze sobą powiązane w klasycznym sensie OOP, a kod to odzwierciedla (oba obiekty istnieją w domenie, zdolność mapowania jest ustawiona w konfiguracji mapowania wraz z wieloma innymi). Usuwa podejrzany kod (moje niefortunne przesłonięcie operatora), pozostawia modele czyste (bez metod), więc tak, wydaje się to rozwiązaniem.
Gerino
2
@EsbenSkovPedersen To rozwiązanie jest jak użycie spychacza do wykopania dziury w celu zainstalowania skrzynki pocztowej. Na szczęście i tak OP chciał wykopać podwórko, więc w tym przypadku działa spychacz. Jednak ogólnie nie poleciłbym tego rozwiązania .
Neil
81

To jest ... Nie świetnie. Pracowałem z kodem, który wykonał tę sprytną sztuczkę i doprowadził do zamieszania. W końcu można oczekiwać, że można przypisać zmienną BigObjectdo SmallObjectzmiennej, jeśli obiekty są wystarczająco powiązane, aby je rzutować. Jednak to nie działa - jeśli spróbujesz, pojawią się błędy kompilatora, ponieważ jeśli chodzi o system typów, nie są one powiązane. Wykonywanie nowych obiektów jest również nieco niesmaczne dla operatora odlewania.

.ToSmallObject()Zamiast tego poleciłbym metodę. Wyraźniej mówi o tym, co się dzieje, i co jest tak szczegółowe.

Telastyn
źródło
18
Doh ... ToSmallObject () wydaje się najbardziej oczywistym wyborem. Czasami najbardziej oczywisty jest najbardziej nieuchwytny;)
Gerino
6
mildly distastefulto mało powiedziane. To niefortunne, że język pozwala tego rodzaju wyglądać jak rzutowanie czcionek. Nikt nie zgadłby, że to rzeczywista transformacja obiektu, chyba że sami to napisali. W zespole jednoosobowym, w porządku. Jeśli współpracujesz z kimkolwiek, w najlepszym przypadku jest to strata czasu, ponieważ musisz przestać i zastanowić się, czy to naprawdę obsada, czy też jest to jedna z tych szalonych transformacji.
Kent A.
3
@Telastyn zgodził się, że nie jest to najbardziej skandaliczny zapach kodu. Ale ukryte tworzenie nowego obiektu przed działaniem, który większość programistów rozumie jako instrukcję dla kompilatora, aby traktować ten sam obiekt jako inny typ, jest nieuprzejmy dla każdego, kto musi pracować z twoim kodem po tobie. :)
Kent A.
4
+1 dla .ToSmallObject(). Rzadko należy zastępować operatorów.
ytoledano
6
@dorus - przynajmniej w .NET, Getoznacza zwrot istniejącej rzeczy. O ile nie przesłonisz operacji na małym obiekcie, dwa Getwywołania zwrócą nierówne obiekty, powodując zamieszanie / błędy / wtfs.
Telastyn
11

Chociaż rozumiem, dlaczego trzeba mieć SmallObject, podchodzę do problemu inaczej. Moje podejście do tego typu problemów polega na użyciu elewacji . Jego jedynym celem jest enkapsulacja BigObjecti udostępnianie tylko określonych członków. W ten sposób jest to nowy interfejs w tej samej instancji, a nie kopia. Oczywiście możesz również chcieć wykonać kopię, ale radziłbym to zrobić za pomocą metody stworzonej w tym celu w połączeniu z fasadą (na przykład return new SmallObject(instance.Clone())).

Fasada ma wiele innych zalet, a mianowicie zapewnia, że ​​niektóre części twojego programu mogą korzystać tylko z członków udostępnionych przez twoją fasadę, skutecznie gwarantując, że nie będzie w stanie wykorzystać tego, o czym nie powinna wiedzieć. Oprócz tego ma tę ogromną zaletę, że masz większą elastyczność w zmienianiu BigObjectprzyszłej konserwacji, bez martwienia się zbytnio o to, jak jest używany w twoim programie. Tak długo, jak możesz naśladować stare zachowanie w takiej czy innej formie, możesz uczynić SmallObjectpracę taką samą, jak wcześniej, bez konieczności zmiany programu wszędzie BigObject, gdzie byś był używany.

Zauważ, że to BigObjectnie zależy od, SmallObjectale na odwrót (tak powinno być moim skromnym zdaniem).

Neil
źródło
Jedyną wspomnianą zaletą fasady nad kopiowaniem pól do nowej klasy jest unikanie kopiowania (co prawdopodobnie nie stanowi problemu, chyba że obiekty mają absurdalną liczbę pól). Z drugiej strony wadą jest konieczność modyfikowania oryginalnej klasy za każdym razem, gdy trzeba konwertować na nową klasę, w przeciwieństwie do metody konwersji statycznej.
Doval
@Doval Chyba o to chodzi. Nie zamieniłbyś go na nową klasę. Stworzyłbyś kolejną fasadę, jeśli tego potrzebujesz. Zmiany wprowadzone w BigObject muszą być zastosowane tylko do klasy Fasada, a nie wszędzie tam, gdzie jest używana.
Neil
Ciekawym rozróżnieniem między tym podejściem a odpowiedzią Telastyn jest to, czy odpowiedzialność za generowanie SmallObjectspoczywa na SmallObjectczy BigObject. Domyślnie takie podejście wymusza SmallObjectunikanie zależności od prywatnych / chronionych członków BigObject. Możemy pójść o krok dalej i uniknąć zależności od prywatnych / chronionych członków SmallObjectza pomocą ToSmallObjectmetody rozszerzenia.
Brian
@Brian W BigObjectten sposób ryzykujesz bałagan . Jeśli chciałbyś zrobić coś podobnego, kupiłbyś możliwość utworzenia ToAnotherObjectmetody rozszerzenia BigObject? Nie powinny to być obawy, BigObjectponieważ prawdopodobnie jest już wystarczająco duży. Pozwala także oddzielić się BigObjectod tworzenia jego zależności, co oznacza, że ​​można korzystać z fabryk i tym podobnych. Drugie podejście silnie paruje BigObjecti SmallObject. W tym konkretnym przypadku może to być w porządku, ale moim skromnym zdaniem nie jest to najlepsza praktyka.
Neil
1
@Neil W rzeczywistości Brian wyjaśnił to źle, ale ma rację - metody rozszerzenia usuwają sprzężenie. Nie jest już BigObjectsprzężony SmallObject, jest to tylko metoda statyczna, która wymaga argumentu BigObjecti zwraca SmallObject. Metody rozszerzeń to tak naprawdę tylko cukier syntaktyczny, który pozwala ładniej wywoływać metody statyczne. Metoda rozszerzenie nie jest część z BigObject, jest to całkowicie odrębny sposób statyczny. W rzeczywistości jest to całkiem dobre wykorzystanie metod rozszerzeń i bardzo przydatne zwłaszcza w przypadku konwersji DTO.
Luaan
6

Istnieje bardzo silna konwencja, która opiera się na zmiennych typach referencyjnych, które zachowują tożsamość. Ponieważ system generalnie nie zezwala na operatory rzutowania zdefiniowane przez użytkownika w sytuacjach, w których obiekt typu źródłowego może być przypisany do odwołania do typu docelowego, istnieje tylko kilka przypadków, w których operacje rzutowania zdefiniowane przez użytkownika byłyby uzasadnione dla odniesienia zmiennego typy.

Zasugerowałbym jako wymóg, aby, biorąc pod uwagę, że x=(SomeType)foo;później y=(SomeType)foo;, z obiema rzutami zastosowanymi do tego samego obiektu, x.Equals(y)zawsze i zawsze byłaby prawdą, nawet jeśli przedmiotowy przedmiot został zmodyfikowany między dwoma rzutami. Taka sytuacja mogłaby mieć zastosowanie, gdyby np. Jeden miał parę obiektów różnego typu, z których każdy posiadał niezmienne odniesienie do drugiego, a rzutowanie jednego z obiektów na inny typ zwróciłoby jego sparowaną instancję. Może to również dotyczyć typów, które służą jako opakowania dla obiektów zmiennych, pod warunkiem, że tożsamości pakowanych obiektów są niezmienne, a dwa opakowania tego samego typu zgłaszałyby się jako równe, gdyby opakowały tę samą kolekcję.

Twój konkretny przykład wykorzystuje zmienne klasy, ale nie zachowuje żadnej formy tożsamości; jako taki sugerowałbym, że nie jest to właściwe użycie operatora odlewania.

supercat
źródło
1

To może być w porządku.

Problem z twoim przykładem polega na tym, że używasz takich przykładowych nazw. Rozważać:

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

Teraz, gdy masz dobry pomysł, co oznacza „ długa” i „ int” , zarówno ukryta rzutowanie na, intjak longi jawne rzutowanie z longna intsą całkiem zrozumiałe. Zrozumiałe jest również, w jaki sposób się 3staje 3i jest po prostu innym sposobem pracy 3. Jest zrozumiałe, jak to się nie powiedzie int.MaxValue + 1w sprawdzonym kontekście. Nawet to, jak będzie działać int.MaxValue + 1w niesprawdzonym kontekście, nie skutkuje int.MinValuenajtrudniejszą rzeczą.

Podobnie, gdy rzutujesz pośrednio na typ bazowy lub jawnie na typ pochodny, jest to zrozumiałe dla każdego, kto wie, jak dziedziczenie działa, co się dzieje i jaki będzie wynik (lub jak może się nie powieść).

Teraz z BigObject i SmallObject nie mam pojęcia, jak ta relacja działa. Jeśli twoje prawdziwe typy są takie, że relacja rzutowania jest oczywista, to rzutowanie może rzeczywiście być dobrym pomysłem, chociaż przez większość czasu, być może zdecydowaną większość, jeśli tak jest, to powinno to znaleźć odzwierciedlenie w hierarchii klas i wystarczy normalne odlewanie oparte na dziedzictwie.

Jon Hanna
źródło
W rzeczywistości nie są niczym więcej niż tym, o co chodzi - ale na przykład BigObjectmoże opisać Employee {Name, Vacation Days, Bank details, Access to different building floors etc.}, a SmallObjectmoże być MoneyTransferRecepient {Name, Bank details}. Istnieje proste tłumaczenie z Employeena MoneyTransferRecepienti nie ma powodu, aby przesyłać do aplikacji bankowej więcej danych niż jest to potrzebne.
Gerino