Come dovrebbe una classe comunicare ai chiamanti durante il runtime quali metodi di interfaccia attualmente supporta?

4

( Ho chiesto a domanda simile che risponde focalizzata sul tempo di compilazione anziché sul runtime. Invece di aggiungere i requisiti di runtime all'altra domanda e invalidare le buone risposte, ho deciso di porre una nuova domanda incentrata su comunicazione di runtime )

Scenario

Un'applicazione web definisce un'interfaccia di backend utente IUserBackend con i metodi

  • getUser (UID)
  • createUser (UID)
  • deleteUser (UID)
  • setPassword (uid, password)
  • ...

Diversi backend dell'utente (ad esempio LDAP, SQL, ...) implementano questa interfaccia ma non tutti i backend possono fare tutto. Ad esempio, un server SQL concreto non consente (autorizzazioni a DELETE ) questo backend dell'utente per eliminare utenti. Oppure l'amministratore non ha impostato le query SQL utilizzate per creare utenti.

Il back-end dell'utente deve reagire a questo durante il runtime e comunicare all'applicazione Web se può o meno eliminare utenti con la configurazione corrente.

Soluzione nota

Ho visto una soluzione in cui IUserInterface ha un metodo implementedActions che restituisce un intero che è il risultato di OR bit a bit delle azioni AND bit a bit con le azioni richieste:

function implementedActions(requestedActions) {
    return (bool)(
        ACTION_GET_USER
        | ACTION_CREATE_USER
        | ACTION_DELTE_USER
        | ACTION_SET_PASSWORD
        ) & requestedActions)
}

Dove

  • ACTION_GET_USER = 1
  • ACTION_CREATE_USER = 2
  • ACTION_DELETE_USER = 4
  • ACTION_SET_PASSWORD = 8
  • .... = 16
  • .... = 32

ecc.

Quindi l'applicazione web imposta una maschera di bit con ciò di cui ha bisogno e implementedActions() risponde con un booleano indipendentemente dal fatto che li supporti.

PARERE

Queste operazioni bit mi sembrano reliquie dal C age, non necessariamente facili da capire in termini di codice pulito.

Domanda

Che cos'è un modello moderno (migliore?) per la classe per comunicare il sottoinsieme dei metodi di interfacce implementati per una data configurazione durante il runtime? Oppure il "metodo operativo bit" dall'alto è ancora la migliore pratica?

( Nel caso abbia importanza: PHP, anche se sto cercando una soluzione generale per le lingue OO )

    
posta problemofficer 27.04.2018 - 20:24
fonte

4 risposte

2

Fai chiedere al chiamante. Pseudocodice:

ACTION = {CREATE, UPDATE, DELETE}

class FooGateway {
   function supports(action) {
      return {ACTION.CREATE, ACTION.UPDATE}.contains(action)
   }

   // The interface requires this, so fail if the caller attempts.
   function delete() {
      error('DELETE not supported')
   }
   // etc
}

Quindi il chiamante è come

gateway = findGatewayFor('Foo')
if gateway != null and gateway.supports(ACTION.DELETE) {
   gateway.delete(something)
   return "Deleted that!"
} else {
   return "Oops, got no way to delete that."
}
    
risposta data 27.04.2018 - 20:40
fonte
1

Come variante di questo, ogni azione che può essere eseguita su un utente può essere incapsulata da un oggetto e un'interfaccia che avvolge il controllo "è supportato" e l'esecuzione dell'azione. Accoppia questo con una factory che implementa un'interfaccia generica e finisci con un modo più sicuro e orientato agli oggetti per farlo.

Prima l'interfaccia per la "fabbrica di azioni":

interface IUserActionFactory {
    function getUser() : IGetUserAction;
    function changePassword() : IChangePasswordAction;
    function deleteUser() : IDeleteUserAction;
}

class LdapUserActionFactory implements IUserActionFactory {
    function getUser() {
        return new LdapGetUserAction();
    }

    function changePassword() {
        return new LdapChangePasswordAction();
    }

    function deleteUser() {
        return new LdapDeleteUserAction();
    }
}

Niente di spettacolare. Questi metodi restituiscono oggetti che supportano un'interfaccia. Senza ulteriori adu, le interfacce:

interface IChangePasswordAction {
    function isSupported();
    function execute($username, $oldPassword, $newPassword);
}

interface IGetUserAction {
    function isSupported();
    function execute($username);
}

interface IDeleteUserAction {
    function isSupported();
    function execute($username);
}

