Czy możliwości obiektu powinny być identyfikowane wyłącznie przez interfejsy, które implementuje?

36

isOperator C # i operator Java instanceofpozwalają na rozgałęzienie w interfejsie (lub, co bardziej nieokiełznane, jego klasie bazowej) zaimplementowanej instancji obiektu.

Czy właściwe jest używanie tej funkcji do rozgałęzień wysokiego poziomu w oparciu o możliwości interfejsu?

A może klasa podstawowa powinna zapewniać zmienne boolowskie, aby zapewnić interfejs opisujący możliwości obiektu?

Przykład:

if (account is IResetsPassword)
       ((IResetsPassword)account).ResetPassword();
else
       Print("Not allowed to reset password with this account type!");

vs.

if (account.CanResetPassword)
       ((IResetsPassword)account).ResetPassword();
else
       Print("Not allowed to reset password with this account type!");

Czy są jakieś pułapki związane z wykorzystaniem implementacji interfejsu do identyfikacji zdolności?


Ten przykład był właśnie taki, przykład. Zastanawiam się nad bardziej ogólnym zastosowaniem.

TheCatWhisperer
źródło
10
Spójrz na dwa przykłady kodu. Który jest bardziej czytelny?
Robert Harvey,
39
Tylko moja opinia, ale wybrałbym pierwszą, tylko dlatego, że zapewnia, że ​​możesz bezpiecznie rzucić na odpowiedni typ. Drugi zakłada, że CanResetPasswordbędzie to prawdą tylko wtedy, gdy coś się zaimplementuje IResetsPassword. Teraz tworzysz właściwość określającą coś, co system typów powinien kontrolować (i ostatecznie zrobi to, gdy wykonasz rzut).
Becuzz,
10
@nocomprende: xkcd.com/927
Robert Harvey
14
Twój tytuł pytania i ciało pytania zadają różne pytania. Aby odpowiedzieć na tytuł: Idealnie tak. Aby zaadresować treść: Jeśli stwierdzisz, że sprawdzasz typ i ręcznie rzucasz, prawdopodobnie nie używasz prawidłowo swojego systemu typów. Czy możesz nam powiedzieć bardziej szczegółowo, jak znalazłeś się w tej sytuacji?
MetaFight,
9
To pytanie może wydawać się proste, ale staje się dość szerokie, gdy zaczynasz o nim myśleć. Pytania, które przychodzą mi do głowy: Czy spodziewasz się dodać nowe zachowania do istniejących kont lub utworzyć typy kont o różnych zachowaniach? Czy wszystkie typy kont mają podobne zachowania lub czy istnieją między nimi duże różnice? Czy przykładowy kod wie o wszystkich typach kont, czy tylko o podstawowym interfejsie IAccount?
Euforia

Odpowiedzi:

7

Z zastrzeżeniem, że najlepiej jest unikać downcastingu, jeśli to możliwe, pierwsza forma jest preferowana z dwóch powodów:

  1. pozwalasz, aby system typów działał dla Ciebie. W pierwszym przykładzie, jeśli ispożary, wtedy spuszczony na pewno zadziała. W drugim formularzu musisz ręcznie upewnić się, że tylko obiekty, które implementują, IResetsPasswordzwracają wartość true dla tej właściwości
  2. delikatna klasa podstawowa. Musisz dodać właściwość do podstawowej klasy / interfejsu dla każdego interfejsu, który chcesz dodać. Jest to uciążliwe i podatne na błędy.

Nie oznacza to, że nie można nieco poprawić tych problemów. Możesz na przykład mieć klasę bazową z zestawem opublikowanych interfejsów, w których możesz sprawdzić włączenie. Ale tak naprawdę po prostu ręcznie wdrażasz część istniejącego systemu typów, który jest zbędny.

BTW, moją naturalną preferencją jest kompozycja nad dziedziczeniem, ale głównie w odniesieniu do stanu. Dziedziczenie interfejsu zwykle nie jest takie złe. Ponadto, jeśli zauważysz, że wdrażasz hierarchię typów biedaka, lepiej skorzystaj z już istniejącej, ponieważ kompilator ci pomoże.

