Najlepsze praktyki dotyczące mapowania typów i metod rozszerzenia

15

Chcę zadać kilka pytań na temat najlepszych praktyk dotyczących typów mapowania i korzystania z metod rozszerzenia w języku C #. Wiem, że ten temat był omawiany wiele razy w ciągu ostatnich kilku lat, ale przeczytałem wiele postów i nadal mam wątpliwości.

Problemem, który napotkałem, było rozszerzenie posiadanej przeze mnie klasy o funkcję „konwersji”. Powiedzmy, że mam klasę „Osoba” reprezentującą obiekt, który będzie używany przez jakąś logikę. Mam również klasę „Klient”, która reprezentuje odpowiedź zewnętrznego interfejsu API (w rzeczywistości będzie więcej niż jeden interfejs API, więc muszę mapować odpowiedź każdego interfejsu API na typ typowy: Osoba). Mam dostęp do kodu źródłowego obu klas i teoretycznie mogę tam implementować własne metody. Muszę przekonwertować klienta na osobę, aby zapisać go w bazie danych. Projekt nie korzysta z żadnych automatycznych mapowań.

Mam na myśli 4 możliwe rozwiązania:

  1. Metoda .ToPerson () w klasie Consumer. To proste, ale wydaje mi się, że przełamujesz wzorzec pojedynczej odpowiedzialności, zwłaszcza że klasa Consumer jest również odwzorowana na inne klasy (niektóre wymagane przez inny zewnętrzny interfejs API), więc musiałaby zawierać wiele metod mapowania.

  2. Konstruktor odwzorowań w klasie Person traktuje Konsumenta jako argument. Również łatwe i wydaje się, że łamanie wzorca pojedynczej odpowiedzialności. Musiałbym mieć wiele konstruktorów odwzorowywania (ponieważ będzie klasa z innego interfejsu API, zapewniająca te same dane co konsument, ale w nieco innym formacie)

  3. Klasa konwerterów z metodami rozszerzeń. W ten sposób mogę napisać metodę .ToPerson () dla klasy Consumer, a kiedy inny API zostanie wprowadzony z własną klasą NewConsumer, mogę po prostu napisać inną metodę rozszerzenia i zachować wszystko w tym samym pliku. Słyszałem opinię, że metody rozszerzenia są ogólnie złe i powinny być stosowane tylko wtedy, gdy jest to absolutnie konieczne, więc to mnie powstrzymuje. W przeciwnym razie podoba mi się to rozwiązanie

  4. Klasa konwertera / mapowania. Tworzę osobną klasę, która będzie obsługiwać konwersje i implementuję metody, które przyjmą instancję klasy źródłowej jako argument i zwrócą instancję klasy docelowej.

Podsumowując, mój problem można sprowadzić do liczby pytań (wszystko w kontekście do tego, co opisałem powyżej):

  1. Czy umieszczenie metody konwersji w obiekcie (POCO?) (Jak metoda .ToPerson () w klasie Consumer) jest rozważane jako przełamanie wzorca pojedynczej odpowiedzialności?

  2. Czy używanie konstruktorów konwertujących w klasie (podobnej do DTO) jest uważane za przełamanie wzorca pojedynczej odpowiedzialności? Zwłaszcza jeśli taką klasę można przekonwertować z wielu typów źródeł, więc byłoby wymaganych wiele konstruktorów konwertujących?

  3. Czy stosowanie metod rozszerzenia przy jednoczesnym dostępie do oryginalnego kodu źródłowego klasy jest uważane za złą praktykę? Czy takie zachowanie może być wykorzystane jako realny wzór do rozdzielenia logiki, czy też jest to anty-wzór?

emsi
źródło
Czy Personklasa jest DTO? czy zawiera jakieś zachowanie?
Yacoub Massad
O ile mi wiadomo, technicznie jest to DTO. Zawiera pewną logikę „wstrzykiwaną” jako metody rozszerzenia, ale ta logika jest ograniczona do metod typu „ConvertToThatClass”. To są wszystkie starsze metody. Moim zadaniem jest wdrożenie nowej funkcjonalności korzystającej z niektórych z tych klas, ale powiedziano mi, że nie powinienem trzymać się obecnego podejścia, jeśli źle jest po prostu zachować jego spójność. Zastanawiam się więc, czy to istniejące podejście do konwersji za pomocą metod rozszerzeń jest dobre i czy powinienem się go trzymać lub spróbować czegoś innego
emsi

Odpowiedzi:

11

Czy umieszczenie metody konwersji w obiekcie (POCO?) (Jak metoda .ToPerson () w klasie Consumer) jest rozważane jako przełamanie wzorca pojedynczej odpowiedzialności?

Tak, ponieważ konwersja to kolejna odpowiedzialność.

Czy używanie konstruktorów konwertujących w klasie (podobnej do DTO) jest uważane za przełamanie wzorca pojedynczej odpowiedzialności? Zwłaszcza jeśli taką klasę można przekonwertować z wielu typów źródeł, więc byłoby wymaganych wiele konstruktorów konwertujących?

Tak, konwersja to kolejna odpowiedzialność. Nie ma znaczenia, czy robisz to za pomocą konstruktorów, czy metod konwersji (np ToPerson.).

Czy stosowanie metod rozszerzenia przy jednoczesnym dostępie do oryginalnego kodu źródłowego klasy jest uważane za złą praktykę?

