Le capacità di un oggetto devono essere identificate esclusivamente dalle interfacce che implementa?


36

L' isoperatore di C # e l' operatore di Java instanceofconsentono di eseguire il branch sull'interfaccia (o, più in modo non veritiero, della sua classe di base) implementata da un'istanza di oggetto.

È appropriato utilizzare questa funzione per la ramificazione di alto livello in base alle funzionalità fornite da un'interfaccia?

O una classe base dovrebbe fornire variabili booleane per fornire un'interfaccia per descrivere le capacità di un oggetto?

Esempio:

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!");

Ci sono delle insidie ​​nell'usare l'implementazione dell'interfaccia per l'identificazione delle capacità?


Questo esempio era proprio questo, un esempio. Mi chiedo un'applicazione più generale.


10
Guarda i tuoi due esempi di codice. Quale è più leggibile?
Robert Harvey,

39
Solo la mia opinione, ma andrei con il primo solo perché si assicura che tu possa tranquillamente lanciare il tipo appropriato. Il secondo presuppone che CanResetPasswordsarà vero solo quando qualcosa verrà implementato IResetsPassword. Ora stai facendo in modo che una proprietà determini qualcosa che il sistema di tipi dovrebbe controllare (e alla fine lo farà quando preformi il cast).
Becuzz,

10
@nocomprende: xkcd.com/927
Robert Harvey l'

14
Il titolo della domanda e il corpo della domanda pongono cose diverse. Per rispondere al titolo: idealmente, sì. Per affrontare il corpo: se ti ritrovi a controllare il tipo e a lanciare manualmente, probabilmente non stai sfruttando correttamente il tuo sistema di tipi. Puoi dirci più specificamente come ti sei trovato in questa situazione?
MetaFight,

9
Questa domanda potrebbe sembrare semplice, ma diventa piuttosto ampia quando inizi a pensarci. Domande che mi vengono in mente: ti aspetti di aggiungere nuovi comportamenti agli account esistenti o di creare tipi di account con comportamenti diversi? Tutti i tipi di account hanno comportamenti simili o ci sono grandi differenze tra loro? Il codice di esempio conosce tutti i tipi di account o solo l'interfaccia IAccount di base?
Euforico,

Risposte:


7

Con l'avvertenza che è meglio evitare il downcasting se possibile, quindi la prima forma è preferibile per due motivi:

  1. stai lasciando che il sistema di tipi funzioni per te. Nel primo esempio, se gli isincendi allora il downcast funzionerà sicuramente. Nel secondo modulo, è necessario assicurarsi manualmente che solo gli oggetti che implementano IResetsPasswordrestituiscano true per quella proprietà
  2. classe base fragile. È necessario aggiungere una proprietà alla classe / interfaccia di base per ogni interfaccia che si desidera aggiungere. Questo è ingombrante e soggetto a errori.

Questo non vuol dire che non puoi migliorare un po 'questi problemi. Ad esempio, potresti avere la classe base con un set di interfacce pubblicate in cui puoi verificare l'inclusione. Ma stai davvero implementando manualmente una parte del sistema di tipi esistente che è ridondante.

A proposito, la mia naturale preferenza è la composizione rispetto all'eredità, ma soprattutto per quanto riguarda lo stato. L'ereditarietà dell'interfaccia non è poi così male. Inoltre, se ti ritrovi a implementare la gerarchia di tipi di un uomo povero, stai meglio usando quello che è già lì perché il compilatore ti aiuterà.