Alex
źródło
Bardzo wolę także pisanie kompozycyjne. +1 za pokazanie, że ten konkretny przykład nie wykorzystuje go poprawnie. Chociaż powiedziałbym, że typowanie obliczeniowe (takie jak tradycyjne dziedziczenie) jest bardziej ograniczone, gdy możliwości różnią się znacznie (w przeciwieństwie do sposobu ich implementacji). Stąd podejście oparte na interfejsie, które rozwiązuje inny problem niż pisanie na podstawie składu lub standardowe dziedziczenie.
TheCatWhisperer
możesz uprościć kontrolę tak: (account as IResettable)?.ResetPassword();lubvar resettable = account as IResettable; if(resettable != null) {resettable.ResetPassword()} else {....}
Berin Loritsch,
89

Czy możliwości obiektu powinny być identyfikowane wyłącznie przez interfejsy, które implementuje?

Możliwości obiektów nie powinny być w ogóle identyfikowane.

Od klienta korzystającego z obiektu nie powinno się wymagać żadnej wiedzy na temat jego działania. Klient powinien wiedzieć tylko, co może powiedzieć obiektowi. To, co robi obiekt, gdy zostanie mu powiedziane, nie stanowi problemu dla klientów.

Więc raczej niż

if (account is IResetsPassword)
    ((IResetsPassword)account).ResetPassword();
else
    Print("Not allowed to reset password with this account type!");

lub

if (account.CanResetPassword)
    ((IResetsPassword)account).ResetPassword();
else
    Print("Not allowed to reset password with this account type!");

rozważać

account.ResetPassword();

lub

account.ResetPassword(authority);

ponieważ accountjuż wie, czy to zadziała. Po co to pytać? Po prostu powiedz mu, co chcesz i pozwól mu zrobić wszystko, co chcesz.

Wyobraź sobie, że zrobiono to w taki sposób, że klient nie dba o to, czy zadziałało, czy nie, ponieważ to jest inny problem. Zadaniem klientów była tylko próba. Zrobiło to już teraz i trzeba poradzić sobie z innymi sprawami. Ten styl ma wiele nazw, ale imię, które lubię najbardziej, to powiedzieć, nie pytaj .

To bardzo kuszące, gdy piszesz do klienta, że ​​myślisz, że musisz wszystko śledzić, a więc przyciągnij do siebie wszystko, co myślisz, że musisz wiedzieć. Kiedy to robisz, obracasz przedmioty na lewą stronę. Doceń swoją ignorancję. Odepchnij szczegóły i pozwól obiektom sobie z nimi poradzić. Im mniej wiesz, tym lepiej.

candied_orange
źródło
54
Polimorfizm działa tylko wtedy, gdy nie wiem ani nie obchodzi mnie dokładnie, o czym mówię. Tak więc radzę sobie z tą sytuacją, aby ostrożnie jej unikać.
candied_orange
7
Jeśli klient naprawdę musi wiedzieć, czy próba się powiodła, metoda może zwrócić wartość logiczną. if (!account.AttemptPasswordReset()) /* attempt failed */;W ten sposób klient zna wynik, ale unika niektórych problemów z logiką decyzyjną w pytaniu. Jeśli próba jest asynchroniczna, można przekazać wywołanie zwrotne.
Radiodef,
5
@DavidZ Oboje mówimy po angielsku. Mogę więc powiedzieć dwa razy, aby głosować za tym komentarzem. Czy to oznacza, że ​​jesteś w stanie to zrobić? Czy muszę wiedzieć, czy możesz, zanim będę mógł Ci to powiedzieć?
candied_orange
5
@QPaysTaxes za wszystko, co wiadomo, że procedura obsługi błędów została przekazana accountpodczas jej budowy. W tym momencie mogą pojawiać się wyskakujące okienka, rejestrowanie, wydruki i alarmy dźwiękowe. Zadaniem klienta jest powiedzenie „zrób to teraz”. Nie musi robić nic więcej. Nie musisz robić wszystkiego w jednym miejscu.
candied_orange
6
@QPaysTaxes Można to załatwić gdzie indziej i na wiele różnych sposobów, jest to zbędne w tej rozmowie i tylko mętnieje na wodzie.
Tom.Bowen89,
17

