Piszę zajęcia, które „muszą być używane w określony sposób” (chyba wszystkie klasy muszą…).
Na przykład, utworzyć fooManager
klasę, która wymaga połączenia do, powiedzmy Initialize(string,string)
. I, idąc dalej za przykładem, klasa byłaby bezużyteczna, gdybyśmy nie słuchali jej ThisHappened
akcji.
Chodzi mi o to, że klasa, którą piszę, wymaga wywołań metod. Ale skompiluje się dobrze, jeśli nie wywołasz tych metod i skończy się pustym nowym FooManager. W pewnym momencie albo nie zadziała, albo może się zawiesi, w zależności od klasy i tego, co robi. Programista, który implementuje moją klasę, oczywiście zajrzy do niej i zrozumie „Och, nie zadzwoniłem do Initialize!”, I byłoby dobrze.
Ale mi się to nie podoba. Idealnie chciałbym, aby kod NIE kompilował się, gdyby metoda nie została wywołana; Zgaduję, że to po prostu niemożliwe. Lub coś, co natychmiast byłoby widoczne i jasne.
Niepokoi mnie obecne podejście, które mam tutaj:
Dodaj prywatną wartość logiczną do klasy i sprawdzaj wszędzie, gdzie jest to konieczne, czy klasa została zainicjowana; jeśli nie, wyrzucę wyjątek z informacją: „Klasa nie została zainicjowana, czy na pewno dzwonisz .Initialize(string,string)
?”.
Takie podejście jest w porządku, ale prowadzi do kompilacji kodu i ostatecznie nie jest konieczne dla użytkownika końcowego.
Czasami jest to nawet więcej kodu, gdy jest więcej metod niż tylko Initiliaze
wywołanie. Staram się, aby moje zajęcia były prowadzone przy użyciu niezbyt wielu publicznych metod / działań, ale to nie rozwiązuje problemu, a jedynie utrzymuje rozsądek.
Szukam tutaj:
- Czy moje podejście jest prawidłowe?
- Czy jest lepszy?
- Co robicie / doradzacie?
- Czy próbuję rozwiązać problem? Koledzy powiedzieli mi, że programista powinien sprawdzić klasę, zanim spróbuje z niej skorzystać. Z szacunkiem się nie zgadzam, ale wierzę, że to kolejna sprawa.
Mówiąc prościej, próbuję znaleźć sposób, aby nigdy nie zapomnieć o implementacji wywołań, gdy ta klasa zostanie ponownie użyta później lub przez kogoś innego.
WYJAŚNIENIA:
Aby wyjaśnić wiele pytań tutaj:
Zdecydowanie NIE mówię tylko o części zajęć z Inicjalizacji, ale raczej o całym jej życiu. Zapobiegaj kolegom wywoływania metody dwa razy, upewniając się, że wywołują X przed Y itp. Wszystko, co skończyłoby się obowiązkiem i dokumentacją, ale chciałbym, żeby było to w kodzie, tak proste i małe, jak to możliwe. Bardzo podobał mi się pomysł Asserts, choć jestem pewien, że będę musiał wymieszać kilka innych pomysłów, ponieważ Asserts nie zawsze będą możliwe.
Używam języka C #! Jak o tym nie wspomniałem ?! Jestem w środowisku Xamarin i buduję aplikacje mobilne, zwykle wykorzystując około 6 do 9 projektów w rozwiązaniu, w tym projekty PCL, iOS, Android i Windows. Jestem programistą przez około półtora roku (szkoła i praca łącznie), stąd moje czasem śmieszne wypowiedzi / pytania. To chyba nie ma znaczenia, ale zbyt wiele informacji nie zawsze jest złą rzeczą.
Nie zawsze mogę umieścić w konstruktorze wszystko, co jest obowiązkowe, ze względu na ograniczenia platformy i użycie wstrzykiwania zależności, ponieważ parametry inne niż interfejsy są poza tabelą. A może moja wiedza nie jest wystarczająca, co jest wysoce możliwe. Przez większość czasu nie jest to kwestia inicjalizacji, ale więcej
jak mogę się upewnić, że zarejestrował się na tym wydarzeniu?
jak mogę się upewnić, że nie zapomniał „zatrzymać procesu w pewnym momencie”
Tutaj pamiętam zajęcia z pobierania reklam. Tak długo, jak widoczny jest widok reklamy, klasa co minutę pobiera nową reklamę. Ta klasa potrzebuje widoku, gdy jest skonstruowana w miejscu, w którym może wyświetlać reklamę, która może oczywiście zawierać parametr. Ale gdy widok zniknie, należy wywołać StopFetching (). W przeciwnym razie klasa nadal pobierałaby reklamy dla widoku, którego nawet tam nie ma, a to źle.
Ponadto w tej klasie znajdują się zdarzenia, które należy wysłuchać, na przykład „AdClicked”. Wszystko działa dobrze, jeśli nie jest słuchane, ale tracimy śledzenie danych analitycznych, jeśli krany nie są zarejestrowane. Reklama nadal działa, więc użytkownik i programista nie zobaczą różnicy, a dane analityczne będą po prostu mieć nieprawidłowe dane. Trzeba tego unikać, ale nie jestem pewien, skąd programista może wiedzieć, że musi zarejestrować się na wydarzeniu tao. Jest to jednak uproszczony przykład, ale istnieje idea „upewnij się, że korzysta z publicznej Akcji, która jest dostępna” i oczywiście w odpowiednim czasie!
initialize
należy wykonać późno po utworzeniu obiektu? Czy ctor byłby zbyt „ryzykowny” w tym sensie, że mógłby rzucać wyjątki i przerywać łańcuch tworzenia?new
jeśli obiekt nie jest gotowy do użycia. Poleganie na metodzie inicjalizacji z pewnością będzie cię prześladować i powinno się jej unikać, chyba że jest to absolutnie konieczne, przynajmniej takie jest moje doświadczenie.Odpowiedzi:
W takich przypadkach najlepiej jest użyć systemu pisma w swoim języku, aby pomóc w prawidłowej inicjalizacji. Jak możemy zapobiec
FooManager
przed wykorzystywane bez zainicjowana? , Przez zapobieganieFooManager
przed stworzony bez informacji niezbędnych do prawidłowego go zainicjować. W szczególności za wszelką inicjalizację odpowiedzialny jest konstruktor. Nigdy nie należy pozwalać konstruktorowi tworzyć obiektu w stanie nielegalnym.Ale dzwoniący muszą zbudować,
FooManager
zanim będą mogli zainicjować, np. PonieważFooManager
jest przekazywany jako zależność.Nie twórz,
FooManager
jeśli go nie masz. Zamiast tego możesz przekazać obiekt, który pozwala pobrać w pełni skonstruowanyFooManager
, ale tylko z informacjami o inicjalizacji. (Mówiąc w programowaniu funkcjonalnym, sugeruję użycie częściowej aplikacji dla konstruktora.) Np .:Problem polega na tym, że musisz podawać informacje o inicjowaniu za każdym razem, gdy uzyskujesz dostęp do
FooManager
.Jeśli jest to konieczne w twoim języku, możesz zawinąć
getFooManager()
operację w klasę przypominającą fabrykę lub konstruktora.Naprawdę chcę sprawdzać w czasie wykonywania, czy
initialize()
metoda została wywołana, zamiast używać rozwiązania na poziomie systemu.Można znaleźć kompromis. Tworzymy klasę opakowującą,
MaybeInitializedFooManager
która maget()
metodę, która zwracaFooManager
, ale wyrzuca, jeśliFooManager
nie została w pełni zainicjowana. Działa to tylko wtedy, gdy inicjalizacja jest wykonywana przez opakowanie lub jeśli istniejeFooManager#isInitialized()
metoda.Nie chcę zmieniać interfejsu API mojej klasy.
W takim przypadku będziesz chciał uniknąć
if (!initialized) throw;
warunków warunkowych w każdej metodzie. Na szczęście istnieje prosty wzorzec rozwiązania tego problemu.Obiekt, który udostępniasz użytkownikom, jest pustą powłoką, która deleguje wszystkie wywołania do obiektu implementacji. Domyślnie obiekt implementacji zgłasza błąd dla każdej metody, że nie został zainicjowany. Jednak
initialize()
metoda zastępuje obiekt implementacji w pełni zbudowanym obiektem.Wyodrębnia to główne zachowanie klasy do
MainImpl
.źródło
FooManager
iUninitialisedImpl
jest lepsza niż powtarzanieif (!initialized) throw;
.FooManager
ma wiele metod, może być łatwiejsze niż potencjalne pominięcie niektórychif (!initialized)
kontroli. Ale w takim przypadku prawdopodobnie wolisz rozbić klasę.Najbardziej skutecznym i pomocnym sposobem zapobiegania „niewłaściwemu” użyciu obiektu przez klientów jest uniemożliwienie go.
Najprostszym rozwiązaniem jest połączenie
Initialize
z konstruktorem. W ten sposób obiekt nigdy nie będzie dostępny dla klienta w stanie niezainicjowanym, więc błąd nie jest możliwy. Jeśli nie możesz zainicjować samego konstruktora, możesz utworzyć metodę fabryczną. Na przykład, jeśli twoja klasa wymaga rejestracji określonych zdarzeń, możesz wymagać detektora zdarzeń jako parametru w sygnaturze konstruktora lub metody fabrycznej.Jeśli musisz mieć dostęp do niezainicjowanego obiektu przed inicjalizacją, możesz mieć zaimplementowane dwa stany jako osobne klasy, więc zaczynasz od
UnitializedFooManager
instancji, która maInitialize(...)
metodę, która zwracaInitializedFooManager
. Metody, które można wywołać tylko w stanie zainicjowanym, istnieją tylko naInitializedFooManager
. W razie potrzeby to podejście można rozszerzyć na wiele stanów.W porównaniu z wyjątkami środowiska wykonawczego reprezentowanie stanów jako odrębnych klas wymaga więcej pracy, ale daje także gwarancję czasu kompilacji, że nie wywołujesz metod, które są nieprawidłowe dla stanu obiektu, i dokumentuje przejścia stanu w kodzie jaśniej.
Ale ogólnie rzecz biorąc, idealnym rozwiązaniem jest zaprojektowanie klas i metod bez ograniczeń, takich jak wymaganie od ciebie wywoływania metod w określonym czasie i określonej kolejności. Nie zawsze jest to możliwe, ale w wielu przypadkach można tego uniknąć, stosując odpowiedni wzór.
Jeśli masz złożone sprzężenie czasowe (kilka metod musi być wywoływanych w określonej kolejności), jednym rozwiązaniem może być odwrócenie kontroli , więc tworzysz klasę, która wywołuje metody w odpowiedniej kolejności, ale używa metod szablonów lub zdarzeń aby umożliwić klientowi wykonywanie niestandardowych operacji na odpowiednich etapach przepływu. W ten sposób odpowiedzialność za wykonywanie operacji w odpowiedniej kolejności jest przenoszona z klienta na samą klasę.
Prosty przykład: masz obiekt
File
-object, który pozwala czytać z pliku. Jednak klient musi zadzwonić,Open
zanimReadLine
będzie można wywołać metodę, i pamiętaj, aby zawsze wywoływaćClose
(nawet jeśli wystąpi wyjątek), po czymReadLine
nie można już wywoływać metody. To jest sprzężenie czasowe. Można tego uniknąć, stosując jedną metodę, która przyjmuje jako wywołanie wywołanie zwrotne lub delegację. Metoda zarządza otwarciem pliku, wywołaniem zwrotnym, a następnie zamknięciem pliku. Może przekazać odrębny interfejs zRead
metodą do wywołania zwrotnego. W ten sposób klient nie może zapomnieć o wywołaniu metod we właściwej kolejności.Interfejs ze sprzężeniem czasowym:
Bez sprzężenia czasowego:
Bardziej ciężkim rozwiązaniem byłoby zdefiniowanie
File
jako klasy abstrakcyjnej, która wymaga implementacji (chronionej) metody, która zostanie wykonana na otwartym pliku. Jest to nazywane wzorcem metody szablonu .Zaleta jest taka sama: klient nie musi pamiętać, aby wywoływać metody w określonej kolejności. Po prostu niemożliwe jest wywoływanie metod w niewłaściwej kolejności.
źródło
UnitializedFooManager
) nie może współużytkować stanu z instancjami (InitializedFooManager
), tak jakInitialize(...)
ma to rzeczywiścieInstantiate(...)
znaczenie semantyczne. W przeciwnym razie ten przykład prowadzi do nowych problemów, jeśli programista użyje dwukrotnie tego samego szablonu. I to nie jest możliwe, aby zapobiec / zweryfikować statycznie, jeśli język nie obsługujemove
semantyki w celu niezawodnego wykorzystania szablonu.Zwykle sprawdzałbym po inicjalizacji i rzucałbym (powiedzmy),
IllegalStateException
jeśli spróbujesz użyć go bez inicjalizacji.Jeśli jednak chcesz być bezpieczny w czasie kompilacji (a jest to godne pochwały i preferowane), dlaczego nie traktować inicjalizacji jako metody fabrycznej zwracającej skonstruowany i zainicjowany obiekt np.
i tak Ci kontrolować tworzenie obiektów i cykl życia, a klienci uzyskać
Component
w wyniku swojej inicjalizacji. To skutecznie leniwa instancja.źródło
IllegalStateException
.IllegalStateException
lubInvalidOperationException
nieoczekiwanie, użytkownik nigdy nie zrozumie, co zrobił źle. Te wyjątki nie powinny stać się obejściem w naprawianiu błędów projektowych.Oderwę się trochę od innych odpowiedzi i nie zgodzę się z tym: nie da się na nie odpowiedzieć bez znajomości języka, w którym pracujesz. Czy jest to opłacalny plan, czy też właściwe „ostrzeżenie” Twoi użytkownicy zależą całkowicie od mechanizmów udostępnianych przez Twój język i konwencji, których inni programiści tego języka używają.
Jeśli masz
FooManager
w Haskell, przestępstwem byłoby zezwolenie użytkownikom na zbudowanie takiego, który nie jest w stanie zarządzaćFoo
s, ponieważ system typów sprawia, że jest to takie proste i są to konwencje, których oczekują programiści Haskell.Z drugiej strony, jeśli piszesz w C, Twoi współpracownicy mieliby pełne prawo do wycofania się z ciebie i zastrzelenia cię za zdefiniowanie osobnych
struct FooManager
istruct UninitializedFooManager
typów, które obsługują różne operacje, ponieważ prowadziłoby to do niepotrzebnie złożonego kodu z bardzo niewielką korzyścią .Jeśli piszesz w Pythonie, nie ma mechanizmu pozwalającego Ci to zrobić.
Prawdopodobnie nie piszesz w języku Haskell, Python lub C, ale są to przykładowe przykłady pod względem tego, ile / jak mało pracy oczekuje system typów i jaki jest w stanie wykonać.
Podążaj za rozsądnymi oczekiwaniami programisty w swoim języku i oprzyj się nadmiernej inżynierii rozwiązania, które nie ma naturalnej, idiomatycznej implementacji (chyba że błąd jest tak łatwy do popełnienia i tak trudny do uchwycenia, że warto przejść w skrajność długości, aby było to niemożliwe). Jeśli nie masz wystarczającego doświadczenia w swoim języku, aby ocenić, co jest rozsądne, postępuj zgodnie z radą kogoś, kto zna to lepiej niż ty.
źródło
Ponieważ wydaje się, że nie chcesz wysyłać testu kodu do klienta (ale wydaje się, że możesz to zrobić dla programisty), możesz użyć funkcji asercji , jeśli są one dostępne w twoim języku programowania.
W ten sposób masz kontrole w środowisku programistycznym (i wszelkie testy, które inny programista mógłby nazwać WILL, nie da się przewidzieć), ale nie wyślesz kodu do klienta, ponieważ twierdzenia (przynajmniej w Javie) są kompilowane selektywnie.
Tak więc klasa Java korzystająca z tego wygląda następująco:
Asercje są doskonałym narzędziem do sprawdzania stanu środowiska uruchomieniowego programu, którego potrzebujesz, ale w rzeczywistości nie oczekuj, że ktoś zrobi coś złego w prawdziwym środowisku.
Są też dość szczupłe i nie zajmują dużych części if () throw ... składnia, nie trzeba ich łapać itp.
źródło
Patrząc na ten problem bardziej ogólnie niż obecne odpowiedzi, które skupiły się głównie na inicjalizacji. Rozważ obiekt, który będzie miał dwie metody,
a()
ib()
. Wymagane jest, abya()
zawsze wywoływać wcześniejb()
. Możesz utworzyć sprawdzanie czasu kompilacji, że dzieje się tak, zwracając nowy obiekta()
i przenoszącb()
go do nowego obiektu zamiast do oryginalnego. Przykładowa implementacja:Teraz niemożliwe jest wywołanie b () bez uprzedniego wywołania a (), ponieważ jest ono implementowane w interfejsie, który jest ukryty, dopóki nie zostanie wywołane a (). Trzeba przyznać, że jest to dość duży wysiłek, więc zwykle nie korzystałbym z tego podejścia, ale są sytuacje, w których może on być korzystny (szczególnie, jeśli twoja klasa będzie ponownie wykorzystywana w wielu sytuacjach przez programistów, którzy mogą nie być zaznajomiony z jego implementacją lub w kodzie, w którym niezawodność ma kluczowe znaczenie). Zauważ również, że jest to uogólnienie wzorca konstruktora, jak sugeruje wiele istniejących odpowiedzi. Działa prawie w ten sam sposób, jedyną prawdziwą różnicą jest to, gdzie dane są przechowywane (w oryginalnym obiekcie, a nie w zwróconym) i kiedy jest przeznaczone do użycia (w dowolnym momencie, a nie tylko podczas inicjalizacji).
źródło
Kiedy implementuję klasę podstawową, która wymaga dodatkowych informacji inicjalizacyjnych lub łączy z innymi obiektami, zanim stanie się użyteczna, staram się uczynić tę klasę podstawową abstrakcyjną, a następnie zdefiniować kilka metod abstrakcyjnych w tej klasie, które są używane w przepływie klasy podstawowej (np.
abstract Collection<Information> get_extra_information(args);
iabstract Collection<OtherObjects> get_other_objects(args);
które zgodnie z umową dziedziczenia muszą zostać zaimplementowane przez konkretną klasę, zmuszając użytkownika tej klasy podstawowej do dostarczenia wszystkich rzeczy wymaganych przez klasę podstawową.Tak więc, kiedy wdrażam klasę podstawową, natychmiast i wyraźnie wiem, co muszę napisać, aby klasa podstawowa zachowała się poprawnie, ponieważ po prostu muszę zaimplementować metody abstrakcyjne i to wszystko.
EDYCJA: aby wyjaśnić, jest to prawie to samo, co dostarczanie argumentów do konstruktora klasy podstawowej, ale implementacje metod abstrakcyjnych pozwalają implementacji przetwarzać argumenty przekazywane do wywołań metod abstrakcyjnych. Lub nawet w przypadku braku argumentów, może być nadal użyteczne, jeśli zwracana wartość metody abstrakcyjnej zależy od stanu, który można zdefiniować w treści metody, co nie jest możliwe, gdy zmienna jest przekazywana jako argument do konstruktor. Oczywiście nadal możesz przekazać argument, który będzie zachowywał się dynamicznie w oparciu o tę samą zasadę, jeśli wolisz używać kompozycji zamiast dziedziczenia.
źródło
Odpowiedź brzmi: tak, nie i czasami. :-)
Niektóre z opisanych przez Ciebie problemów można łatwo rozwiązać, przynajmniej w wielu przypadkach.
Sprawdzanie w czasie kompilacji jest z pewnością lepsze niż sprawdzanie w czasie wykonywania, co jest lepsze niż brak sprawdzania w ogóle.
Czas działania RE:
Wymuszanie, aby funkcje były wywoływane w określonej kolejności itp., Jest przynajmniej koncepcyjnie proste przy użyciu kontroli w czasie wykonywania. Wystarczy wstawić flagi, które mówią, która funkcja została uruchomiona, i niech każda funkcja zaczyna się od czegoś takiego jak „jeśli nie warunek wstępny-1 lub warunek wstępny-2, to wyrzuć wyjątek”. Jeśli kontrole stają się skomplikowane, możesz wypchnąć je do funkcji prywatnej.
Możesz sprawić, by niektóre rzeczy działały automatycznie. Na przykład programiści często mówią o „leniwej populacji” obiektu. Podczas tworzenia instancji ustawiasz flagę wypełnioną na wartość false, lub odwołanie do jakiegoś kluczowego obiektu na wartość null. Następnie, gdy wywoływana jest funkcja, która potrzebuje odpowiednich danych, sprawdza flagę. Jeśli ma wartość false, wypełnia dane i ustawia flagę na true. Jeśli to prawda, to po prostu zakłada, że dane tam są. Możesz zrobić to samo z innymi warunkami wstępnymi. Ustaw flagę w konstruktorze lub jako wartość domyślną. Następnie, gdy osiągniesz punkt, w którym powinna zostać wywołana jakaś funkcja warunku wstępnego, jeśli jeszcze nie została wywołana, wywołaj ją. Oczywiście działa to tylko wtedy, gdy masz dane do wywołania go w tym momencie, ale w wielu przypadkach możesz zawrzeć dowolne wymagane dane w parametrach funkcji „
Czas kompilacji RE:
Jak powiedzieli inni, jeśli trzeba zainicjować, zanim będzie można użyć obiektu, wstawi to inicjację do konstruktora. To dość typowa rada dla OOP. Jeśli to w ogóle możliwe, należy zmusić konstruktora do utworzenia prawidłowej, użytecznej instancji obiektu.
Widzę dyskusję na temat tego, co należy zrobić, jeśli trzeba przekazać odniesienia do obiektu przed jego zainicjowaniem. Nie mogę wymyślić prawdziwego przykładu tego z głowy, ale przypuszczam, że to może się zdarzyć. Rozwiązania zostały zasugerowane, ale rozwiązania, które widziałem tutaj i które mogę wymyślić, są nieuporządkowane i komplikują kod. W pewnym momencie musisz zapytać: czy warto tworzyć brzydki, trudny do zrozumienia kod, abyśmy mogli sprawdzać w czasie kompilacji, a nie w czasie wykonywania? Czy też tworzę dużo pracy dla siebie i innych dla celu, który jest miły, ale nie wymagany?
Jeśli dwie funkcje powinny być zawsze uruchamiane razem, prostym rozwiązaniem jest uczynienie z nich jednej funkcji. Jak gdyby za każdym razem, gdy dodajesz reklamę do strony, powinieneś również dodać do niej moduł obsługi metryk, a następnie wstaw kod, aby dodać moduł obsługi metryk wewnątrz funkcji „dodaj reklamę”.
Niektóre rzeczy byłyby prawie niemożliwe do sprawdzenia w czasie kompilacji. Jak wymóg, że pewna funkcja nie może być wywołana więcej niż raz. (Napisałem wiele takich funkcji. Niedawno napisałem funkcję, która wyszukuje pliki w magicznym katalogu, przetwarza znalezione pliki, a następnie usuwa je. Oczywiście po usunięciu pliku nie można go ponownie uruchomić. Itd.) Nie znam żadnego języka, który ma funkcję, która pozwala zapobiec dwukrotnemu wywołaniu funkcji w tej samej instancji podczas kompilacji. Nie wiem, w jaki sposób kompilator mógł ostatecznie dowiedzieć się, że tak się dzieje. Wszystko, co możesz zrobić, to poddać się kontroli czasu wykonywania. To szczególnie kontrola czasu pracy jest oczywista, prawda? msgstr "jeśli już-tu-tutaj = prawda, to wyrzuć wyjątek, inny ustawiony już-już-tutaj = prawda"
RE niemożliwe do wyegzekwowania
Często zdarza się, że klasa wymaga czyszczenia po zakończeniu: zamykania plików lub połączeń, zwalniania pamięci, zapisywania wyników końcowych w bazie danych itp. Nie ma łatwego sposobu na wymuszenie tego w czasie kompilacji lub czas pracy. Myślę, że większość języków OOP ma pewien rodzaj funkcji „finalizatora”, która jest wywoływana, gdy instancja jest zbierana w pamięci, ale myślę, że większość twierdzi również, że nie mogą zagwarantować, że to się kiedykolwiek uruchomi. Języki OOP często zawierają funkcję „dispose” z jakimś przepisem umożliwiającym ustawienie zakresu użycia instancji, klauzulę „using” lub „with”, a kiedy program wychodzi z tego zakresu, uruchamiane jest dispose. Wymaga to jednak od programisty użycia odpowiedniego specyfikatora zakresu. Nie zmusza programisty do robienia tego dobrze,
W przypadkach, w których nie można zmusić programisty do prawidłowego korzystania z klasy, staram się, aby program wybuchł, jeśli zrobi to źle, zamiast podawać nieprawidłowe wyniki. Prosty przykład: widziałem wiele programów, które inicjują wiązkę danych do wartości fikcyjnych, dzięki czemu użytkownik nigdy nie uzyska wyjątku zerowego, nawet jeśli nie wywoła funkcji w celu prawidłowego zapełnienia danych. I zawsze zastanawiam się dlaczego. Robisz wszystko, aby trudniej było znaleźć błędy. Jeśli programista nie zadzwoni do funkcji „ładuj dane”, a następnie beztrosko spróbuje użyć danych, otrzyma wyjątek wskaźnika zerowego, wyjątek ten szybko pokaże mu, gdzie jest problem. Ale jeśli ukryjesz go, czyniąc dane pustymi lub zerowymi w takich przypadkach, program może uruchomić się do końca, ale wygenerować niedokładne wyniki. W niektórych przypadkach programista może nawet nie zdawać sobie sprawy, że wystąpił problem. Jeśli to zauważy, być może będzie musiał przejść długą ścieżkę, aby znaleźć miejsce, w którym popełnił błąd. Lepiej wcześniej zawieść, niż odważnie kontynuować.
Ogólnie rzecz biorąc, zawsze dobrze jest, gdy niewłaściwe użycie obiektu jest po prostu niemożliwe. Dobrze jest, jeśli funkcje są zbudowane w taki sposób, że programiści nie mogą po prostu wykonywać nieprawidłowych wywołań lub jeśli nieprawidłowe wywołania generują błędy podczas kompilacji.
Ale jest też punkt, w którym musisz powiedzieć: programista powinien przeczytać dokumentację dotyczącą funkcji.
źródło
Tak, twoje instynkty są precyzyjne. Wiele lat temu pisałem artykuły w czasopismach (trochę jak to miejsce, ale na martwym drzewie i wydawane raz w miesiącu), omawiając debugowanie , abugowanie i antybugowanie .
Jeśli uda Ci się nakłonić kompilator do wychwycenia niewłaściwego użycia, jest to najlepszy sposób. Dostępne funkcje językowe zależą od języka i nie został określony. W każdym razie szczegółowe techniki byłyby szczegółowe dla poszczególnych pytań na tej stronie.
Ale test wykrywający użycie w czasie kompilacji zamiast w czasie wykonywania jest naprawdę tym samym rodzajem testu: nadal musisz wiedzieć, jak najpierw wywołać odpowiednie funkcje i używać ich we właściwy sposób.
Zaprojektowanie komponentu tak, aby nawet to nie było problemem, jest o wiele bardziej subtelne i podoba mi się, że bufor jest jak przykład elementu . Część 4 (opublikowana dwadzieścia lat temu! Wow!) Zawiera przykład z dyskusją, który jest bardzo podobny do twojego przypadku: Tej klasy należy używać ostrożnie, ponieważ bufor () może nie zostać wywołany po rozpoczęciu korzystania z read (). Raczej należy go użyć tylko raz, natychmiast po zakończeniu budowy. Gdyby klasa zarządzała buforem automatycznie i niewidocznie dla osoby dzwoniącej, uniknęłaby problemu koncepcyjnie.
Pytasz, czy testy można wykonać w czasie wykonywania, aby zapewnić prawidłowe użytkowanie, czy powinieneś to zrobić?
Powiedziałbym tak . Pozwoli to zaoszczędzić Twojemu zespołowi wiele pracy związanej z debugowaniem pewnego dnia, gdy kod zostanie utrzymany, a określone użycie zostanie zakłócone. Testowanie można skonfigurować jako formalny zestaw ograniczeń i niezmienników, które można przetestować. Nie oznacza to, że narzut związany z dodatkowym miejscem do śledzenia stanu i sprawdzania jest zawsze pozostawiony dla każdego połączenia. Jest w klasie, więc można go nazwać, a kod dokumentuje rzeczywiste ograniczenia i założenia.
Być może sprawdzanie odbywa się tylko w kompilacji debugowania.
Być może kontrola jest złożona (na przykład kontrola stosu), ale jest obecna i może być wykonywana raz na jakiś czas lub dodawane tu i tam wywołania, gdy kod jest przetwarzany i pojawia się jakiś problem, lub w celu wykonania konkretnych testów upewnij się, że nie ma takiego problemu w programie testów jednostkowych lub testach integracyjnych , który prowadzi do tej samej klasy.
Możesz zdecydować, że koszty ogólne są trywialne. Jeśli klasa zapisuje operacje we / wy, dodatkowy bajt stanu i test na taki stan to nic. Typowe klasy iostream sprawdzają, czy strumień jest w złym stanie.
Twoje instynkty są dobre. Tak trzymaj!
źródło
Zasada ukrywania informacji (enkapsulacji) polega na tym, że podmioty spoza klasy nie powinny wiedzieć więcej niż jest to wymagane do prawidłowego korzystania z tej klasy.
Twoja sprawa wygląda na to, że próbujesz sprawić, by obiekt klasy działał poprawnie, bez informowania użytkowników zewnętrznych o klasie. Ukryłeś więcej informacji, niż powinieneś.
Słowa mądrości:
* Tak czy inaczej, musisz przeprojektować klasę i / lub konstruktor i / lub metody.
* Nie projektując poprawnie i głodząc klasę na podstawie prawidłowych informacji, ryzykujesz, że klasa pęknie w nie jednym, ale potencjalnie wielu miejscach.
* Jeśli źle napisałeś wyjątki i komunikaty o błędach, twoja klasa będzie rozdawać jeszcze więcej o sobie.
* Na koniec, jeśli osoba postronna powinna wiedzieć, że musi zainicjować a, b i c, a następnie wywołać d (), e (), f (int), to masz przeciek w swojej abstrakcji.
źródło
Ponieważ określiłeś, że używasz C #, realnym rozwiązaniem twojej sytuacji jest wykorzystanie Roslyn Code Analyzers . Umożliwi to natychmiastowe wykrycie naruszeń, a nawet zaproponowanie poprawek kodu .
Jednym ze sposobów wdrożenia byłoby udekorowanie metod tymczasowo połączonych za pomocą atrybutu określającego kolejność, w której metody muszą być nazywane 1 . Gdy analizator znajdzie te atrybuty w klasie, sprawdza, czy metody są wywoływane w kolejności. Dzięki temu Twoja klasa będzie wyglądać mniej więcej tak:
1: Nigdy nie napisałem Roslyn Code Analyzer, więc może to nie być najlepsza implementacja. Pomysł użycia Roslyn Code Analyzer do sprawdzenia, czy interfejs API jest używany prawidłowo, to jednak dźwięk w 100%.
źródło
Sposób, w jaki rozwiązałem ten problem wcześniej, był z prywatnym konstruktorem i
MakeFooManager()
metodą statyczną w klasie. Przykład:Ponieważ konstruktor jest zaimplementowany, nikt nie może utworzyć instancji
FooManager
bez przejściaMakeFooManager()
.źródło
Istnieje kilka podejść:
Spraw, aby klasa była łatwa w użyciu, a trudna w użyciu. Niektóre inne odpowiedzi koncentrują się wyłącznie na tym punkcie, więc po prostu wspomnę o RAII i wzorze konstruktora .
Pamiętaj, jak używać komentarzy. Jeśli klasa sama się wyjaśnia, nie pisz komentarzy. * W ten sposób ludzie będą częściej czytać twoje komentarze, kiedy klasa ich naprawdę potrzebuje. A jeśli masz jeden z tych rzadkich przypadków, w których klasa jest trudna w użyciu i potrzebuje komentarza, dołącz przykłady.
Użyj twierdzeń w kompilacjach debugowania.
Zgłaszaj wyjątki, jeśli użycie klasy jest nieprawidłowe.
* Oto rodzaje komentarzy, których nie powinieneś pisać:
źródło