Preferisco anche molto la digitazione compositiva. +1 per avermi fatto capire che questo particolare esempio non lo sta sfruttando correttamente. Sebbene, direi che la tipizzazione computazionale (come l'eredità tradizionale) è più limitata quando le capacità variano ampiamente (al contrario di come vengono implementate). Da qui l'approccio di interfacciamento, che risolve un problema diverso rispetto alla tipizzazione compositiva o all'eredità standard.
TheCatWhisperer,

potresti semplificare il controllo in questo modo: (account as IResettable)?.ResetPassword();oppurevar resettable = account as IResettable; if(resettable != null) {resettable.ResetPassword()} else {....}
Berin Loritsch l'

89

Le capacità di un oggetto devono essere identificate esclusivamente dalle interfacce che implementa?

Le capacità di un oggetto non devono essere identificate affatto.

Il client che utilizza un oggetto non dovrebbe essere tenuto a sapere nulla sul suo funzionamento. Il client dovrebbe sapere solo cose che può dire all'oggetto di fare. Quello che fa l'oggetto, una volta detto, non è il problema dei client.

Quindi piuttosto che

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

o

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

ritenere

account.ResetPassword();

o

account.ResetPassword(authority);

perché account sa già se funzionerà. Perché chiederlo? Digli quello che vuoi e lascia che faccia tutto ciò che farà.

Immagina che ciò sia fatto in modo tale che al cliente non importi se ha funzionato o meno perché è un altro problema. Il compito dei clienti era solo quello di tentare. Lo ha fatto ora e ha altre cose da affrontare. Questo stile ha molti nomi, ma il nome che mi piace di più è dire, non chiedere .

È molto allettante quando si scrive al cliente di pensare di dover tenere traccia di tutto e quindi tirare verso di sé tutto ciò che si pensa di sapere. Quando lo fai, capovolgi gli oggetti. Valorizza la tua ignoranza. Allontana i dettagli e lascia che gli oggetti li gestiscano. Meno sai, meglio è.


54
Il polimorfismo funziona solo quando non so o mi interessa esattamente di cosa sto parlando. Quindi il modo in cui mi occupo di quella situazione è evitarlo attentamente.
candied_orange,

7
Se il client ha davvero bisogno di sapere se il tentativo è riuscito, il metodo potrebbe restituire un valore booleano. if (!account.AttemptPasswordReset()) /* attempt failed */;In questo modo il client conosce il risultato ma evita alcuni dei problemi con la logica decisionale nella domanda. Se il tentativo è asincrono, passeresti una richiamata.
Radiodef,

5
@DavidZ Parliamo entrambi inglese. Quindi posso dirti di votare questo commento due volte. Significa che sei in grado di farlo? Devo sapere se puoi prima di poterti dire di farlo?
candied_orange,

5
@QPaysTaxes per tutto quello che sai accountquando è stato passato un gestore degli errori . A questo punto potrebbero apparire popup, registrazioni, stampe e avvisi audio. Il compito del cliente è di dire "fallo ora". Non deve fare altro. Non devi fare tutto in un unico posto.
candied_orange

6
@QPaysTaxes Potrebbe essere gestito altrove e in diversi modi, è superfluo per questa conversazione e confonderebbe solo le acque.
Tom.Bowen89,

17

sfondo

L'ereditarietà è un potente strumento che ha uno scopo nella programmazione orientata agli oggetti. Tuttavia, non risolve elegantemente ogni problema: a volte, altre soluzioni sono migliori.

Se ripensi alle tue prime lezioni di informatica (supponendo di avere una laurea in CS), potresti ricordare un professore che ti ha dato un paragrafo che indica ciò che il cliente vuole che faccia il software. Il tuo compito è leggere il paragrafo, identificare gli attori e le azioni e venire fuori con uno schema approssimativo di quali sono le classi e i metodi. Ci saranno alcuni conduttori che sembrano importanti, ma non lo sono. Esiste anche una reale possibilità di interpretare erroneamente i requisiti.

Questa è un'abilità importante che persino i più esperti di noi sbagliano: identificare correttamente i requisiti e tradurli in linguaggi automatici.

La tua domanda

Sulla base di ciò che hai scritto, penso che potresti fraintendere il caso generale di azioni opzionali che le classi possono eseguire. Sì, so che il tuo codice è solo un esempio e sei interessato al caso generale. Tuttavia, sembra che tu voglia sapere come gestire la situazione in cui determinati sottotipi di un oggetto possono eseguire un'azione, mentre altri sottotipi non possono.

Solo perché un oggetto come un accountha un tipo di account non significa che si traduca in un tipo in una lingua OO. "Digitare" in un linguaggio umano non significa sempre "classe". Nel contesto di un account, "tipo" può essere più strettamente correlato a "set di autorizzazioni". Si desidera utilizzare un account utente per eseguire un'azione, ma tale azione potrebbe o meno essere eseguita da tale account. Invece di utilizzare l'ereditarietà, vorrei utilizzare un delegato o un token di sicurezza.

La mia soluzione

Considera una classe di account che ha diverse azioni opzionali che può eseguire. Invece di definire "può eseguire l'azione X" tramite ereditarietà, perché l'account non deve restituire un oggetto delegato (reimpostazione password, mittente modulo, ecc.) O un token di accesso?

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

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

Il vantaggio dell'ultima opzione è che cosa succede se voglio utilizzare le credenziali del mio account per reimpostare la password di qualcun altro ?

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

Non solo il sistema è più flessibile, non solo ho rimosso i cast di tipo, ma il design è più espressivo . Posso leggere questo e capire facilmente cosa sta facendo e come può essere utilizzato.


TL; DR: sembra un problema XY . Generalmente di fronte a due opzioni non ottimali, vale la pena fare un passo indietro e pensare "cosa sto davvero cercando di realizzare qui? Dovrei davvero pensare a come rendere meno brutto il typecasting o dovrei cercare modi per rimuovere completamente il typecast? "


1
+1 per gli odori del design, anche se non hai usato la frase.
Jared Smith,

che dire dei casi in cui reimpostare la password ha argomenti completamente diversi a seconda del tipo? ResetPassword (pswd) vs ResetPasswordToRandom ()
TheCatWhisperer

1
@TheCatWhisperer Il primo esempio è "cambia password", che è un'azione diversa. Il secondo esempio è quello che descrivo in questa risposta.

Perché no 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?.. facile. Si crea un oggetto dominio Account per l'altro account e si utilizza quello. Le classi statiche sono problematiche per i test unitari (questo metodo per definizione avrà bisogno di accedere ad un po 'di spazio di archiviazione, quindi ora non è più possibile testare facilmente i codici che toccano più quella classe) e meglio evitarlo. L'opzione di restituire qualche altra interfaccia è spesso una buona idea, ma in realtà non risolve il problema sottostante (hai appena spostato il problema sull'altra interfaccia).
Voo