tło

Dziedziczenie to potężne narzędzie, które służy celowi w programowaniu obiektowym. Jednak nie rozwiązuje każdego problemu elegancko: czasem inne rozwiązania są lepsze.

Jeśli przypomnisz sobie swoje wczesne zajęcia z informatyki (zakładając, że masz dyplom CS), możesz przypomnieć sobie profesora, który dał ci akapit z informacją o tym, co klient chce zrobić z oprogramowaniem. Twoim zadaniem jest przeczytanie akapitu, zidentyfikowanie aktorów i działań oraz przedstawienie przybliżonego szkicu klas i metod. Pojawią się tam nieprzyzwoite tropy, które wyglądają, jakby były ważne, ale nie są. Istnieje również bardzo realna możliwość błędnej interpretacji wymagań.

Jest to ważna umiejętność, którą popełniają nawet najbardziej doświadczeni z nas: prawidłowe identyfikowanie wymagań i tłumaczenie ich na języki maszynowe.

Twoje pytanie

Na podstawie tego, co napisałeś, myślę, że możesz nie rozumieć ogólnego przypadku opcjonalnych akcji, które mogą wykonywać klasy. Tak, wiem, że Twój kod jest tylko przykładem i jesteś zainteresowany ogólną sprawą. Wydaje się jednak, że chcesz wiedzieć, jak poradzić sobie z sytuacją, w której pewne podtypy obiektu mogą wykonać akcję, ale inne podtypy nie.

To, że obiekt taki jak accountma typ konta, nie oznacza, że ​​przekłada się na typ w języku OO. „Pisanie” w ludzkim języku nie zawsze oznacza „klasę”. W kontekście konta „typ” może ściślej korelować z „zestawem uprawnień”. Chcesz użyć konta użytkownika do wykonania akcji, ale ta akcja może lub nie może być wykonana przez to konto. Zamiast korzystać z dziedziczenia, używałbym delegata lub tokena zabezpieczającego.

Moje rozwiązanie

Rozważ klasę kont, która ma kilka opcjonalnych akcji, które może wykonać. Zamiast definiować „może wykonać akcję X” poprzez dziedziczenie, dlaczego konto nie zwróci obiektu delegowanego (resetowania hasła, osoby wysyłającej formularz itp.) Lub tokena dostępu?

account.getPasswordResetter().doAction();
account.getFormSubmitter().doAction(view.getContents());

AccountManager.resetPassword(account, account.getAccessToken());

Korzyść dla ostatniej opcji nie ma co, jeśli chcę wykorzystać moje poświadczenia konta, aby zresetować ktoś innego hasła?

AccountManager.resetPassword(otherAccount, adminAccount.getAccessToken());

System jest nie tylko bardziej elastyczny, nie tylko usunąłem odlewy typu, ale projekt jest bardziej wyrazisty . Mogę to przeczytać i łatwo zrozumieć, co robi i jak można z niej korzystać.


TL; DR: brzmi to jak problem XY . Zasadniczo w obliczu dwóch opcji, które nie są optymalne, warto cofnąć się o krok i zastanowić się: „co tak naprawdę chcę tutaj osiągnąć? Czy naprawdę powinienem zastanowić się, jak sprawić, by rzutowanie tekstu było mniej brzydkie, czy też powinienem poszukać sposobów usunąć całkowicie typecast? ”


