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

36

L'operatore is di C # e l'operatore instanceof di Java ti consentono di diramare sull'interfaccia (o, in modo più inconsapevole, la sua classe base) implementata da un'istanza dell'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 trappole per l'utilizzo dell'interfaccia per l'identificazione delle capacità?

Questo esempio era solo questo, un esempio. Mi sto chiedendo di un'applicazione più generale.

    
posta TheCatWhisperer 11.12.2017 - 18:28
fonte

8 risposte

7

Con l'avvertenza che è meglio evitare il downcast, se possibile, allora il primo modulo è preferibile per due motivi:

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

Questo non vuol dire che non puoi migliorare questi problemi in qualche modo. Ad esempio, potresti avere la classe base con un set di interfacce pubblicate che puoi controllare nell'inclusione. Ma in realtà stai implementando manualmente solo una parte del sistema di tipi esistente che è ridondante.

A proposito, la mia preferenza naturale è la composizione sull'eredità, ma soprattutto per quanto riguarda lo stato. L'ereditarietà dell'interfaccia non è così male, di solito. Inoltre, se ti trovi a implementare la gerarchia di tipi di un uomo povero, è meglio usare quello che è già lì come il compilatore ti aiuterà.

    
risposta data 11.12.2017 - 19:32
fonte
90

Should an object's capabilities be identified exclusively by the interfaces it implements?

Le funzionalità degli oggetti non dovrebbero essere identificate affatto.

Il client che usa un oggetto non dovrebbe essere tenuto a sapere nulla su come funziona. Il client dovrebbe conoscere solo le cose che può dire all'oggetto. Ciò che l'oggetto fa, una volta che è stato detto, non è il problema dei clienti.

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

considerare

account.ResetPassword();

o

account.ResetPassword(authority);

perché account sa già se funzionerà. Perché chiedere? Dillo solo quello che vuoi e lascia che faccia qualunque cosa faccia.

Immagina che ciò avvenga in modo tale che al cliente non importi se ha funzionato o meno, perché questo è un altro problema. Il lavoro dei clienti era solo per fare il tentativo. 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 scrivi al cliente di pensare di dover tenere traccia di tutto e quindi tirare verso di te tutto ciò che pensi di aver bisogno di sapere. Quando lo fai rovesci gli oggetti. Valuta la tua ignoranza. Spingi i dettagli e lascia che gli oggetti li gestiscano. Meno ne sai e meglio è.

    
risposta data 11.12.2017 - 22:41
fonte
17

Sfondo

L'ereditarietà è uno strumento potente 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 che tu abbia un diploma in CS), potresti ricordare un professore che ti fornisce un paragrafo che stabilisce cosa il cliente vuole che il software faccia. Il tuo compito è leggere il paragrafo, identificare gli attori e le azioni, e ottenere una descrizione approssimativa delle classi e dei metodi. Ci saranno dei barboni che sembrano importanti, ma non lo sono. C'è anche una reale possibilità di interpretare erroneamente i requisiti.

Questa è una competenza importante che anche i più esperti di noi hanno sbagliato: identificare correttamente i requisiti e tradurli in linguaggi macchina.

La tua domanda

In base a ciò che hai scritto, penso che potresti fraintendere il caso generale delle azioni opzionali che le classi possono eseguire. Sì, so che il tuo codice è solo un esempio e ti interessa il caso generale. Tuttavia, sembra che tu voglia sapere come gestire la situazione in cui alcuni sottotipi di un oggetto possono eseguire un'azione, ma altri sottotipi non possono.

Solo perché un oggetto come account ha un tipo di account non significa che si traduce in un tipo in un linguaggio OO. "Digitare" in una lingua umana non significa sempre "classe". Nel contesto di un account, "type" potrebbe essere più strettamente correlato con "set di autorizzazioni". Si desidera utilizzare un account utente per eseguire un'azione, ma tale azione potrebbe essere o non essere in grado di essere eseguita da tale account. Anziché utilizzare l'ereditarietà, utilizzerei un delegato o un token di sicurezza.

La mia soluzione