1

Bill Venner afferma che il tuo approccio è assolutamente perfetto ; vai alla sezione intitolata "Quando utilizzare instanceof". Stai eseguendo il downcasting a un tipo / interfaccia specifici che implementano solo quel comportamento specifico, quindi hai assolutamente ragione di essere selettivo al riguardo.

Tuttavia, se vuoi usare un altro approccio, ci sono molti modi per radere uno yak.

C'è l'approccio del polimorfismo; potresti sostenere che tutti gli account hanno una password, quindi tutti gli account dovrebbero almeno essere in grado di tentare di reimpostare una password. Ciò significa che, nella classe di account di base, dovremmo avere un resetPassword()metodo che tutti gli account implementano ma a modo loro. La domanda è come dovrebbe comportarsi quel metodo quando la capacità non è presente.

Potrebbe restituire un vuoto e completare silenziosamente se reimpostare la password o meno, magari assumendosi la responsabilità di stampare il messaggio internamente se non reimposta la password. Non è la migliore idea.

Potrebbe restituire un valore booleano che indica se il ripristino ha avuto esito positivo. Attivando quel valore booleano, potremmo stabilire che la reimpostazione della password non è riuscita.

Potrebbe restituire una stringa che indica il risultato del tentativo di reimpostazione della password. La stringa potrebbe fornire maggiori dettagli sul perché un reset non è riuscito e potrebbe essere emesso.

Potrebbe restituire un oggetto ResetResult che trasmette più dettagli e combina tutti i precedenti elementi di ritorno.

Potrebbe restituire un vuoto e invece generare un'eccezione se si tenta di ripristinare un account che non dispone di tale capacità (non farlo poiché l'utilizzo della gestione delle eccezioni per il normale controllo del flusso è una cattiva pratica per una serie di motivi).

Avere un canResetPassword()metodo di corrispondenza potrebbe non sembrare la cosa peggiore al mondo, dal momento che teoricamente è una funzionalità statica integrata nella classe quando è stata scritta. Questo è un indizio sul perché l'approccio del metodo sia una cattiva idea, poiché suggerisce che la capacità è dinamica e che canResetPassword()potrebbe cambiare, il che crea anche la possibilità aggiuntiva che potrebbe cambiare tra chiedere l'autorizzazione ed effettuare la chiamata. Come menzionato altrove, dire piuttosto che chiedere il permesso.

La composizione sull'ereditarietà potrebbe essere un'opzione: potresti avere un passwordResettercampo finale (o un equivalente getter) e classi equivalenti che puoi controllare per null prima di chiamarlo. Mentre agisce un po 'come chiedere l'autorizzazione, la finalità potrebbe evitare qualsiasi natura dinamica inferita.