źródło
1
+1 za projekt pachnie, nawet jeśli nie użyłeś tego wyrażenia.
Jared Smith
co z przypadkami, w których zresetowanie hasła ma zupełnie inne argumenty w zależności od typu? ResetPassword (pswd) vs ResetPasswordToRandom ()
TheCatWhisperer
1
@TheCatWhisperer Pierwszym przykładem jest „zmiana hasła”, która jest inną akcją. Drugi przykład opisuję w tej odpowiedzi.
Dlaczego nie account.getPasswordResetter().resetPassword();.
AnoE,
1
The benefit to the last option there is what if I want to use my account credentials to reset someone else's password?.. łatwy. Tworzysz obiekt domeny Konto dla drugiego konta i używasz go. Klasy statyczne są problematyczne przy testowaniu jednostkowym (ta metoda z definicji będzie wymagała dostępu do pewnej pamięci, więc teraz nie można łatwo testować jednostkowo kodu, który dotyka tej klasy) i najlepiej tego unikać. Opcja zwrotu jakiegoś innego interfejsu jest często dobrym pomysłem, ale tak naprawdę nie rozwiązuje podstawowego problemu (właśnie przeniosłeś problem na inny interfejs).
Voo,
1

Bill Venner mówi, że twoje podejście jest absolutnie w porządku ; przejdź do sekcji zatytułowanej „Kiedy korzystać z instanceof”. Sprowadzasz się do określonego typu / interfejsu, który implementuje tylko to konkretne zachowanie, więc masz absolutną rację, jeśli chodzi o selektywność.

Jeśli jednak chcesz zastosować inne podejście, istnieje wiele sposobów na ogolenie jak.

Istnieje podejście polimorficzne; możesz argumentować, że wszystkie konta mają hasło, dlatego wszystkie konta powinny przynajmniej móc spróbować zresetować hasło. Oznacza to, że w podstawowej klasie konta powinniśmy mieć resetPassword()metodę, którą wszystkie konta wdrażają, ale na swój własny sposób. Pytanie brzmi, jak ta metoda powinna się zachowywać, gdy nie ma takiej możliwości.

Może zwrócić pustkę i po cichu dokończyć, czy zresetuje hasło, czy nie, być może bierze odpowiedzialność za wewnętrzne wydrukowanie wiadomości, jeśli nie zresetuje hasła. Nie najlepszy pomysł.

Może zwrócić wartość logiczną wskazującą, czy reset się powiódł. Włączając tę ​​wartość logiczną, możemy stwierdzić, że resetowanie hasła nie powiodło się.

Może zwrócić ciąg znaków wskazujący wynik próby zresetowania hasła. Ciąg może podawać więcej szczegółów na temat przyczyny niepowodzenia resetowania i może być wyprowadzany.

Może zwrócić obiekt ResetResult, który przekazuje więcej szczegółów i łączy wszystkie poprzednie elementy zwracane.

Może zwrócić pustkę i zamiast tego rzucić wyjątek, jeśli spróbujesz zresetować konto, które nie ma takiej możliwości (nie rób tego, ponieważ używanie wyjątków do normalnej kontroli przepływu jest złą praktyką z różnych powodów).

Posiadanie canResetPassword()metody dopasowania może nie wydawać się najgorszą rzeczą na świecie, ponieważ teoretycznie jest to funkcja statyczna wbudowana w klasę, gdy została napisana. Jest to wskazówka, dlaczego podejście metodyczne jest złym pomysłem, ponieważ sugeruje, że zdolność jest dynamiczna i że canResetPassword()może się zmienić, co stwarza dodatkową możliwość, że może się zmienić między pytaniem o pozwolenie a wykonaniem połączenia. Jak wspomniano w innym miejscu, powiedz zamiast pytać o pozwolenie.

Kompozycja nad dziedziczeniem może być opcją: możesz mieć końcowe passwordResetterpole (lub równoważny getter) i równoważne klasy, dla których możesz sprawdzić wartość null przed wywołaniem jej. Chociaż działa to trochę jak prośba o pozwolenie, ostateczność może uniknąć jakiejkolwiek wywnioskowanej natury dynamicznej.

Możesz pomyśleć o przekazaniu funkcjonalności na własną klasę, która może wziąć pod uwagę parametr jako parametr i działać zgodnie z nim (np. resetter.reset(account)), Chociaż często jest to również zła praktyka (powszechną analogią jest sklepikarz sięgający do portfela w celu uzyskania gotówki).