Considera una classe di account con diverse azioni facoltative che può eseguire. Invece di definire "può eseguire l'azione X" tramite l'ereditarietà, perché non fare in modo che l'account restituisca un oggetto delegato (resetter password, form submitter, 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 devo fare se desidero utilizzare le credenziali dell'account my per reimpostare la password di qualcuno altro?

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

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

TL; DR: si legge come un problema XY . In genere, quando ci si trova di fronte a due opzioni non ottimali, vale la pena fare un passo indietro e pensare "che cosa sto davvero cercando di ottenere qui?" Dovrei davvero pensare a come rendere meno brutto il typecasting, o dovrei cercare modi per rimuovere completamente il typecast? "

    
risposta data 11.12.2017 - 21:13
fonte
1

Bill Venner afferma che il tuo approccio è assolutamente soddisfacente ; passare alla sezione intitolata 'When to Use instanceof'. Stai eseguendo il downcast su un tipo / interfaccia specifico che implementa solo quel comportamento specifico, quindi hai assolutamente ragione di essere selettivo a riguardo.

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

C'è l'approccio al polimorfismo; si potrebbe 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 dell'account di base, dovremmo avere un metodo resetPassword() 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 in silenzio se reimposta la password o meno, assumendosi la responsabilità di stampare internamente il messaggio se non reimposta la password. Non è la migliore idea.

Potrebbe restituire un valore booleano che indica se il reset ha avuto successo. Accendendo quel booleano, potremmo riferire che la reimpostazione della password non è andata a buon fine.

Potrebbe restituire una stringa che indica il risultato del tentativo di reimpostazione della password. La stringa potrebbe fornire maggiori dettagli sul motivo per cui 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 void e invece lanciare un'eccezione se provi a resettare un account che non ha questa capacità (non farlo poiché usare la gestione delle eccezioni per il controllo di flusso normale è una cattiva pratica per una serie di motivi) .

Avere un metodo di corrispondenza canResetPassword() potrebbe non sembrare la cosa peggiore al mondo, dal momento che si tratta di una capacità statica integrata nella classe quando è stata scritta. Questo è un indizio sul motivo per cui l'approccio al metodo è una cattiva idea, tuttavia, poiché suggerisce che la capacità è dinamica e che canResetPassword() potrebbe cambiare, il che crea anche la possibilità aggiunta che potrebbe cambiare tra chiedere il permesso e fare il chiamata. Come accennato altrove, dirlo piuttosto che chiedere il permesso.

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

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

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

    
risposta data 13.12.2017 - 16:15
fonte
0

Per questo specifico esempio di " puoi reimpostare una password ", ti consigliamo di utilizzare la composizione sull'ereditarietà (in questo caso, l'ereditarietà di un'interfaccia / contratto) . Perché, in questo modo:

class Foo : IResetsPassword {
    //...
}

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

class Foo {

    PasswordResetter passwordResetter;

}

Ora, in fase di esecuzione, è possibile verificare se myFoo.passwordResetter != null prima di eseguire questa operazione. Se vuoi disaccoppiare le cose ancora di più (e hai intenzione 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...

UPDATE

Dopo aver letto alcune altre risposte e commenti da OP ho capito che la domanda non riguarda la soluzione compositiva. Quindi penso che la domanda riguardi come identificare meglio le capacità degli oggetti in generale, in uno scenario come il seguente:

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 citate:

OP secondo esempio:

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

Diciamo che questo codice sta ricevendo alcune classi di account di base (es: BaseAccount nel mio esempio); questo non va bene dato che sta inserendo i booleani nella classe base, inquinandolo con un codice che non ha assolutamente senso essere lì.

Primo esempio di 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 rispetto all'opzione precedente, ma a seconda dell'implementazione interromperà il principio di Solid, e probabilmente controlli come questo sarebbero diffusi attraverso il codice e renderebbe più difficile l'ulteriore manutenzione.

Anomalia di CandiedOrange:

account.ResetPassword(authority);

Se questo metodo ResetPassword è inserito in BaseAccount class, allora sta anche inquinando 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 ritiene che le capacità siano dinamiche (e potrebbero cambiare nel tempo). Tuttavia, dopo aver letto diversi commenti da OP, credo che il discorso qui riguardi il polimorfismo e le classi definite staticamente (anche se l'esempio degli account indica intuitivamente lo scenario dinamico). EG: in questo esempio di AccountManager il controllo per il permesso sarebbe interrogato su DB; nella domanda OP i controlli sono tentativi di lancio degli oggetti.

Un altro suggerimento da parte mia:

Usa un modello di metodo Template per la ramificazione di alto livello. La gerarchia di classe menzionata viene mantenuta così com'è; creiamo solo gestori più appropriati per gli oggetti, al fine di evitare calchi e proprietà / metodi inappropriati per la polarizzazione della 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 o eseguire operazioni di runtime utilizzando i metodi di estensione, utilizzare la parola chiave dynamic oppure utilizzare l'ultima opzione solo un cast per passare l'oggetto account all'operazione (in questo caso il numero di cast è solo uno, all'inizio dell'operazione).

    
risposta data 11.12.2017 - 19:05
fonte
0

Nessuna delle opzioni che hai presentato è buona OO. Se stai scrivendo istruzioni riguardo al tipo di oggetto, probabilmente stai facendo OO in modo sbagliato (ci sono delle eccezioni, questa non è una.) Ecco la semplice risposta OO alla tua domanda (potrebbe non essere valido 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!");}
  }

Ho modificato questo esempio per corrispondere a ciò che stava facendo il codice originale. Sulla base di alcuni commenti, sembra che la gente si stia appisolando sul fatto che questo sia il codice specifico "giusto" qui. Questo non è il punto di questo esempio. L'intero overload Polymorphic è essenzialmente quello di eseguire in modo condizionale diverse implementazioni di logica in base al tipo dell'oggetto. Quello che stai facendo in entrambi gli esempi è l'inceppamento manuale di ciò che la tua lingua ti offre come caratteristica. In breve, puoi eliminare i sottotipi e mettere la possibilità di reimpostare come proprietà booleane del tipo di account (ignorando altre funzionalità dei sottotipi).

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

    
risposta data 11.12.2017 - 21:37
fonte
0

risposta

La mia risposta un po 'supponente alla tua domanda è:

The capabilities of an object should be identified through themselves, not by implementing an interface. A statically, strongly typed language will limit this possibility, though (by design).

Addendum:

Branching on object type is evil.

Rambling

Vedo che non hai aggiunto il tag c# , ma ti stai chiedendo in un contesto object-oriented generale.

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

In lingue OO tipizzate dinamicamente, questo problema non si verifica a causa di un concetto chiamato "digitazione anatra". In breve, ciò significa che in pratica non si dichiara il tipo di un oggetto in nessun punto (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. Alcuni linguaggi hanno persino un metodo generico, catch-all method_missing , che lo rende ancora più flessibile.

Quindi, in un tale linguaggio (Ruby, ad esempio), il tuo codice diventa:

account.reset_password

Periodo.

Il account reimposta la password, genera un'eccezione "negata" o genera un'eccezione "non so come gestire 'reset_password'".

Se hai bisogno di diramarti 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 su lingue digitate dinamicamente, questa è solo una preferenza personale. Anche le lingue tipizzate in modo statico hanno le stesse caratteristiche, quindi per favore non prendete questo vagabondaggio come una battuta contro le lingue tipizzate staticamente ...

    
risposta data 12.12.2017 - 16:53
fonte
0

Sembra che le tue opzioni derivino da diverse forze motivazionali. Il primo sembra essere un hackerato a causa della modellazione di oggetti scadente. Ha dimostrato che le tue astrazioni sono sbagliate, dal momento che rompono il polimorfismo. Più probabilmente che non si rompe principio Open closed , che si traduce in un accoppiamento più stretto .

La tua seconda opzione potrebbe essere soddisfacente dalla prospettiva OOP, ma il casting di tipo mi confonde. Se il tuo account ti consente di reimpostare la sua password, perché hai bisogno di un typecasting? Pertanto, supponendo che la tua clausola CanResetPassword rappresenti un requisito aziendale fondamentale che fa parte dell'astrazione dell'account, la seconda opzione è OK.

    
risposta data 19.12.2017 - 21:35
fonte

Leggi altre domande sui tag