Potresti pensare di esternalizzare la funzionalità nella sua stessa classe che potrebbe prendere un account come parametro e agire su di esso (ad esempio resetter.reset(account)), anche se questa è spesso una cattiva pratica (un'analogia comune è un negoziante che raggiunge il tuo portafoglio per ottenere denaro).

Nelle lingue con la capacità, potresti usare mixin o tratti, tuttavia, finirai nella posizione in cui hai iniziato dove potresti verificare l'esistenza di tali capacità.


Non tutti gli account potrebbero avere una password (dal punto di vista di questo particolare sistema). Ad esempio l'account può essere di terze parti ... google, facebook, ect. Per non dire che questa non è un'ottima risposta!
TheCatWhisperer,

0

Per questo esempio specifico di " può reimpostare una password ", consiglierei di utilizzare la composizione sull'ereditarietà (in questo caso, l'ereditarietà di un'interfaccia / contratto). Perché, facendo questo:

class Foo : IResetsPassword {
    //...
}

Stai specificando immediatamente (al momento della compilazione) che la tua classe " può reimpostare una password ". Ma, se nel tuo scenario, la presenza della capacità è condizionata e dipende da altre cose, non puoi più specificare le cose al momento della compilazione. Quindi, suggerisco di fare questo:

class Foo {

    PasswordResetter passwordResetter;

}

Ora, in fase di esecuzione, è possibile verificare se myFoo.passwordResetter != nullprima di eseguire questa operazione. Se vuoi separare ancora di più le cose (e prevedi di aggiungere molte più funzionalità), potresti:

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...

AGGIORNARE

Dopo aver letto alcune altre risposte e commenti dall'OP ho capito che la domanda non riguarda la soluzione compositiva. Quindi penso che la domanda sia su come identificare meglio le capacità degli oggetti in generale, in uno scenario come di seguito:

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, ...);
}

Ora proverò ad analizzare tutte le opzioni menzionate:

Secondo esempio OP:

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

Supponiamo che questo codice stia ricevendo una classe di account di base (ad esempio: BaseAccountnel mio esempio); questo è male poiché inserisce booleani nella classe base, inquinandolo con codice che non ha alcun senso essere lì.

Primo esempio OP:

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

Per rispondere alla domanda, questo è più appropriato dell'opzione precedente, ma a seconda dell'implementazione infrangerà il principio L di solido, e probabilmente verifiche del genere verrebbero diffuse nel codice e renderebbe più difficile l'ulteriore manutenzione.

Risposta di CandiedOrange:

account.ResetPassword(authority);

Se questo ResetPasswordmetodo viene inserito nella BaseAccountclasse, inquina anche la classe base con codice inappropriato, come nel secondo esempio di OP

Risposta del pupazzo di neve:

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

Questa è una buona soluzione, ma considera che le capacità sono dinamiche (e potrebbero cambiare nel tempo). Tuttavia, dopo aver letto diversi commenti di OP, credo che il discorso qui riguardi il polimorfismo e le classi definite staticamente (anche se l'esempio di account punta intuitivamente allo scenario dinamico). Ad esempio: in questo AccountManageresempio il controllo per l'autorizzazione sarebbe una query a DB; nella domanda OP i controlli sono tentativi di lanciare gli oggetti.

Un altro suggerimento da parte mia:

Utilizzare il modello Metodo modello per ramificazioni di alto livello. La gerarchia di classi menzionata viene mantenuta così com'è; creiamo solo gestori più appropriati per gli oggetti, al fine di evitare cast e proprietà / metodi inappropriati che inquinano la classe base.

//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(...);
    }
}

È possibile associare l'operazione alla classe di account appropriata utilizzando un dizionario / hashtable oppure eseguire operazioni di runtime utilizzando i metodi di estensione, utilizzare la dynamicparola chiave o come ultima opzione utilizzare solo un cast per passare l'oggetto account all'operazione (in in questo caso il numero di lanci è solo uno, all'inizio dell'operazione).


1
Funzionerebbe solo per implementare sempre il metodo "Execute ()" ma a volte non dovrebbe fare nulla? Restituisce un bool, quindi immagino che significhi che l'abbia fatto o no. Ciò lo restituisce al client: "Ho provato a reimpostare la password, ma non è successo (per qualsiasi motivo), quindi ora dovrei ..."

