À qui incombe-t-il de mettre en cache / mémoriser les résultats de la fonction?

Je travaille sur un logiciel qui permet à l’utilisateur d’étendre un système en implémentant un ensemble d’interfaces.

Afin de tester la viabilité de ce que nous faisons, mon entreprise ” mange sa propre nourriture pour chien ” en mettant en œuvre toute notre logique métier dans ces classes de la même manière qu’un utilisateur.

Nous avons des classes / méthodes d’utilité qui lient tout et utilisent la logique définie dans les classes extensibles.


Je veux mettre en cache les résultats des fonctions définies par l’utilisateur. Où devrais-je faire cela?

  • Est-ce les cours eux-mêmes? Il semble que cela puisse entraîner beaucoup de duplication de code.

  • Est-ce que ce sont les utilitaires / moteurs qui utilisent ces classes? Si tel est le cas, un utilisateur non informé peut appeler directement la fonction de classe sans bénéficier d’avantages de la mise en cache.


Exemple de code

public interface ILetter { ssortingng[] GetAnimalsThatStartWithMe(); } public class A : ILetter { public ssortingng[] GetAnimalsThatStartWithMe() { return new [] { "Aardvark", "Ant" }; } } public class B : ILetter { public ssortingng[] GetAnimalsThatStartWithMe() { return new [] { "Baboon", "Banshee" }; } } /* ...Left to user to define... */ public class Z : ILetter { public ssortingng[] GetAnimalsThatStartWithMe() { return new [] { "Zebra" }; } } public static class LetterUtility { public static ssortingng[] GetAnimalsThatStartWithLetter(char letter) { if(letter == 'A') return (new A()).GetAnimalsThatStartWithMe(); if(letter == 'B') return (new B()).GetAnimalsThatStartWithMe(); /* ... */ if(letter == 'Z') return (new Z()).GetAnimalsThatStartWithMe(); throw new ApplicationException("Letter " + letter + " not found"); } } 

LetterUtility doit- il être responsable de la mise en cache? Chaque instance individuelle d’ ILetter devrait-elle ? Y a-t-il autre chose qui puisse être fait?

J’essaie de garder cet exemple court afin que ces exemples de fonctions n’aient pas besoin de la mise en cache. Mais considérez que j’ajoute cette classe qui fait que (new C()).GetAnimalsThatStartWithMe() prend 10 secondes à chaque exécution:

 public class C : ILetter { public ssortingng[] GetAnimalsThatStartWithMe() { Thread.Sleep(10000); return new [] { "Cat", "Capybara", "Clam" }; } } 

Je me trouve aux sockets entre rendre le logiciel aussi rapide que possible et conserver moins de code (dans cet exemple: mettre le résultat en cache dans LetterUtility ) et effectuer exactement le même travail encore et encore (dans cet exemple: attendre 10 secondes chaque fois que C est utilisé) .

Quelle couche est la mieux responsable de la mise en cache des résultats de ces fonctions pouvant être définies par l’utilisateur?

La réponse est assez évidente: la couche qui peut correctement mettre en œuvre la stratégie de cache souhaitée est la bonne couche.

Une politique de cache correcte doit avoir deux caractéristiques:

  • Il ne doit jamais servir de données périmées; il doit savoir si la méthode mise en cache va produire un résultat différent et invalider le cache à un moment donné avant que l’appelant obtienne des données périmées

  • Il doit gérer efficacement les ressources mises en cache pour le compte de l’utilisateur. Un cache sans politique d’expiration qui grandit sans limite porte un autre nom: nous les appelons généralement “memory leaks”.

Quelle est la couche de votre système qui connaît les réponses aux questions “le cache est-il périmé?” et “la cache est-elle trop grande?” C’est la couche qui doit implémenter le cache.

