Użyj klasy abstrakcyjnej w języku C # jako definicji

21

Jako programista C ++ jestem przyzwyczajony do plików nagłówkowych C ++ i uważam, że korzystne jest posiadanie pewnego rodzaju wymuszonej „dokumentacji” w kodzie. Zwykle mam zły czas, kiedy muszę czytać kod C # z tego powodu: nie mam takiej mentalnej mapy klasy, z którą pracuję.

Załóżmy, że jako inżynier oprogramowania projektuję framework programu. Czy byłoby zbyt szalone, aby zdefiniować każdą klasę jako abstrakcyjną klasę niezaimplementowaną, podobnie do tego, co zrobilibyśmy z nagłówkami C ++ i pozwolić programistom na jej wdrożenie?

Domyślam się, że może istnieć kilka powodów, dla których ktoś może uznać to za okropne rozwiązanie, ale nie jestem pewien, dlaczego. Co należałoby rozważyć przy takim rozwiązaniu?

Dani Barca Casafont
źródło
13
C # to zupełnie inny język programowania niż C ++. Niektóre składnie wyglądają podobnie, ale musisz zrozumieć język C #, aby wykonać dobrą pracę. I powinieneś coś zrobić ze złym nawykiem, który masz polegając na plikach nagłówkowych. Pracuję z C ++ od dziesięcioleci, nigdy nie czytałem plików nagłówkowych, tylko je zapisywałem.
Wygięty
17
Jedną z najlepszych rzeczy w C # i Javie jest wolność od plików nagłówkowych! Po prostu użyj dobrego IDE.
Erik Eidt
9
Deklaracje w plikach nagłówkowych C ++ nie kończą się jako część wygenerowanego pliku binarnego. Są tam dla kompilatora i linkera. Te abstrakcyjne klasy C # byłyby częścią generowanego kodu, bez żadnej korzyści.
Mark Benningfield
16
Równoważna konstrukcja w języku C # to interfejs, a nie klasa abstrakcyjna.
Robert Harvey
6
@DaniBarcaCasafont „Widzę nagłówki jako szybki i niezawodny sposób na sprawdzenie, jakie są metody i atrybuty klasy, jakich typów parametrów oczekuje i jakie zwraca”. Podczas korzystania z programu Visual Studio moją alternatywą jest skrót Ctrl-MO.
wha7ever - Przywróć Monikę

Odpowiedzi:

44

Powodem, dla którego dokonano tego w C ++, było przyspieszenie i łatwiejsze wdrożenie kompilatorów. To nie był projekt ułatwiający programowanie .

Celem plików nagłówkowych było umożliwienie kompilatorowi wykonania super szybkiego pierwszego przejścia w celu poznania wszystkich oczekiwanych nazw funkcji i przydzielenia im lokalizacji pamięci, aby można było do nich odwoływać się po wywołaniu w plikach cp, nawet jeśli klasa je definiująca miała jeszcze nie zostały przeanalizowane.

Próba odtworzenia konsekwencji starych ograniczeń sprzętowych w nowoczesnym środowisku programistycznym nie jest zalecana!

Zdefiniowanie interfejsu lub klasy abstrakcyjnej dla każdej klasy obniży produktywność; co jeszcze mogłeś zrobić z tym czasem ? Ponadto inni programiści nie będą przestrzegać tej konwencji.

W rzeczywistości inni programiści mogą usunąć twoje abstrakcyjne klasy. Jeśli znajdę interfejs w kodzie, który spełnia oba te kryteria, usuwam go i refaktoryzuję z kodu:1. Does not conform to the interface segregation principle 2. Only has one class that inherits from it

Inną sprawą jest to, że w Visual Studio są narzędzia, które robią to, co chcesz osiągnąć automatycznie:

  1. The Class View
  2. The Object Browser
  3. W Solution Explorermożesz kliknąć trójkąty, aby wydać klasy, aby zobaczyć ich funkcje, parametry i typy zwrotów.
  4. Pod kartami plików znajdują się trzy małe rozwijane menu, najbardziej odpowiednie z nich zawiera listę wszystkich członków bieżącej klasy.

Wypróbuj powyższe przed poświęceniem czasu na replikację plików nagłówkowych C ++ w języku C #.


Co więcej, istnieją techniczne powody, aby tego nie robić ... sprawi, że Twój ostateczny plik binarny będzie większy, niż powinien. Powtórzę ten komentarz Mark Benningfield:

Deklaracje w plikach nagłówkowych C ++ nie kończą się jako część wygenerowanego pliku binarnego. Są tam dla kompilatora i linkera. Te abstrakcyjne klasy C # byłyby częścią generowanego kodu, bez żadnej korzyści.


