No sé C# en absoluto, así que cualquier cosa que diga sobre C# debe tomarse con un grano de sal. Sin embargo, intentaré explicar qué sucede en esa pieza de código de Ruby.
class << Cache
Ruby tiene algo que se llama métodos simples. No tienen nada que ver con el patrón de diseño de software de Singleton, solo son métodos que están definidos para un solo objeto. Entonces, puede tener dos instancias de la misma clase y agregar métodos a uno de esos dos objetos.
Existen dos sintaxis diferentes para los métodos singleton. Una es simplemente prefijar el nombre del método con el objeto, por lo que def foo.bar(baz)
definiría un método bar
solo para el objeto foo
. El otro método se llama abriendo la clase singleton y se parece sintácticamente similar a definir una clase, porque eso es también lo que ocurre semánticamente: los métodos singleton en realidad viven en una clase invisible que se inserta entre el objeto y su clase real en la clase jerarquía.
Esta sintaxis se ve así: class << foo
. Esto abre la clase de singleton del objeto foo
y cada método definido dentro de ese cuerpo de clase se convierte en un método singleton del objeto foo
.
¿Por qué se usa aquí? Bueno, Ruby es un lenguaje puro orientado a objetos, lo que significa que todo, incluyendo clases es un objeto. Ahora, si los métodos se pueden agregar a objetos individuales, y las clases son objetos, esto significa que los métodos se pueden agregar a clases individuales. En otras palabras, Ruby no tiene necesidad de la distinción artificial entre métodos regulares y métodos estáticos (que son un fraude, de todos modos: no son realmente métodos, solo procedimientos glorificados). Lo que es un método estático en C#, es solo un método regular en la clase de singleton de un objeto de clase.
Todo esto es solo una forma larga de explicar que todo lo definido entre class << Cache
y su correspondiente end
se convierte en static
.
STALE_REFRESH = 1
STALE_CREATED = 2
En Ruby, cada variable que comienza con una letra mayúscula, es en realidad una constante. Sin embargo, en este caso no los traduciremos como campos static const
, sino más bien como enum
, porque así es como se usan.
# Caches data received from a block
#
# The difference between this method and usual Cache.get
# is following: this method caches data and allows user
# to re-generate data when it is expired w/o running
# data generation code more than once so dog-pile effect
# won't bring our servers down
#
def smart_get(key, ttl = nil, generation_time = 30.seconds)
Este método tiene tres parámetros (cuatro en realidad, vamos a ver exactamente qué más adelante), dos de ellos son opcionales (ttl
y generation_time
). Ambos tienen un valor predeterminado, sin embargo, en el caso de ttl
el valor predeterminado no se usa realmente, sirve más como un marcador para saber si el argumento se pasó o no.
30.seconds
es una extensión que la biblioteca ActiveSupport
agrega a la clase Integer
. En realidad, no hace nada, simplemente devuelve self
. Se usa en este caso solo para hacer que la definición del método sea más legible. (Hay otros métodos que hacen algo más útil, por ejemplo, Integer#minutes
, que devuelve self * 60
y Integer#hours
y así sucesivamente.) Utilizaremos esto como una indicación, que el tipo de parámetro no debe ser int
, sino más bien System.TimeSpan
.
# Fallback to default caching approach if no ttl given
return get(key) { yield } unless ttl
Esto contiene varias construcciones complejas de Ruby. Comencemos con el más fácil: modificadores condicionales finales. Si un cuerpo condicional contiene solo una expresión, entonces el condicional se puede agregar al final de la expresión. Entonces, en lugar de decir if a > b then foo end
, también puede decir foo if a > b
. Entonces, lo anterior es equivalente a unless ttl then return get(key) { yield } end
.
El siguiente también es fácil: unless
es solo azúcar sintáctica para if not
. Entonces, ahora estamos en if not ttl then return get(key) { yield } end
El tercer es el sistema de verdad de Ruby. En Ruby, la verdad es bastante simple. En realidad, la falsedad es bastante simple, y la verdad cae naturalmente: la palabra clave especial false
es falsa, y la palabra clave especial nil
es falsa, todo lo demás es verdadero. Por lo tanto, en este caso, el condicional será solo verdadero, si ttl
es o nil
. false
no es un valor razonablemente terrible para un período de tiempo, por lo que el único interesante es nil
. El fragmento se habría escrito más claramente así: if ttl.nil? then return get(key) { yield } end
. Como el valor predeterminado para el parámetro ttl
es nil
, este condicional es verdadero, si no se pasó ningún argumento para ttl
. Por lo tanto, el condicional se usa para determinar con cuántos argumentos se llamó el método, lo que significa que no vamos a traducirlo como una condicional sino como una sobrecarga de método.
Ahora, en el yield
. En Ruby, cada método puede aceptar un bloque de código implícito como argumento. Es por eso que escribí anteriormente que el método realmente toma cuatro argumentos, no tres. Un bloque de código es simplemente una pieza anónima de código que puede pasarse, almacenarse en una variable e invocarse más tarde. Ruby hereda bloques de Smalltalk, pero el concepto se remonta a 1958, a las expresiones lambda de Lisp. Al mencionar bloques de código anónimos, pero al menos ahora, al mencionar expresiones lambda, debe saber cómo representar este cuarto parámetro de método implícito: un tipo de delegado, más específicamente, un Func
.
Entonces, ¿qué es yield
do? Transfiere el control al bloque. Básicamente es solo una forma muy conveniente de invocar un bloque, sin tener que almacenarlo explícitamente en una variable y luego llamarlo.
# Create window for data refresh
real_ttl = ttl + generation_time * 2
stale_key = "#{key}.stale"
Esta sintaxis #{foo}
se llama cadena de interpolación. Significa "reemplazar el token dentro de la cadena con el resultado de evaluar la expresión entre llaves". Es solo una versión muy concisa de String.Format()
, que es exactamente a lo que vamos a traducirlo.
# Try to get data from memcache
value = get(key)
stale = get(stale_key)
# If stale key has expired, it is time to re-generate our data
unless stale
put(stale_key, STALE_REFRESH, generation_time) # lock
value = nil # force data re-generation
end
# If no data retrieved or data re-generation forced, re-generate data and reset stale key
unless value
value = yield
put(key, value, real_ttl)
put(stale_key, STALE_CREATED, ttl) # unlock
end
return value
end
end
Ésta es mi débil intento de traducción de la versión de Ruby a C#:
public class Cache<Tkey, Tvalue> {
enum Stale { Refresh, Created }
/* Caches data received from a delegate
*
* The difference between this method and usual Cache.get
* is following: this method caches data and allows user
* to re-generate data when it is expired w/o running
* data generation code more than once so dog-pile effect
* won't bring our servers down
*/
public static Tvalue SmartGet(Tkey key, TimeSpan ttl, TimeSpan generationTime, Func<Tvalue> strategy)
{
// Create window for data refresh
var realTtl = ttl + generationTime * 2;
var staleKey = String.Format("{0}stale", key);
// Try to get data from memcache
var value = Get(key);
var stale = Get(staleKey);
// If stale key has expired, it is time to re-generate our data
if (stale == null)
{
Put(staleKey, Stale.Refresh, generationTime); // lock
value = null; // force data re-generation
}
// If no data retrieved or data re-generation forced, re-generate data and reset stale key
if (value == null)
{
value = strategy();
Put(key, value, realTtl);
Put(staleKey, Stale.Created, ttl) // unlock
}
return value;
}
// Fallback to default caching approach if no ttl given
public static Tvalue SmartGet(Tkey key, Func<Tvalue> strategy) =>
Get(key, strategy);
// Simulate default argument for generationTime
// C# 4.0 has default arguments, so this wouldn't be needed.
public static Tvalue SmartGet(Tkey key, TimeSpan ttl, Func<Tvalue> strategy) =>
SmartGet(key, ttl, new TimeSpan(0, 0, 30), strategy);
// Convenience overloads to allow calling it the same way as
// in Ruby, by just passing in the timespans as integers in
// seconds.
public static Tvalue SmartGet(Tkey key, int ttl, int generationTime, Func<Tvalue> strategy) =>
SmartGet(key, new TimeSpan(0, 0, ttl), new TimeSpan(0, 0, generationTime), strategy);
public static Tvalue SmartGet(Tkey key, int ttl, Func<Tvalue> strategy) =>
SmartGet(key, new TimeSpan(0, 0, ttl), strategy);
}
Tenga en cuenta que no sé C#, no sé .NET, no he probado esto, Ni siquiera sé si es sintácticamente válido. Espero que ayude de todos modos.
@ Jorg - ¿me ayudarás si publico un código para la conversión de Ruby a C#? – maliks
respuesta perfecta: buena explicación + código – trinalbadger587