Ora tutto ciò che devi fare è usarli (più sull'implementazione successiva):

class UserService {
    private $actionFactory;

    public function __construct(IUserActionFactory $actionFactory) {
        $this->actionFactory = $actionFactory;
    }

    public function getUser($username) {
        $getUserAction = $this->actionFactory->getUser();

        if ($getUserAction->isSupported()) {
            return $getUserAction->execute($username);
        }
        else {
            return null;
        }
    }

    public function changePassword($username, $oldPassword, $newPassword) {
        $changePasswordAction = $this->actionFactory->changePassword();

        if ($changePasswordAction->isSupported()) {
            return $changePasswordAction->execute($username, $oldPassword, $newPassword);
        }
        else {
            return false;
        }
    }

    public function deleteUser($username) {
        $deleteUserAction = $this->actionFactory->deleteUser();

        if ($deleteUserAction->isSupported()) {
            return $deleteUserAction->execute($username);
        }
        else {
            return false;
        }
    }
}

UserService ha solo bisogno di un oggetto IUserActionFactory come argomento del costruttore. Quindi, eseguire ogni azione diventa una metodologia di controllo e chiamata prevedibile.

Nelle classi che implementano ciascuna azione puoi specializzarti in un particolare servizio utente, ad esempio LDAP:

// Generic exception for when an action cannot be executed
class ActionNotSupportedException extends Exception { }

class LdapGetUserAction implements IGetUserAction {
    public function isSupported() {
        return true;
    }

    public function execute($username) {
        $user = // get from LDAP

        return $user;
    }   
}

class LdapChangePasswordAction implements IChangePasswordAction {
    function isSupported() {
        return true;
    }

    function execute($username, $oldPassword, $newPassword) {
        if (/* password gets changed */) {
            return true;
        }
        else {
            return false;
        }
    }
}

class LdapDeleteUserAction implements IDeleteUserAction {
    public function isSupported() {
        return false;
    }

    public function execute($username) {
        throw new ActionNotSupportedException("Cannot delete users via LDAP");
    }
}

Suppongo che potresti chiamare il metodo "execute" qualcosa di specifico, come "executeGetUser" o "getUser". Con nomi di metodi univoci potresti avere 1 classe che implementa tutte le interfacce.

Ora se viene lanciata un'eccezione dovrebbe essere perché, diciamo, LDAP va giù e il server non è raggiungibile, o si tenta di cambiare una password per un utente che non esiste, invece di cercare di interpretare le eccezioni come "questo cosa non è supportata. "

E per usarlo:

$userService = new UserService(new LdapUserActionFactory());

if ($userService->deleteUser($POST['username'])) {
    echo "User deleted";
}
else {
    echo "You cannot delete this user";
}

Cambiare la gestione degli utenti è facile una volta che hai le classi di implementazione:

$userService = new UserService(new ActiveDirectoryUserActionFactory());

if ($userService->deleteUser($POST['username'])) {
    echo "User deleted";
}
else {
    echo "You cannot delete this user";
}

Se chiami un metodo su $userService e lancia una ActionNotSupportedException, allora sai di avere un difetto da correggere in UserFactory. Non dovrebbe generare questa eccezione.

    
risposta data 27.04.2018 - 22:23
fonte
1

Non ho familiarità con le sintassi PHP ma forse potresti sfruttare lambdas, se PHP ti permette di definire un'interfaccia come un insieme di "getter" che restituiscono le funzioni da chiamare. (Ad esempio, variabili lambda readonly di firme specifiche)

Ogni implementazione concreta nel suo costruttore imposta le variabili lambda effettive su una funzione specifica o su una lambda passata nel costruttore che specifica l'azione da intraprendere quando la funzione non è disponibile. Inoltre, il costruttore può accettare una lambda per se stessa (se PHP consente di specificare la firma), per informare il cliente in anticipo, se necessario.

In questo modo il chiamante non ha bisogno di enumerare o controllare ciò che è disponibile, può dire al costruttore cosa fare quando una funzione non è disponibile. Ciò consente di richiamare in modo reattivo qualcosa sul lato dell'applicazione Web ogni volta che viene chiamato un messaggio non disponibile ("Eliminazione di questo utente non è disponibile") o di eseguire attivamente qualcosa nel momento in cui viene creata l'implementazione per passare di nuovo un oggetto all'applicazione Web in anticipo di qualsiasi azione dell'utente (es .: nascondere il pulsante cancella utente). Le static lambda potrebbero essere utili per farlo all'inizializzazione del programma.

    
risposta data 28.04.2018 - 00:03
fonte
0

Hai iniziato con il piede sbagliato. Controlla la I in SOLID: il principio di separazione delle interfacce.

Un'interfaccia dovrebbe essere piccola, cioè definire una sola capacità. Ottenere qualcosa ed eliminare qualcosa sono diverse funzionalità nel dominio del problema, quindi per le interfacce separate. E una volta che hai il tuo problema sarà sparito.

Usa l'operatore di tipo instanceOf .

    
risposta data 28.04.2018 - 08:21
fonte

Leggi altre domande sui tag