Quelque chose comme la mise en cache peut être considéré comme une préoccupation “transversale” (http://en.wikipedia.org/wiki/Cross-cutting_concern):

En informatique, les préoccupations transversales sont des aspects d’un programme qui affectent d’autres préoccupations. Ces préoccupations ne peuvent souvent pas être clairement décomposées du rest du système, à la fois dans la conception et dans la mise en œuvre, et peuvent entraîner une dispersion (duplication de code), un enchevêtrement (dépendances significatives entre les systèmes), ou les deux. Par exemple, si vous écrivez une application pour la gestion de dossiers médicaux, la tenue de livres et l’indexation de ceux-ci sont une préoccupation essentielle, tandis que consigner un historique des modifications apscopes à la firebase database des enregistrements ou à la firebase database d’utilisateurs, ou à un système d’authentification, constituerait une préoccupation transversale, ils touchent plus de parties du programme.

Les préoccupations transversales peuvent souvent être mises en œuvre via Aspect Oriented Programming (http://en.wikipedia.org/wiki/Aspect-oriented_programming).

En informatique, la programmation orientée aspect (AOP) est un paradigme de programmation qui vise à accroître la modularité en permettant la séparation des problèmes transversaux. L’AOP constitue une base pour le développement logiciel orienté aspect.

Il existe de nombreux outils dans .NET pour faciliter la programmation orientée aspect. J’adore ceux qui offrent une mise en œuvre totalement transparente. Dans l’exemple de la mise en cache:

 public class Foo { [Cache(10)] // cache for 10 minutes public virtual void Bar() { ... } } 

C’est tout ce que vous devez faire … tout se passe automatiquement en définissant un comportement comme suit:

 public class CachingBehavior { public void Intercept(IInvocation invocation) { ... } // this method intercepts any method invocations on methods atsortingbuted with the [Cache] atsortingbute. // In the case of caching, this method would check if some cache store contains the data, and if it does return it...else perform the normal method operation and store the result } 

Il existe deux écoles générales pour expliquer cela:

  1. Post-build IL tissage. Des outils tels que PostSharp, Microsoft CCI et Mono Cecil peuvent être configurés pour réécrire automatiquement ces méthodes atsortingbuées pour déléguer automatiquement vos comportements.

  2. Proxy d’exécution. Des outils tels que Castle DynamicProxy et Microsoft Unity peuvent générer automatiquement des types de proxy (un type dérivé de Foo qui remplace Bar dans l’exemple ci-dessus) et qui délèguent votre comportement.

Bien que je ne connaisse pas C #, cela semble être un cas d’utilisation de la programmation orientée aspect (AOP). L’idée est que vous pouvez «injecter» du code à exécuter à certains points de la stack d’exécution.

Vous pouvez append le code de mise en cache comme suit:

 IF( InCache( object, method, method_arguments ) ) RETURN Cache(object, method, method_arguments); ELSE ExecuteMethod(); StoreResultsInCache(); 

Vous définissez ensuite que ce code doit être exécuté avant chaque appel de vos fonctions d’interface (et de toutes les sous-classes implémentant également ces fonctions).

Un expert .NET peut-il nous éclairer sur la manière dont vous procéderiez dans .NET?

En général, la mise en cache et la mémorisation ont du sens lorsque:

  1. L’obtention du résultat est (ou peut au moins être) une latence élevée ou autrement coûteuse que les dépenses engendrées par la mise en cache elle-même.
  2. Les résultats ont un modèle de recherche dans lequel il y aura des appels fréquents avec les mêmes entrées pour la fonction (c’est-à-dire non seulement les arguments mais toutes les instances, les données statiques et autres qui affectent le résultat).
  3. Il n’y a pas de mécanisme de mise en cache déjà existant dans le code auquel le code en question appelle, ce qui rend cela inutile.
  4. Il n’y aura pas d’autre mécanisme de mise en cache dans le code qui appelle le code en question, ce qui rend cela inutile (pourquoi mémoriser GetHashCode() dans cette méthode n’a presque jamais de sens, même si les gens sont souvent tentés de le faire lorsque l’implémentation est relativement coûteuse) .
  5. Il est impossible de devenir obsolète, peu susceptible de devenir obsolète lorsque le cache est chargé, sans importance s’il devient obsolète, ou lorsque l’obscurité est facile à détecter.

Il existe des cas où chaque cas d’utilisation d’un composant correspond à tous ceux-ci. Il y en a beaucoup plus là où ils ne voudront pas. Par exemple, si un composant met en cache les résultats mais n’est jamais appelé deux fois avec les mêmes entrées par un composant client particulier, cette mise en cache est simplement un gaspillage qui a eu un impact négatif sur les performances (peut-être négligeable, voire sévère).

Le plus souvent, il est beaucoup plus logique que le code client décide de la stratégie de mise en cache qui lui convient. Il sera également souvent plus facile de modifier un usage particulier pour un usage particulier à ce stade face à des données réelles que dans le composant (car les données réelles auxquelles il sera confronté peuvent varier considérablement d’une utilisation à l’autre).

Il est encore plus difficile de savoir quel degré de légèreté pourrait être acceptable. En règle générale, un composant doit supposer qu’il doit disposer de 100% de fraîcheur, tandis que le composant client peut savoir qu’un certain degré de légèreté suffira.

D’autre part, il peut être plus facile pour un composant d’obtenir des informations utiles pour le cache. Les composants peuvent travailler main dans la main dans ces cas, bien qu’il soit beaucoup plus complexe (par exemple, le mécanisme If-Modified-Since utilisé par les services Web RESTful, dans lequel un serveur peut indiquer qu’un client peut utiliser en toute sécurité les informations qu’il a mises en cache. ).

En outre, un composant peut avoir une stratégie de mise en cache configurable. Le regroupement de connexions est une politique de mise en cache, pensez à la manière dont cela est configurable.

Donc en résumé:

Le composant qui peut déterminer quelle mise en cache est à la fois possible et utile.

Quel est le plus souvent le code client. Bien qu’avoir les détails de la latence et de la paresse probables documentés par les auteurs du composant aidera ici.

Peut-être moins souvent le code client avec l’aide du composant, bien que vous deviez exposer les détails de la mise en cache pour permettre cela.

Et peut parfois être le composant avec la politique de mise en cache configurable par le code d’appel.

Le composant ne peut que rarement, car il est plus rare que tous les cas d’utilisation soient bien servis par la même politique de mise en cache. Une exception importante concerne les cas où la même instance de ce composant servira plusieurs clients, car les facteurs qui affectent ce qui précède sont répartis sur ces clients.

Tous les articles précédents ont soulevé quelques points positifs. Voici un aperçu très approximatif de la manière dont vous pourriez le faire. J’ai écrit ceci à la volée, donc il pourrait être nécessaire de le peaufiner:

 interface IMemoizer { bool IsValid(T args); //Is the cache valid, or stale, etc. bool TryLookup(T args, out R result); void StoreResult(T args, R result); } static IMemoizerExtensions { Func Memoizing(this IMemoizer src, Func method) { return new Func(args => { R result; if (src.TryLookup(args, result) && src.IsValid(args)) { return result; } else { result = method.Invoke(args); memoizer.StoreResult(args, result); return result; } }); } }