J'apprends à intégrer IObservable dans mon code. Voici deux approches différentes pour une classe simple qui imprime le mot le plus récent d'un IObservable<string>. Quelle est l'approche la plus propre? Je n'aime pas WordPrinterWithCache car il introduit une variable d'état supplémentaire (la _lastWord) et le code intéressant est maintenant dispersé dans toute la classe. Je préfère WordPrinterWithExtraSubject car le code intéressant est localisé chez le constructeur. Mais surtout, il semble plus systématiquement fonctionnel et «réactif»; Je réagis à la combinaison de deux "événements": (1) un nouveau mot est émis, et (2) l'invocation de la méthode PrintMostRecent.

Cependant, j'ai lu que l'utilisation de sujets n'est pas souhaitable lorsqu'elle n'est pas strictement nécessaire et que j'introduis un Subject<Unit> inutile. Essentiellement, ce que je fais ici est de générer une observable à partir d'un appel de méthode afin que je puisse utiliser les fonctions de combinateur observables dans plus d'endroits. J'aime l'idée de construire mon code en utilisant les combinateurs et les abonnements observables plutôt que d'utiliser des arbres d'invocation de méthodes «à l'ancienne». Je ne sais pas si c'est une bonne idée.

public class WordPrinterWithCache
{
    string _lastWord = string.Empty;

    public WordPrinterWithCache(IObservable<string> words)
    {
        words.Subscribe(w => _lastWord = w);
    }

    public void PrintMostRecent() => Console.WriteLine(_lastWord);
}

public class WordPrinterWithExtraSubject
{
    Subject<Unit> _printRequest = new Subject<Unit>();

    public WordPrinterWithExtraSubject(IObservable<string> words)
    {
        _printRequest
            .WithLatestFrom(words.StartWith(string.Empty), (_, w) => w)
            .Subscribe(w => Console.WriteLine(w));
    }

    public void PrintMostRecent() => _printRequest.OnNext(Unit.Default);
}

Le scénario réel dans mon code est que j'ai un ICommand. Lorsque l'utilisateur appelle la commande (peut-être en cliquant sur un bouton), je souhaite agir sur la valeur la plus récente d'une observable spécifique. Par exemple, peut-être que ICommand représente Delete et que je souhaite supprimer l'élément sélectionné dans une liste représentée par un IObservable<Guid>. Cela devient moche de garder un tas de variables de cache "dernière valeur émise" pour chaque observable.

L'approche vers laquelle je me penche est une implémentation ICommand semblable à ce que vous voyez ci-dessous. Cela me permet d'écrire du code comme deleteCommand.WithLatestFrom(selectedItems,(d,s)=>s).Subscribe(selected=>delete(selected));

public class ObservableCommand : ICommand, IObservable<object>
{
    bool _mostRecentCanExecute = true;
    Subject<object> _executeRequested = new Subject<object>();

    public ObservableCommand(IObservable<bool> canExecute)
    {
        canExecute.Subscribe(c => _mostRecentCanExecute = c);
    }

    public event EventHandler CanExecuteChanged; // not implemented yet

    public bool CanExecute(object parameter) => _mostRecentCanExecute;

    public void Execute(object parameter) => _executeRequested.OnNext(parameter);

    public IDisposable Subscribe(IObserver<object> observer) => _executeRequested.Subscribe(observer);
}
1
JustinM 17 janv. 2017 à 00:09

2 réponses

Meilleure réponse

Bien que je ne sois pas sûr d'utiliser le terme «transformer un appel de méthode en un événement observable», je suis fan de l'utilisation d'un code entièrement déclaratif, fonctionnel et piloté par Rx depuis un certain temps maintenant.

Votre description d'une implémentation ICommand correspond très étroitement à celle que j'ai écrite il y a quelques années et que j'ai utilisée à plusieurs reprises et avec beaucoup de succès depuis. En outre, cela est devenu la base d'un modèle que j'appelle «comportements réactifs» qui offre de nombreux avantages. De mon blog:

  • Favorise le développement axé sur le comportement et les tests unitaires.
  • Promeut des pratiques de programmation fonctionnelles et thread-safe.
  • Réduit le risque d'effets secondaires (et si cela est bien fait, peut les éliminer) car des comportements spécifiques sont isolés dans une seule méthode bien nommée.
  • Arrête la "pourriture du code" car tout comportement est encapsulé dans des méthodes spécifiquement nommées. Envie d'un nouveau comportement? Ajoutez une nouvelle méthode. Vous ne voulez plus de comportement spécifique? Je viens de le supprimer. Vous voulez qu'un comportement spécifique change? Changez la méthode unique et sachez que vous n'avez rien cassé d'autre.
  • Fournit des mécanismes concis pour agréger plusieurs entrées et promeut les processus asynchrones au statut de première classe.
  • Réduit le besoin de classes utilitaires car les données peuvent être transmises via le pipeline sous forme de classes anonymes fortement typées.
  • Empêche les fuites de mémoire car tous les comportements renvoient un élément jetable qui, lorsqu'il est supprimé, supprime tous les abonnements et supprime toutes les ressources gérées.

Vous pouvez lire l'article complet sur mon blog.

0
ibebbs 18 janv. 2017 à 09:41

Donc, en règle générale (ligne directrice), je suggère fortement de ne pas avoir un IObservable<T> comme paramètre d'une méthode. La mise en garde évidente est si cette méthode est un nouvel opérateur Rx, par exemple. Select, MySpecialBuffer, Debounce etc.

La théorie ici est que IObservable<T> est un mécanisme de rappel. Il permet à quelque chose de rappeler une autre chose dont il ne sait autrement rien. Cependant, dans ce cas, vous avez quelque chose qui connaît à la fois le IObservable<T> (paramètre) et l'autre chose (WordPrinterWithCache). Alors, pourquoi y a-t-il cette couche supplémentaire d'indirection? Quoi qu'il en soit, ce qui poussait des valeurs vers le IObservable<T> pouvait simplement appeler une méthode de l'instance WordPrinterWithCache.

Dans ce cas, il suffit d'appeler une méthode sur l'autre chose

public class WordPrinterWithCache
{
    private string _lastWord = string.Empty;

    public void SetLastWord(string word)
    {
        _lastWord = word;
    }

    public void PrintMostRecent() => Console.WriteLine(_lastWord);
}

Maintenant, cette classe commence à paraître plutôt inutile, mais cela pourrait être correct. Simple c'est bien.

Utilisez Rx pour vous aider avec la superposition.

Les couches en amont dépendent des couches en aval. Ils appelleront directement des méthodes (émettent des commandes) sur les couches en aval.

Les couches en aval n'ont pas accès aux couches en amont. Ainsi, pour leur exposer des données, ils peuvent soit renvoyer des valeurs à partir de méthodes, soit exposer des rappels. Le motif GoF Observer, les événements .NET et Rx sont des moyens de fournir des rappels aux couches en amont.

2
Lee Campbell 19 janv. 2017 à 01:40