W językach, w których jest to możliwe, możesz używać miksów lub cech, ale ostatecznie kończysz w miejscu, w którym zacząłeś sprawdzać istnienie tych możliwości.

Danikow
źródło
Nie wszystkie konta mogą mieć hasło (w każdym razie z perspektywy tego konkretnego systemu). Na przykład konto może być stroną trzecią ... google, facebook, ect. Nie wspominając, że to nie jest świetna odpowiedź!
TheCatWhisperer
0

W tym konkretnym przykładzie „ można zresetować hasłozalecam użycie kompozycji zamiast dziedziczenia (w tym przypadku dziedziczenie interfejsu / kontraktu). Ponieważ robiąc to:

class Foo : IResetsPassword {
    //...
}

Natychmiast określasz (w czasie kompilacji), że twoja klasa „ może zresetować hasło ”. Ale jeśli w twoim scenariuszu obecność zdolności jest warunkowa i zależy od innych rzeczy, nie możesz już więcej określać rzeczy w czasie kompilacji. Następnie sugeruję zrobienie tego:

class Foo {

    PasswordResetter passwordResetter;

}

Teraz w czasie wykonywania możesz sprawdzić, czy myFoo.passwordResetter != nullprzed wykonaniem tej operacji. Jeśli chcesz oddzielić rzeczy jeszcze bardziej (i planujesz dodać o wiele więcej możliwości), możesz:

class Foo {
    //... foo stuff
}

class PasswordResetOperation {
    bool Execute(Foo foo) { ... }
}

class SendMailOperation {
    bool Execute(Foo foo) { ... }
}

//...and you follow this pattern for each new capability...

AKTUALIZACJA

Po przeczytaniu kilku innych odpowiedzi i komentarzy z OP zrozumiałem, że pytanie nie dotyczy rozwiązania kompozycyjnego. Myślę więc, że pytanie dotyczy tego, jak lepiej zidentyfikować możliwości obiektów w ogóle, w scenariuszu takim jak poniżej:

class BaseAccount {
    //...
}
class GuestAccount : BaseAccount {
    //...
}
class UserAccount : BaseAccount, IMyPasswordReset, IEditPosts {
    //...
}
class AdminAccount : BaseAccount, IPasswordReset, IEditPosts, ISendMail {
    //...
}

//Capabilities

interface IMyPasswordReset {
    bool ResetPassword();
}

interface IPasswordReset {
    bool ResetPassword(UserAccount userAcc);
}

interface IEditPosts {
    bool EditPost(long postId, ...);
}

interface ISendMail {
    bool SendMail(string from, string to, ...);
}

Teraz spróbuję przeanalizować wszystkie wymienione opcje:

Drugi przykład PO:

if (account.CanResetPassword)
       ((IResetsPassword)account).ResetPassword();
else
       Print("Not allowed to reset password with this account type!");

Powiedzmy, że ten kod otrzymuje podstawową klasę konta (np .: BaseAccountw moim przykładzie); jest to złe, ponieważ wstawia booleany do klasy podstawowej, zanieczyszczając go kodem, który w ogóle nie ma sensu.

Pierwszy przykład PO:

if (account is IResetsPassword)
       ((IResetsPassword)account).ResetPassword();
else
       Print("Not allowed to reset password with this account type!");

Aby odpowiedzieć na to pytanie, jest to bardziej odpowiednie niż poprzednia opcja, ale w zależności od implementacji złamie zasadę L bryły i prawdopodobnie takie kontrole zostaną rozłożone w kodzie i utrudnią dalszą konserwację.

Anser CandiedOrange:

account.ResetPassword(authority);

Jeśli ta ResetPasswordmetoda jest wstawiona do BaseAccountklasy, wówczas również zanieczyszcza klasę podstawową nieodpowiednim kodem, jak w drugim przykładzie OP

Odpowiedź bałwana:

AccountManager.resetPassword(otherAccount, adminAccount.getAccessToken());

