Usare l'ordine di risoluzione del metodo di Python per l'iniezione di dipendenza - è male?


11

Ho visto Pycon di Raymond Hettinger parlare di "Super Considered Super" e ho imparato un po 'dell'MRO (Method Resolution Order) di Python che linearizza le classi "parent" in modo deterministico. Possiamo usarlo a nostro vantaggio, come nel codice seguente, per fare l'iniezione di dipendenza. Quindi ora, naturalmente, voglio usare superper tutto!

Nell'esempio seguente, la Userclasse dichiara le sue dipendenze ereditando da entrambi LoggingServicee UserService. Questo non è particolarmente speciale. La parte interessante è che possiamo usare l'Ordine di risoluzione del metodo anche deridere le dipendenze durante i test delle unità. Il codice seguente crea un MockUserServiceelemento che eredita UserServicee fornisce un'implementazione dei metodi che vogliamo deridere. Nell'esempio seguente, forniamo un'implementazione di validate_credentials. Per poter MockUserServicegestire qualsiasi chiamata, validate_credentialsè necessario posizionarlo prima UserServicenell'MRO. Ciò è stato creato creando una classe wrapper attorno a Userrichiamata MockUsere facendola ereditare da Usere MockUserService.

Ora, quando lo facciamo MockUser.authenticatee, a sua volta, si chiama in super().validate_credentials() MockUserServiceprecedenza UserServicenell'ordine di risoluzione del metodo e, poiché offre un'implementazione concreta di validate_credentialsquesta implementazione, verrà utilizzata. Sì, abbiamo deriso con successo i UserServicenostri test unitari. Considera che UserServicepotrebbe fare alcune costose chiamate di rete o di database: abbiamo appena rimosso il fattore di latenza di questo. Non vi è inoltre alcun rischio di UserServicetoccare i dati live / prod.

class LoggingService(object):
    """
    Just a contrived logging class for demonstration purposes
    """
    def log_error(self, error):
        pass


class UserService(object):
    """
    Provide a method to authenticate the user by performing some expensive DB or network operation.
    """
    def validate_credentials(self, username, password):
        print('> UserService::validate_credentials')
        return username == 'iainjames88' and password == 'secret'


class User(LoggingService, UserService):
    """
    A User model class for demonstration purposes. In production, this code authenticates user credentials by calling
    super().validate_credentials and having the MRO resolve which class should handle this call.
    """
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def authenticate(self):
        if super().validate_credentials(self.username, self.password):
            return True
        super().log_error('Incorrect username/password combination')
        return False

class MockUserService(UserService):
    """
    Provide an implementation for validate_credentials() method. Now, calls from super() stop here when part of MRO.
    """
    def validate_credentials(self, username, password):
        print('> MockUserService::validate_credentials')
        return True


class MockUser(User, MockUserService):
    """
    A wrapper class around User to change it's MRO so that MockUserService is injected before UserService.
    """
    pass

if __name__ == '__main__':
    # Normal useage of the User class which uses UserService to resolve super().validate_credentials() calls.
    user = User('iainjames88', 'secret')
    print(user.authenticate())

    # Use the wrapper class MockUser which positions the MockUserService before UserService in the MRO. Since the class
    # MockUserService provides an implementation for validate_credentials() calls to super().validate_credentials() from
    # MockUser class will be resolved by MockUserService and not passed to the next in line.
    mock_user = MockUser('iainjames88', 'secret')
    print(mock_user.authenticate())

Sembra abbastanza intelligente, ma è un buon e valido uso dell'eredità multipla di Python e dell'ordine di risoluzione dei metodi? Quando penso all'eredità nel modo in cui ho appreso OOP con Java, mi sembra completamente sbagliato perché non possiamo dire che Usersia UserServiceo Usersia un LoggingService. Pensare in questo modo, usare l'ereditarietà come usa il codice sopra riportato non ha molto senso. O è? Se usiamo l'eredità solo per fornire il riutilizzo del codice e non pensare in termini di relazioni genitori-> figli, allora questo non sembra così male.

Sto sbagliando?


Sembra che ci siano due domande diverse qui: "Questo tipo di manipolazione MRO è sicuro / stabile?" e "È inesatto affermare che l'ereditarietà di Python modella una relazione" is-a "? Stai cercando di chiedere a entrambi, o solo a uno di loro? (sono entrambe buone domande, voglio solo assicurarti di rispondere a quella giusta, o dividerlo in due domande se non le vuoi entrambe)
Ixrec

Ho affrontato le domande mentre le leggevo, ho lasciato fuori qualcosa?
Aaron Hall,

@lxrec Penso che tu abbia assolutamente ragione. Sto cercando di porre due domande diverse. Penso che la ragione per cui questo non sembri "giusto" è perché sto pensando a "è-uno" stile di eredità (quindi GoldenRetriever "è -un" Cane e cane "è-un" Animale) invece di questo tipo di approccio compositivo. Penso che questo sia qualcosa per cui potrei aprire un'altra domanda :)
Iain

Anche questo mi confonde in modo significativo. Se la composizione è preferibile all'eredità, perché non passare istanze di LoggingService e UserService al costruttore dell'utente e impostarle come membri? Quindi è possibile utilizzare la tipizzazione duck per l'iniezione delle dipendenze e passare invece un'istanza di MockUserService al costruttore User. Perché è preferibile usare super per DI?
Jake Spracher,

Risposte:


7

Usare l'ordine di risoluzione del metodo di Python per l'iniezione di dipendenza - è male?

No. Questo è un uso teorico previsto dell'algoritmo di linearizzazione C3. Questo è in contrasto con le tue familiari relazioni is-a, ma alcuni considerano la composizione da preferire all'eredità. In questo caso, hai composto alcune relazioni has-a. Sembra che tu sia sulla strada giusta (anche se Python ha un modulo di registrazione, quindi la semantica è un po 'discutibile, ma come esercizio accademico va perfettamente bene).

Non penso che deridere o rattoppare le scimmie sia una cosa negativa, ma se riesci ad evitarli con questo metodo, è un bene per te - con una maggiore complessità, hai evitato di modificare le definizioni delle classi di produzione.

Sto sbagliando?

Sembra buona. Hai scavalcato un metodo potenzialmente costoso, senza patch scimmia o usando una patch finta, il che, di nuovo, significa che non hai nemmeno modificato direttamente le definizioni della classe di produzione.

Se l'intento era quello di esercitare la funzionalità senza effettivamente avere credenziali nel test, probabilmente dovresti fare qualcosa del tipo:

>>> print(MockUser('foo', 'bar').authenticate())
> MockUserService::validate_credentials
True

invece di usare le tue credenziali reali e controlla che i parametri siano ricevuti correttamente, magari con asserzioni (dato che questo è il codice di prova, dopo tutto.):

def validate_credentials(self, username, password):
    print('> MockUserService::validate_credentials')
    assert username_ok(username), 'username expected to be ok'
    assert password_ok(password), 'password expected to be ok'
    return True

Altrimenti, sembra che tu l'abbia capito. Puoi verificare l'MRO in questo modo:

>>> MockUser.mro()
[<class '__main__.MockUser'>, 
 <class '__main__.User'>, 
 <class '__main__.LoggingService'>, 
 <class '__main__.MockUserService'>, 
 <class '__main__.UserService'>, 
 <class 'object'>]

E puoi verificare che il MockUserServiceabbia la precedenza sul UserService.

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.