Niekoniecznie. Możesz tworzyć metody rozszerzenia, nawet jeśli masz kod źródłowy klasy, którą chcesz rozszerzyć. Myślę, że to, czy utworzysz metodę rozszerzenia, czy nie, powinno zależeć od charakteru takiej metody. Na przykład, czy zawiera dużo logiki? Czy to zależy od czegoś innego niż członków samego obiektu? Powiedziałbym, że nie powinieneś mieć metody rozszerzenia wymagającej zależności do działania lub zawierającej złożoną logikę. W metodzie rozszerzenia powinna znajdować się tylko najprostsza logika.

Czy takie zachowanie może być wykorzystane jako realny wzór do rozdzielenia logiki, czy też jest to anty-wzór?

Jeśli logika jest złożona, myślę, że nie powinieneś używać metody rozszerzenia. Jak zauważyłem wcześniej, powinieneś używać metod rozszerzenia tylko do najprostszych rzeczy. Nie uważałbym konwersji za prostą.

Sugeruję utworzenie usług konwersji. Możesz mieć do tego celu jeden ogólny interfejs:

public interface IConverter<TSource,TDestination>
{
    TDestination Convert(TSource source_object);
}

I możesz mieć takie konwertery:

public class PersonToCustomerConverter : IConverter<Person,Customer>
{
    public Customer Convert(Person source_object)
    {
        //Do the conversion here. Note that you can have dependencies injected to this class
    }
}

I możesz użyć Dependency Injection, aby wstrzyknąć konwerter (np. IConverter<Person,Customer>) Do dowolnej klasy, która wymaga możliwości konwersji pomiędzy Personi Customer.

Yacoub Massad
źródło
Jeśli się nie mylę, jest już IConverterw frameworku, który tylko czeka na wdrożenie.
RubberDuck,
@RubberDuck, gdzie to istnieje?
Yacoub Massad
Myślałem o tym IConvertable, czego nie szukamy tutaj. Mój błąd.
RubberDuck,
Ach ha! Znalazłem to, co myślę o @YacoubMassad. Converterjest używany przez Listpodczas rozmowy ConvertAll. msdn.microsoft.com/en-us/library/kt456a2y(v=vs.110).aspx Nie wiem jednak, jak użyteczne jest to dla OP.
RubberDuck,
Również istotne. Ktoś inny przyjął zaproponowane przez ciebie podejście. codereview.stackexchange.com/q/51889/41243
RubberDuck
5

Czy umieszczenie metody konwersji w obiekcie (POCO?) (Podobnie jak .ToPerson()metoda w klasie Consumer) jest rozważane jako przełamanie wzorca pojedynczej odpowiedzialności?

Tak. ConsumerKlasa jest odpowiedzialny za przechowywanie danych związanych z konsumentem (i ewentualnie wykonując pewne działania) i nie powinien być odpowiedzialny za przekształcanie się do innego, niezwiązanego typu.

Czy używanie konstruktorów konwertujących w klasie (podobnej do DTO) jest uważane za przełamanie wzorca pojedynczej odpowiedzialności? Zwłaszcza jeśli taką klasę można przekonwertować z wielu typów źródeł, więc byłoby wymaganych wiele konstruktorów konwertujących?

Prawdopodobnie. Zazwyczaj mam metody poza domeną i obiektami DTO do przeprowadzenia konwersji. Często używam repozytorium, które przyjmuje obiekty domeny i (de) serializuje je, albo do bazy danych, pamięci, pliku, cokolwiek. Jeśli zastosuję tę logikę w swoich klasach domen, teraz powiązałem je z jedną konkretną formą serializacji, która jest nieodpowiednia do testowania (i innych rzeczy). Jeśli zastosuję logikę w moich klasach DTO, przywiążę je do mojej domeny, ponownie ograniczając testowanie.

Czy stosowanie metod rozszerzenia przy jednoczesnym dostępie do oryginalnego kodu źródłowego klasy jest uważane za złą praktykę?

Nie ogólnie, choć można je nadużywać. Metodami rozszerzeń tworzysz opcjonalne rozszerzenia, które ułatwiają czytanie kodu. Mają wady - łatwiej jest tworzyć niejednoznaczności, które kompilator musi rozwiązać (czasem po cichu) i może utrudnić debugowanie kodu, ponieważ nie jest całkowicie oczywiste, skąd pochodzi metoda rozszerzenia.

W twoim przypadku klasa konwertera lub programu mapującego wydaje się najprostszym i najprostszym podejściem, chociaż nie sądzę, abyś robił coś złego , używając metod rozszerzenia.

D Stanley
źródło
2

Co powiesz na użycie AutoMappera lub niestandardowego mapera

MyMapper
   .CreateMap<Person>()
   .To<PersonViewModel>()
   .Map(p => p.Name, vm => vm.FirstName)
   .To<SomeDTO>()
   .Map(...);

z domeny

 db.Persons
   .ToListAsync()
   .Map<PersonViewModel>();

Pod maską można streścić AutoMapper lub rzucić własnego mapera

Anders
źródło
2

Wiem, że to stare, ale wciąż rozkoszne. Umożliwi to konwersję w obie strony. Uważam to za pomocne podczas pracy z Entity Framework i tworzeniem modeli widoków (DTO).

public interface IConverter<TSource, TDestination>
{
    TDestination Convert(TSource source_object);
    TSource Convert(TDestination source_object);
}

public class PersonCustomerConverter : IConverter<Person, Customer>
{
    public Customer Convert(Person source_object)
    {
        //Do the conversion here. Note that you can have dependencies injected to this class
    }
    public Person Convert(Customer source_object)
    {
        //Do the conversion here. Note that you can have dependencies injected to this class
    }
}

Uważam, że AutoMapper jest trochę za bardzo do robienia naprawdę prostych operacji mapowania.

KC.
źródło