Załóżmy, że mam dwie klasy, które wyglądają tak (pierwszy blok kodu i ogólny problem są związane z C #):
class A
{
public int IntProperty { get; set; }
}
class B
{
public int IntProperty { get; set; }
}
Klasy te nie mogą być w żaden sposób zmieniane (są częścią zespołu zewnętrznego). Dlatego nie mogę zmusić ich do wdrożenia tego samego interfejsu ani dziedziczenia tej samej klasy, która zawierałaby IntProperty.
Chcę zastosować logikę do IntProperty
właściwości obu klas, aw C ++ mógłbym użyć klasy szablonów, aby to zrobić dość łatwo:
template <class T>
class LogicToBeApplied
{
public:
void T CreateElement();
};
template <class T>
T LogicToBeApplied<T>::CreateElement()
{
T retVal;
retVal.IntProperty = 50;
return retVal;
}
A potem mógłbym zrobić coś takiego:
LogicToBeApplied<ClassA> classALogic;
LogicToBeApplied<ClassB> classBLogic;
ClassA classAElement = classALogic.CreateElement();
ClassB classBElement = classBLogic.CreateElement();
W ten sposób mogłem stworzyć jedną ogólną klasę fabryczną, która działałaby zarówno dla ClassA, jak i ClassB.
Jednak w języku C # muszę napisać dwie klasy z dwoma różnymi where
klauzulami, mimo że kod dla logiki jest dokładnie taki sam:
public class LogicAToBeApplied<T> where T : ClassA, new()
{
public T CreateElement()
{
T retVal = new T();
retVal.IntProperty = 50;
return retVal;
}
}
public class LogicBToBeApplied<T> where T : ClassB, new()
{
public T CreateElement()
{
T retVal = new T();
retVal.IntProperty = 50;
return retVal;
}
}
Wiem, że jeśli chcę mieć różne klasy w where
klauzuli, muszą one być powiązane, tj. Aby odziedziczyć tę samą klasę, jeśli chcę zastosować do nich ten sam kod w sensie, który opisałem powyżej. Po prostu dwie denerwujące metody są bardzo denerwujące. Nie chcę też używać refleksji z powodu problemów z wydajnością.
Czy ktoś może zasugerować jakieś podejście, w którym można to napisać w bardziej elegancki sposób?
Odpowiedzi:
Dodaj interfejs proxy (czasami nazywany adapterem , czasami z niewielkimi różnicami), zaimplementuj go
LogicToBeApplied
w kategoriach proxy, a następnie dodaj sposób na zbudowanie instancji tego proxy z dwóch lambd: jednego dla get get i jednego dla zestawu.Teraz, ilekroć potrzebujesz przekazać IProxy, ale masz instancję klasy innej firmy, możesz po prostu przekazać niektóre lambdy:
Ponadto możesz pisać proste pomocniki do tworzenia instancji LamdaProxy z instancji A lub B. Mogą to być nawet metody rozszerzeń, które zapewnią ci „płynny” styl:
A teraz konstrukcja serwerów proxy wygląda następująco:
Jeśli chodzi o twoją fabrykę, zobaczyłbym, czy możesz ją przekształcić w „główną” metodę fabryczną, która akceptuje IProxy i wykonuje na nim całą logikę oraz inne metody, które właśnie przechodzą
new A().Proxied()
lubnew B().Proxied()
:Nie ma możliwości zrobienia odpowiednika kodu C ++ w C #, ponieważ szablony C ++ opierają się na typowaniu strukturalnym . Tak długo, jak dwie klasy mają tę samą nazwę i sygnaturę metody, w C ++ możesz wywoływać tę metodę ogólnie na obu z nich. C # ma typowe wpisywanie - nazwa klasy lub interfejsu jest częścią jego typu. Dlatego klasy
A
iB
nie mogą być traktowane tak samo w żadnym przypadku, chyba że jawna relacja „jest” jest zdefiniowana przez dziedziczenie lub implementację interfejsu.Jeśli podstawowa implementacja tych metod dla każdej klasy jest zbyt duża, możesz napisać funkcję, która pobiera obiekt i buduje w sposób refleksyjny
LambdaProxy
, szukając określonej nazwy właściwości:Nie udaje się to fatalnie, gdy dane obiekty są niepoprawnego typu; refleksja z natury wprowadza możliwość awarii, których system typu C # nie może zapobiec. Na szczęście możesz uniknąć refleksji, dopóki obciążenie związane z utrzymaniem pomocników nie stanie się zbyt duże, ponieważ nie musisz modyfikować interfejsu IProxy ani implementacji LambdaProxy, aby dodać cukier refleksyjny.
Jednym z powodów, dla których to działa,
LambdaProxy
jest „maksymalnie ogólny”; może dostosować dowolną wartość, która implementuje „ducha” kontraktu IProxy, ponieważ implementacja LambdaProxy jest całkowicie zdefiniowana przez dane funkcje pobierające i ustawiające. Działa nawet, jeśli klasy mają różne nazwy właściwości lub różne typy, które są rozsądnie i bezpiecznie reprezentowane jakoint
s, lub jeśli istnieje jakiś sposób odwzorowania pojęcia, któreProperty
ma reprezentować dowolne inne cechy klasy. Pośrednictwo zapewniane przez funkcje zapewnia maksymalną elastyczność.źródło
ReflectiveProxier
można zbudować serwer proxydynamic
? Wydaje mi się, że miałbyś te same podstawowe problemy (tj. Błędy, które są wychwytywane tylko w czasie wykonywania), ale składnia i łatwość konserwacji byłyby znacznie prostsze.Oto zarys korzystania z adapterów bez dziedziczenia po A i / lub B, z możliwością użycia ich dla istniejących obiektów A i B:
Zwykle wolałbym ten rodzaj adaptera obiektowego niż proxy klasy, unikają one brzydkich problemów, na które można natknąć się z dziedziczeniem. Na przykład to rozwiązanie będzie działać, nawet jeśli A i B są klasami zapieczętowanymi.
źródło
new int Property
? niczego nie zasłaniasz.Możesz dostosować
ClassA
iClassB
za pomocą wspólnego interfejsu. W ten sposób Twój kodLogicAToBeApplied
pozostanie taki sam. Jednak niewiele różni się od tego, co masz.źródło
A
,B
typy do wspólnego interfejsu. Dużą zaletą jest to, że nie musimy powielać wspólnej logiki. Wadą jest to, że logika tworzy teraz instancję opakowania / proxy zamiast rzeczywistego typu.LogicToBeApplied
ma pewną złożoność i nie powinno się go powtarzać w dwóch miejscach w bazie kodu pod żadnym pozorem. Wówczas dodatkowy kod płyty kotłowej jest często nieistotny.Wersja C ++ działa tylko dlatego, że jej szablony używają „statycznego pisania kaczego” - wszystko się kompiluje, o ile typ zawiera poprawne nazwy. To bardziej przypomina system makr. Ogólny system C # i innych języków działa zupełnie inaczej.
Odpowiedzi devnulla i Doca Browna pokazują, w jaki sposób można użyć wzorca adaptera, aby utrzymać ogólny algorytm i nadal działać na dowolnych typach… z kilkoma ograniczeniami. W szczególności tworzysz teraz inny typ niż chcesz.
Przy odrobinie podstępu możliwe jest użycie dokładnie zamierzonego typu bez żadnych zmian. Jednak teraz musimy wyodrębnić wszystkie interakcje z typem docelowym do osobnego interfejsu. Tutaj te interakcje to budowa i przypisanie własności:
W interpretacji OOP byłby to przykład wzorca strategii , choć mieszany z rodzajami.
Następnie możemy przepisać Twoją logikę, aby użyć tych interakcji:
Definicje interakcji wyglądałyby następująco:
Dużą wadą tego podejścia jest to, że programista musi napisać i przekazać instancję interakcji podczas wywoływania logiki. Jest to dość podobne do rozwiązań opartych na wzorcach adapterów, ale jest nieco bardziej ogólne.
Z mojego doświadczenia wynika, że jest to najbliższa funkcja szablonów w innych językach. Podobne techniki są używane w Haskell, Scala, Go i Rust do implementacji interfejsów poza definicją typu. Jednak w tych językach kompilator wkracza i domyślnie wybiera poprawną instancję interakcji, aby w rzeczywistości nie zobaczyć dodatkowego argumentu. Jest to również podobne do metod rozszerzenia C #, ale nie jest ograniczone do metod statycznych.
źródło
Jeśli naprawdę chcesz rzucić ostrożność na wiatr, możesz użyć „dynamiki”, aby kompilator zadbał o wszystkie nieprzyjemności odbicia. Spowoduje to błąd środowiska wykonawczego, jeśli przekażesz obiekt do SetSomeProperty, który nie ma właściwości o nazwie SomeProperty.
źródło
Pozostałe odpowiedzi poprawnie identyfikują problem i zapewniają wykonalne rozwiązania. C # nie (ogólnie) obsługuje „pisanie kaczek” („Jeśli chodzi jak kaczka ...”), więc nie ma sposobu, aby wymusić twoje
ClassA
iClassB
być wymiennym, jeśli nie zostały zaprojektowane w ten sposób.Jeśli jednak jesteś gotów zaakceptować ryzyko wystąpienia błędu w czasie wykonywania, istnieje łatwiejsza odpowiedź niż użycie Reflection.
C # ma
dynamic
słowo kluczowe, które jest idealne w takich sytuacjach. Mówi kompilatorowi: „Nie będę wiedział, jaki to typ, dopóki nie zostanie uruchomiony (a może nawet wtedy), więc pozwólcie mi cokolwiek z tym zrobić”.Korzystając z tego, możesz zbudować dokładnie taką funkcję, jaką chcesz:
Zwróć także uwagę na użycie
static
słowa kluczowego. Dzięki temu możesz użyć tego jako:dynamic
Korzystanie z Refleksji nie ma wpływu na wydajność , ponieważ jest to jednorazowe działanie (i dodatkowa złożoność). Za pierwszym razem, gdy Twój kod uderzy w wywołanie dynamiczne określonego typu, będzie miał niewielki narzut , ale powtarzane połączenia będą tak samo szybkie, jak kod standardowy. Jednak będzie uzyskaćRuntimeBinderException
, jeśli starają się przechodzić w coś, co nie ma tej własności, i nie ma dobry sposób, aby sprawdzić, czy z wyprzedzeniem. Być może warto w konkretny sposób poradzić sobie z tym błędem.źródło
Możesz użyć odbicia, aby wyciągnąć właściwość według nazwy.
Oczywiście tą metodą ryzykujesz błąd w czasie wykonywania. Właśnie to C # próbuje powstrzymać.
Czytałem gdzieś, że przyszła wersja C # pozwoli ci przekazywać obiekty jako interfejs, którego nie dziedziczą, ale pasują do siebie. Co również rozwiązałoby twój problem.
(Spróbuję wykopać artykuł)
Inną metodą, chociaż nie jestem pewien, czy oszczędza ci to żadnego kodu, byłoby podklasowanie A i B, a także dziedziczenie interfejsu z IntProperty.
źródło
Chciałem po prostu użyć
implicit operator
konwersji razem z podejściem delegata / lambda odpowiedzi Jacka.A
iB
są zgodne z założeniami:Dzięki temu łatwo jest uzyskać ładną składnię z niejawnymi konwersjami zdefiniowanymi przez użytkownika (nie są wymagane żadne metody rozszerzenia ani podobne):
Ilustracja użycia:
Te
Initialize
metody wykazuje, w jaki sposób można pracować zAdapter
nie dbając o to, czy jest toA
lubB
czy coś innego. WywołanieInitialize
metody pokazuje, że nie potrzebujemy żadnego (widocznego) odlewu.AsProxy()
lub podobnego materiału do obróbki betonuA
lubB
jakoAdapter
.Zastanów się, czy chcesz wrzucić
ArgumentNullException
konwersje zdefiniowane przez użytkownika, jeśli przekazany argument jest odwołaniem zerowym, czy nie.źródło