4
Avere i metodi "canX ()" e "doX ()" è un antipattern: se un'interfaccia espone un metodo "doX ()", è meglio essere in grado di fare X anche se fare X significa un no-op (es. Modello di oggetti null ). Non dovrebbe mai spettare agli utenti di un'interfaccia impegnarsi in un accoppiamento temporale (invocare il metodo A prima del metodo B) perché è troppo facile sbagliare e rompere le cose.

1
Avere interfacce non ha alcun rapporto con l'uso della composizione rispetto all'eredità. Rappresentano solo una funzionalità disponibile. Il modo in cui questa funzionalità entra in gioco è indipendente dall'uso dell'interfaccia. Quello che hai fatto qui è usare un'implementazione ad hoc di un'interfaccia senza alcun vantaggio.
Miyamoto Akira,

Interessante: la risposta più votata dice sostanzialmente ciò che dice il mio commento sopra. Alcuni giorni sei solo fortunato.

@nocomprende - sì, ho aggiornato la mia risposta in base a diversi commenti. Grazie per i feedback :)
Emerson Cardoso

0

Nessuna delle opzioni che hai presentato è buona OO. Se stai scrivendo se le istruzioni attorno al tipo di un oggetto, molto probabilmente stai facendo OO sbagliato (ci sono eccezioni, questa non è una.) Ecco la semplice risposta OO alla tua domanda (potrebbe non essere C # valido):

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!");}
  }

Ho modificato questo esempio in modo che corrispondesse a quello che stava facendo il codice originale. Sulla base di alcuni dei commenti, sembra che le persone si stiano bloccando se questo è il codice specifico "giusto" qui. Non è questo il punto di questo esempio. L'intero sovraccarico polimorfico consiste essenzialmente nell'esecuzione condizionale di diverse implementazioni della logica in base al tipo di oggetto. Quello che stai facendo in entrambi gli esempi è l'ostruzione manuale di ciò che la tua lingua ti offre come funzionalità. In breve, è possibile eliminare i sottotipi e mettere la possibilità di reimpostare come proprietà booleana del tipo di Conto (ignorando altre caratteristiche dei sottotipi).

Senza una visione più ampia del design, è impossibile dire se questa è una buona soluzione per il tuo particolare sistema. È semplice e se funziona per quello che stai facendo, probabilmente non avrai mai bisogno di pensarci ancora a meno che qualcuno non riesca a controllare CanResetPassword () prima di chiamare ResetPassword (). Potresti anche restituire un valore booleano o fallire in silenzio (non consigliato). Dipende davvero dalle specifiche del design.


Non è possibile ripristinare tutti gli account. Inoltre, un account potrebbe avere più funzionalità rispetto al ripristino?
TheCatWhisperer,

2
"Non è possibile ripristinare tutti gli account." Non sono sicuro se questo è stato scritto prima della modifica. La seconda classe è NotResetable. "potrebbe un account avere più funzionalità che essere ripristinabile" Mi aspetterei di sì, ma non sembra essere importante per la domanda a portata di mano.
JimmyJames,

1
Questa soluzione va nella giusta direzione, penso, poiché fornisce una buona soluzione all'interno di OO. Probabilmente cambierebbe il nome del nome dell'interfaccia in IPasswordReseter (o qualcosa in tal senso), per separare le interfacce. IAccount potrebbe essere dichiarato come supporto di più altre interfacce. Ciò ti darebbe la possibilità di avere classi con l'implementazione che viene quindi composta e utilizzata dalla delega sugli oggetti del dominio. @JimmyJames, nitpick minore, su C # entrambe le classi dovranno specificare che implementano IAccount. Altrimenti il ​​codice sembra un po 'strano ;-)
Miyamoto Akira l'