To dobre rozwiązanie, ale uważa, że ​​możliwości są dynamiczne (i mogą się zmieniać z czasem). Jednak po przeczytaniu kilku komentarzy z OP myślę, że tutaj mowa jest o polimorfizmie i klasach zdefiniowanych statycznie (chociaż przykład kont intuicyjnie wskazuje na scenariusz dynamiczny). EG: w tym AccountManagerprzykładzie sprawdzeniem uprawnień byłyby zapytania do DB; w pytaniu PO kontrole są próbami rzucenia obiektów.

Kolejna sugestia ode mnie:

Użyj wzorca metody szablonu dla rozgałęzień wysokiego poziomu. Wspomniana hierarchia klas jest zachowana bez zmian; tworzymy tylko bardziej odpowiednie procedury obsługi obiektów, aby uniknąć rzutowania i nieodpowiednich właściwości / metod zanieczyszczających klasę podstawową.

//Template method
class BaseAccountOperation {

    BaseAccount account;

    void Execute() {

        //... some processing

        TryResetPassword();

        //... some processing

        TrySendMail();

        //... some processing
    }

    void TryResetPassword() {
        Print("Not allowed to reset password with this account type!");
    }

    void TrySendMail() {
        Print("Not allowed to reset password with this account type!");
    }
}

class UserAccountOperation : BaseAccountOperation {

    UserAccount userAccount;

    void TryResetPassword() {
        account.ResetPassword(...);
    }

}

class AdminAccountOperation : BaseAccountOperation {

    AdminAccount adminAccount;

    override void TryResetPassword() {
        account.ResetPassword(...);
    }

    void TrySendMail() {
        account.SendMail(...);
    }
}

Można powiązać operację z odpowiednią klasą konta, używając słownika / tablicy hashtable, lub wykonać operacje w czasie wykonywania przy użyciu metod rozszerzenia, użyć dynamicsłowa kluczowego lub jako ostatnią opcję użyć tylko jednej rzutowania , aby przekazać obiekt konta do operacji (w w tym przypadku liczba rzutów jest tylko jedna, na początku operacji).

Emerson Cardoso
źródło
1
Czy działałoby zawsze po prostu implementowanie metody „Execute ()”, ale czasami nie robi nic? Zwraca bool, więc zgaduję, że to znaczy, że to zrobiło, czy nie. Powoduje to powrót do klienta: „Próbowałem zresetować hasło, ale tak się nie stało (z jakiegokolwiek powodu), więc teraz powinienem ...”
4
Posiadanie metod „canX ()” i „doX ()” jest antypatternem: jeśli interfejs eksponuje metodę „doX ()”, lepiej być w stanie wykonać X, nawet jeśli wykonywanie X oznacza brak operacji (np. Wzorzec obiektu zerowego ). Użytkownicy interfejsu nigdy nie powinni mieć obowiązku angażowania się w sprzężenie czasowe (wywołaj metodę A przed metodą B), ponieważ zbyt łatwo jest się pomylić i zepsuć.
1
Posiadanie interfejsów nie ma wpływu na użycie kompozycji zamiast dziedziczenia. Reprezentują one tylko funkcjonalność, która jest dostępna. Sposób, w jaki ta funkcjonalność się pojawia, jest niezależny od użycia interfejsu. To, co zrobiłeś tutaj, to zastosowanie interfejsu ad-hoc bez żadnych korzyści.
Miyamoto Akira,
Interesujące: w głosowaniu, które otrzymało najwięcej głosów, jest to, co mówi mój komentarz powyżej. W niektóre dni po prostu masz szczęście.
@nocomprende - tak, zaktualizowałem swoją odpowiedź zgodnie z kilkoma komentarzami. Dzięki za opinie :)
Emerson Cardoso,
0

