Utilizzo dell'ordine di risoluzione dei metodi di Python per l'iniezione delle dipendenze - è così male?

9

Ho visto il discorso di Raymond Hettinger su Pycon "Super Considerato Super" e ho imparato un po 'sull'MRO di Python (Method Resolution Order) che linearizza le classi "parent" delle classi in modo deterministico. Possiamo usare questo a nostro vantaggio, come nel codice sottostante, per fare un'iniezione di dipendenza. Quindi ora, naturalmente, voglio usare super per tutto!

Nell'esempio seguente, la classe User dichiara le sue dipendenze ereditando da LoggingService e UserService . Questo non è particolarmente speciale. La parte interessante è che possiamo usare l'ordine di risoluzione dei metodi anche per deridere le dipendenze durante i test unitari. Il codice seguente crea un MockUserService che eredita da UserService e fornisce un'implementazione dei metodi che vogliamo prendere in giro. Nell'esempio seguente, forniamo un'implementazione di validate_credentials . Per fare in modo che% co_de gestisca le chiamate su MockUserService , dobbiamo posizionarlo prima di validate_credentials nell'MRO. Ciò avviene creando una classe wrapper attorno a UserService chiamata User e ereditandola da MockUser e User .

Ora, quando facciamo MockUserService e, a sua volta, le chiamate a MockUser.authenticate super().validate_credentials() è prima di MockUserService nell'ordine di risoluzione dei metodi e, poiché offre un'implementazione concreta di UserService , questa implementazione essere usato. Sì, abbiamo superato con successo validate_credentials nei nostri test unitari. Considera che UserService potrebbe effettuare alcune costose chiamate di rete o di database: abbiamo appena rimosso il fattore di latenza. Inoltre, non vi è alcun rischio che UserService tocchi 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())

Questo sembra abbastanza intelligente, ma è un uso valido e valido dell'eredità multipla di Python e dell'ordine di risoluzione dei metodi? Quando penso all'ereditarietà nel modo in cui ho imparato l'OOP con Java, mi sento completamente sbagliato perché non possiamo dire che UserService è un User o UserService è un User . Pensando in questo modo, l'uso dell'ereditarietà come il codice precedente lo usa non ha molto senso. O è? Se usiamo l'ereditarietà solo per fornire il riutilizzo del codice, e non pensando in termini di relazioni genitore-genitore, allora questo non sembra così male.

Sto sbagliando?

    
posta Iain 01.01.2016 - 23:50
fonte

1 risposta

6

Using Python's Method Resolution Order for Dependency Injection - is this bad?

No. Questo è un uso teorico previsto dell'algoritmo di linearizzazione C3. Questo va contro il tuo familiare è-una relazione, 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 benissimo).

Non penso che il mocking o il patch delle scimmie sia una cosa negativa, ma se puoi evitarli con questo metodo, fa bene a te - con una complessità dichiaratamente maggiore, hai evitato di modificare le definizioni della classe di produzione.

Am I doing it wrong?

Sembra buono. Hai scavalcato un metodo potenzialmente costoso, senza patch di scimmia o usando una patch fittizia, che, ancora una volta, significa che non hai nemmeno modificato direttamente le definizioni della classe di produzione.

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

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

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

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 MockUserService abbia la precedenza su UserService .

    
risposta data 02.01.2016 - 01:02
fonte