1
Inoltre, controlla un commento di Snowman in una delle altre risposte su CanX () e DoX (). Non sempre un anti-pattern, tuttavia, come ha usato (ad esempio sul comportamento dell'interfaccia utente)
Miyamoto Akira,

1
Questa risposta inizia bene: questa è una cattiva OOP. Sfortunatamente la tua soluzione è altrettanto negativa perché sovverte anche il sistema dei tipi ma genera un'eccezione. Una buona soluzione sembra diversa: non avere metodi non implementati su un tipo. Il framework .NET utilizza effettivamente il tuo approccio per alcuni tipi, ma è stato inequivocabilmente un errore.
Konrad Rudolph,

0

Risposta

La mia risposta leggermente supponente alla tua domanda è:

Le capacità di un oggetto dovrebbero essere identificate da sole, non implementando un'interfaccia. Un linguaggio staticamente fortemente tipizzato limiterà questa possibilità, tuttavia (in base alla progettazione).

Addendum:

La ramificazione sul tipo di oggetto è malvagia.

vagante

Vedo che non hai aggiunto il c#tag, ma lo chiedi in un object-orientedcontesto generale .

Quello che stai descrivendo è un tecnicismo di linguaggi statici / tipizzati e non specifici di "OO" in generale. Il tuo problema deriva, tra l'altro, dal fatto che non puoi chiamare metodi in quelle lingue senza avere un tipo sufficientemente stretto in fase di compilazione (tramite definizione variabile, parametro del metodo / definizione del valore restituito o cast esplicito). Ho fatto entrambe le tue varianti in passato, in questo tipo di lingue, ed entrambi mi sembrano brutti in questi giorni.

Nei linguaggi OO tipizzati in modo dinamico, questo problema non si verifica a causa di un concetto chiamato "digitazione anatra". In breve, ciò significa che fondamentalmente non si dichiara il tipo di un oggetto ovunque (tranne quando lo si crea). Si inviano messaggi agli oggetti e gli oggetti gestiscono tali messaggi. Non ti importa se l'oggetto ha effettivamente un metodo con quel nome. Alcune lingue hanno anche un generico, tutto sommatomethod_missing metodo che lo rende ancora più flessibile.

Quindi, in una tale lingua (Ruby, per esempio), il tuo codice diventa:

account.reset_password

Periodo.

Il account sarà o reimpostare la password, un'eccezione "negato", o di lanciare un "non so come gestire 'reset_password'" eccezione.

Se hai bisogno di diramare esplicitamente per qualsiasi motivo, lo faresti comunque sulla capacità, non sulla classe:

account.reset_password  if account.can? :reset_password

(Dove can? solo un metodo che controlla se l'oggetto può eseguire qualche metodo, senza chiamarlo.)

Nota che mentre la mia preferenza personale è attualmente in lingue tipizzate dinamicamente, questa è solo una preferenza personale. Le lingue tipizzate staticamente hanno qualcosa che va bene anche per loro, quindi per favore non prendere questa confusione come una bash contro le lingue tipizzate staticamente ...


Fantastico avere la tua prospettiva qui! Dal lato java / C # tendiamo a dimenticare che esiste un diverso ramo di OOP. Quando dico ai miei colleghi, JS (per tutti i suoi problemi) è, in molti modi, più OO che C #, ridono di me!
TheCatWhisperer,

Adoro C #, e penso che sia un buon linguaggio per scopi generici , ma è una mia opinione personale, che in termini di OO, è piuttosto scarsa. In entrambe le caratteristiche e pratiche, tende a incoraggiare la programmazione procedurale.
TheCatWhisperer,

2
Probabilmente, testare l'esistenza di un metodo in un linguaggio tipizzato a forma di anatra è lo stesso del test per l'implementazione di un'interfaccia in un linguaggio tipizzato staticamente: si sta eseguendo un controllo di runtime se l'oggetto ha una particolare capacità. Ogni dichiarazione di metodo è effettivamente la propria interfaccia, identificata solo dal nome del metodo, e la sua esistenza non può essere utilizzata per inferire altre informazioni sull'oggetto.
IMSoP,

0

Sembra che le tue opzioni derivino da diverse forze motivazionali. Il primo sembra essere un hack impiegato a causa della modellazione di oggetti scadente. Ha dimostrato che le tue astrazioni sono sbagliate, poiché rompono il polimorfismo. Molto probabilmente rompe il principio chiuso di Open , che si traduce in un accoppiamento più stretto.

La tua seconda opzione potrebbe andare bene dal punto di vista OOP, ma il tipo casting mi confonde. Se il tuo account ti consente di reimpostare la sua password, perché hai bisogno di un typecasting? Quindi supponendo che la tua CanResetPasswordclausola rappresenti alcuni requisiti aziendali fondamentali che fanno parte dell'astrazione dell'account, la tua seconda opzione è OK.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.