Żadna z przedstawionych opcji nie jest dobra. Jeśli piszesz, jeśli oświadczenia dotyczące typu obiektu, najprawdopodobniej źle robisz OO (są wyjątki, to nie jest jeden). Oto prosta odpowiedź OO na twoje pytanie (może nie być poprawna C #):

interface IAccount {
  bool CanResetPassword();

  void ResetPassword();

  // Other Account operations as needed
}

public class Resetable : IAccount {
  public bool CanResetPassword() {
    return true;
  }

  public void ResetPassword() {
    /* RESET PASSWORD */
  }
}

public class NotResetable : IAccount {
  public bool CanResetPassword() {
    return false;
  }

  public void ResetPassword() {
    Print("Not allowed to reset password with this account type!");}
  }

Zmodyfikowałem ten przykład, aby pasował do tego, co robił oryginalny kod. Na podstawie niektórych komentarzy wydaje się, że ludzie rozłączają się, czy jest to „właściwy” konkretny kod tutaj. Nie o to chodzi w tym przykładzie. Całe przeciążenie polimorficzne polega przede wszystkim na warunkowym wykonaniu różnych implementacji logiki w zależności od typu obiektu. W obu przykładach robisz ręczne zacinanie się w tym, co oferuje Twój język. W skrócie możesz pozbyć się podtypów i ustawić możliwość resetowania jako właściwość logiczną typu Konta (ignorując inne funkcje podtypów).

Bez szerszego spojrzenia na projekt nie można stwierdzić, czy jest to dobre rozwiązanie dla konkretnego systemu. Jest to proste i jeśli zadziała to, co robisz, prawdopodobnie nigdy nie będziesz musiał więcej o tym myśleć, chyba że ktoś nie sprawdzi CanResetPassword () przed wywołaniem ResetPassword (). Możesz także zwrócić wartość logiczną lub zakończyć się niepowodzeniem w trybie cichym (niezalecane). To naprawdę zależy od specyfiki projektu.

JimmyJames
źródło
Nie wszystkie konta można zresetować. Ponadto, czy konto może mieć więcej funkcji niż możliwość resetowania?
TheCatWhisperer
2
„Nie wszystkie konta można zresetować”. Nie jestem pewien, czy napisano to przed edycją. Druga klasa to NotResetable. „może konto ma więcej funkcji niż możliwość resetowania” Spodziewałbym się tego, ale wydaje się, że nie jest to ważne dla tego pytania.
JimmyJames,
1
Myślę, że to rozwiązanie idzie w dobrym kierunku, ponieważ zapewnia dobre rozwiązanie w OO. Prawdopodobnie zmieniłby nazwę interfejsu na IPasswordReseter (lub coś w tym celu), aby segregować interfejsy. IAccount można zadeklarować jako obsługujący wiele innych interfejsów. Dałoby to opcję posiadania klas z implementacją, która jest następnie komponowana i używana przez delegację na obiektach domeny. @ JimmyJames, drobne nitpick, na C # obie klasy będą musiały określić, że implementują IAccount. W przeciwnym razie kod wygląda nieco dziwnie ;-)
Miyamoto Akira,
1
Sprawdź także komentarz Snowmana w jednej z pozostałych odpowiedzi na temat CanX () i DoX (). Nie zawsze jednak jest to anty-wzorzec, ponieważ wykorzystuje (na przykład zachowanie interfejsu użytkownika)
Miyamoto Akira,
1
Ta odpowiedź zaczyna się dobrze: to źle OOP. Niestety rozwiązanie jest tak samo złe, bo to również obala system typu, ale zgłasza wyjątek. Dobre rozwiązanie wygląda inaczej: nie stosuj nie zaimplementowanych metod na typie. .NET Framework faktycznie używa twojego podejścia do niektórych typów, ale był to jednoznacznie błąd.
Konrad Rudolph,
0

Odpowiedź

Moja lekko uparta opinia na twoje pytanie brzmi:

Możliwości obiektu powinny być identyfikowane przez nich samych, a nie poprzez implementację interfejsu. Jednak język o silnym typie statystycznym ograniczy tę możliwość (zgodnie z projektem).

Uzupełnienie:

Rozgałęzienie na typ obiektu jest złe.

Pnący

Widzę, że nie dodałeś c#tagu, ale pytasz w ogólnym object-orientedkontekście.

To, co opisujesz, to technika statycznych / silnych języków pisanych, a nie specyfika „OO” w ogóle. Twój problem wynika między innymi z faktu, że nie możesz wywoływać metod w tych językach bez posiadania wystarczająco wąskiego typu w czasie kompilacji (poprzez definicję zmiennej, definicję parametru metody / wartości zwracanej lub jawną obsadę). W przeszłości robiłem oba wasze warianty, w tego rodzaju językach, i oba wydają mi się teraz brzydkie.

W dynamicznie wpisywanych językach OO ten problem nie występuje z powodu koncepcji zwanej „pisaniem kaczych”. Krótko mówiąc, oznacza to, że zasadniczo nie deklarujesz nigdzie typu obiektu (z wyjątkiem tworzenia). Wysyłasz wiadomości do obiektów, a obiekty obsługują te wiadomości. Nie obchodzi Cię, czy obiekt rzeczywiście ma metodę o tej nazwie. Niektóre języki mają nawet ogólną method_missingmetodę „ catch-all” , co czyni ją jeszcze bardziej elastyczną.

W takim języku (na przykład Ruby) kod staje się:

account.reset_password

Kropka.

accountBędzie albo zresetować hasło, rzucać „odrzuć” wyjątek, albo rzucać „nie wiem, jak obsługiwać«reset_password»” wyjątek.

Jeśli potrzebujesz jawnie rozgałęzić się z jakiegokolwiek powodu, nadal robiłbyś to dla możliwości, a nie dla klasy:

account.reset_password  if account.can? :reset_password

(Gdzie can?jest tylko metoda sprawdzająca, czy obiekt może wykonać jakąś metodę, bez jej wywoływania).

Zwróć uwagę, że chociaż moje osobiste preferencje dotyczą obecnie dynamicznie pisanych języków, są to tylko osobiste preferencje. Języki o typie statycznym również się do nich odnoszą, więc proszę, nie przejmuj się tym bełkotem przeciwko językom o typie statycznym ...

AnoE
źródło
Wspaniale mieć tutaj swoją perspektywę! My po stronie java / C # często zapominamy o istnieniu innej gałęzi OOP. Kiedy mówię moim kolegom, JS (pomimo wszystkich swoich problemów) jest pod wieloma względami bardziej OO niż C #, śmieją się ze mnie!
TheCatWhisperer,
Uwielbiam C # i myślę, że jest to dobry język ogólnego przeznaczenia , ale to moja osobista opinia, że ​​jeśli chodzi o OO, jest dość ubogi. Zarówno pod względem funkcji, jak i praktyki, zachęca do programowania proceduralnego.
TheCatWhisperer 12.12.17
2
Prawdopodobnie testowanie pod kątem istnienia metody w języku typu kaczego jest takie samo, jak testowanie implementacji interfejsu w języku o typie statycznym: wykonuje się sprawdzenie w czasie wykonywania, czy obiekt ma określoną zdolność. Każda deklaracja metody jest faktycznie własnym interfejsem, identyfikowanym tylko przez nazwę metody, a jej istnienie nie może być wykorzystane do wnioskowania o innych informacjach o obiekcie.
IMSoP,
0

Wygląda na to, że twoje opcje pochodzą od różnych sił motywacyjnych. Pierwszy wydaje się być włamaniem stosowanym z powodu złego modelowania obiektów. Wykazało, że twoje abstrakcje są błędne, ponieważ łamią polimorfizm. Bardziej prawdopodobne niż to, że łamie zasadę otwartego zamknięcia , co skutkuje ściślejszym sprzężeniem.

Twoja druga opcja może być dobra z punktu widzenia OOP, ale rzutowanie tekstu myli mnie. Jeśli twoje konto pozwala zresetować hasło, dlaczego więc potrzebujesz rzutowania? Więc zakładając, że twoja CanResetPasswordklauzula reprezentuje pewne podstawowe wymagania biznesowe, które są częścią abstrakcji konta, twoja druga opcja jest OK.

Zapadło
źródło