Ponadto, wspomniany przez Roberta Harveya, technicznie najbliższym odpowiednikiem nagłówka w C # byłby interfejs, a nie klasa abstrakcyjna.

TheCatWhisperer
źródło
2
Skończyłoby się również z wieloma niepotrzebnymi wirtualnymi wywołaniami wysyłki (co nie jest dobre, jeśli twój kod jest wrażliwy na wydajność).
Lucas Trzesniewski
5
oto przywołana zasada segregacji interfejsu .
NH.
2
Można również po prostu zwinąć całą klasę (prawy przycisk myszy -> Tworzenie konspektu -> Zwiń do definicji), aby uzyskać widok z lotu ptaka.
jpmc26
„co jeszcze mogłeś zrobić z tym czasem” -> Uhm, kopiuj i wklej i bardzo, naprawdę drobna praca? Zajmuje mi to około 3 sekund, jeśli w ogóle. Oczywiście mógłbym użyć tego czasu, aby zamiast tego wyjąć końcówkę filtra w celu przygotowania papierosa. Nie bardzo istotny punkt. [Oświadczenie: Uwielbiam zarówno idiomatic C #, jak i idiomatic C ++ i kilka innych języków]
fresnel
1
@phresnel: Chciałbym zobaczyć, jak próbujesz powtórzyć definicje klasy i odziedziczyć klasę oryginalną od abstrakcyjnej w 3s, potwierdzone bez błędów, dla każdej klasy, która jest wystarczająco duża, aby nie mieć łatwego widoku z lotu ptaka to (ponieważ jest to proponowany przypadek użycia PO: rzekomo nieskomplikowane w inny sposób skomplikowane klasy). Niemal z natury rzeczy, skomplikowana natura oryginalnej klasy oznacza, że ​​nie można po prostu napisać abstrakcyjnej definicji klasy na pamięć (ponieważ oznaczałoby to, że znasz ją już na pamięć), a następnie wziąć pod uwagę czas potrzebny na zrobienie tego dla całej bazy kodu.
Flater
14

Po pierwsze, zrozum, że czysto abstrakcyjna klasa jest tak naprawdę interfejsem, który nie może wykonywać wielokrotnego dziedziczenia.

Napisz klasę, wyodrębnij interfejs. Tak bardzo, że mamy na to refaktoryzację. Szkoda. Po tym, że wzorzec „każda klasa dostaje interfejs” nie tylko powoduje bałagan, ale całkowicie nie ma sensu.

Interfejsu nie należy uważać za po prostu formalne przekształcenie tego, co klasa może zrobić. Interfejs należy traktować jako umowę narzuconą przez użycie kodu klienta szczegółowo opisującego jego potrzeby.

Nie mam żadnych problemów z napisaniem interfejsu, który obecnie ma tylko jedną klasę implementującą go. Nie obchodzi mnie to, czy żadna klasa jeszcze tego nie implementuje. Ponieważ myślę o tym, czego potrzebuje mój kod. Interfejs wyraża wymagania związane z użyciem kodu. Cokolwiek przyjdzie później, może robić, co lubi, o ile spełnia te oczekiwania.

Teraz nie robię tego za każdym razem, gdy jeden obiekt używa innego. Robię to, gdy przekraczam granicę. Robię to, gdy nie chcę, aby jeden obiekt dokładnie wiedział, z którym innym przedmiotem rozmawia. To jedyny sposób, w jaki zadziała polimorfizm. Robię to, gdy oczekuję, że obiekt, o którym mówi mój kod klienta, prawdopodobnie się zmieni. Z pewnością nie robię tego, gdy używam klasy String. Klasa String jest ładna i stabilna i nie czuję potrzeby, aby się przed nią nie zmieniać.

Decydując się na bezpośrednią interakcję z konkretną implementacją, a nie poprzez abstrakcję, przewidujesz, że implementacja jest wystarczająco stabilna, aby zaufać, że się nie zmieni.

Właśnie w ten sposób hartuję zasadę inwersji zależności . Nie powinieneś ślepo fanatycznie stosować tego do wszystkiego. Kiedy dodajesz abstrakcję, naprawdę mówisz, że nie ufasz, że klasa implementacyjna ma być stabilna przez cały czas trwania projektu.

To wszystko zakłada, że ​​próbujesz postępować zgodnie z zasadą otwartej zamkniętej . Ta zasada jest ważna tylko wtedy, gdy koszty związane z wprowadzaniem bezpośrednich zmian w ustalonym kodzie są znaczne. Jednym z głównych powodów, dla których ludzie nie zgadzają się co do tego, jak ważne jest odsprzęganie obiektów, jest to, że nie wszyscy ponoszą takie same koszty przy dokonywaniu bezpośrednich zmian. Jeśli ponowne testowanie, ponowna kompilacja i redystrybucja całej bazy kodu jest dla ciebie trywialna, wówczas zaspokojenie potrzeby zmiany za pomocą bezpośredniej modyfikacji jest prawdopodobnie bardzo atrakcyjnym uproszczeniem tego problemu.

Po prostu nie ma mózgowej odpowiedzi na to pytanie. Interfejs lub klasa abstrakcyjna nie jest czymś, co powinieneś dodać do każdej klasy i nie możesz po prostu policzyć liczby klas implementujących i zdecydować, że nie jest to konieczne. Chodzi o radzenie sobie ze zmianami. Co oznacza, że ​​przewidujesz przyszłość. Nie zdziw się, jeśli pomylisz się. Uprość to, jak to możliwe, bez cofania się w kąt.

Dlatego nie pisz abstrakcji, aby pomóc nam w czytaniu kodu. Mamy do tego narzędzia. Wykorzystaj abstrakcje, aby oddzielić to, co wymaga oddzielenia.

candied_orange
źródło
5

Tak, byłoby to okropne, ponieważ (1) Wprowadza niepotrzebny kod (2) Wprowadzi czytelnika w błąd.

Jeśli chcesz programować w C #, powinieneś po prostu przyzwyczaić się do czytania w C #. Kod napisany przez inne osoby i tak nie będzie zgodny z tym wzorem.

JacquesB
źródło
1

Testy jednostkowe

Zdecydowanie sugerowałbym pisanie testów jednostkowych zamiast zaśmiecania kodu interfejsami lub klasami abstrakcyjnymi (z wyjątkiem przypadków uzasadnionych z innych powodów).

Dobrze napisany test jednostkowy nie tylko opisuje interfejs twojej klasy (jak zrobiłby to plik nagłówkowy, klasa abstrakcyjna lub interfejs), ale także opisuje, na przykład, pożądaną funkcjonalność.

Przykład: Gdzie mógłbyś napisać plik nagłówkowy myclass.h, taki jak ten:

class MyClass
{
public:
  void foo();
};

Zamiast tego w c # napisz taki test:

[TestClass]
public class MyClassTests
{
    [TestMethod]
    public void MyClass_should_have_method_Foo()
    {
        //Arrange
        var myClass = new MyClass();
        //Act
        myClass.Foo();
        //Verify
        Assert.Inconclusive("TODO: Write a more detailed test");
    }
}

Ten bardzo prosty test przekazuje te same informacje, co plik nagłówkowy. (Powinniśmy mieć klasę o nazwie „MyClass” z funkcją parametryczną „Foo”). Chociaż plik nagłówkowy jest bardziej zwarty, test zawiera znacznie więcej informacji.

Zastrzeżenie: taki proces polegający na tym, że starszy inżynier oprogramowania testuje (nie zalicza się) testy dla innych programistów, aby brutalnie rozwiązywać konflikty z metodologiami takimi jak TDD, ale w twoim przypadku byłaby to ogromna poprawa.

Guran
źródło
W jaki sposób wykonuje się testy jednostkowe (testy izolowane) = potrzebne makiety bez interfejsów? Jakie ramy obsługują kpiny z rzeczywistej implementacji, większość, które widziałem, używa interfejsu do zamiany implementacji na próbną implementację.
Bulan
Nie sądzę, żeby kłamstwa były potrzebne do celów PO. Pamiętaj, że nie są to testy jednostkowe jako narzędzia TDD, ale testy jednostkowe jako zamiennik plików nagłówkowych. (Mogą oczywiście ewoluować w „zwykłe” testy jednostkowe z próbami itp.)
Guran
Ok, jeśli wezmę pod uwagę tylko PO, rozumiem lepiej. Przeczytaj swoją odpowiedź jako bardziej ogólną odpowiedź dotyczącą stosowania. Dzięki za wyjaśnienie!
Bulan
@Bulan potrzeba częstego kpienia często wskazuje na zły projekt
TheCatWhisperer
@TheCatWhisperer Tak, jestem tego świadomy, więc nie ma argumentu, nie widzę, że nigdzie tego nie zrobiłem: DI mówił o testowaniu w ogóle, że wszystkie frameworki, których użyłem, używają interfejsów do zamiany poza rzeczywistą implementacją i jeśli istniała inna technika, w jaki sposób próbowałaś się wyśmiewać, jeśli nie masz interfejsów.
Bulan