Write stateless C#.md
· 9.8 KiB · Markdown
Raw
# Demo 1 - Wrap stateful objects in services
## Bad example
```csharp
public class MyWorkflow
{
private IConfigDatabaseAccess _configDatabaseAccess;
private IMonitorDatabaseAccess _monitorDatabaseAccess;
private virtual void InitConfigDatabase()
{
_configDatabaseAccess = new ConfigDatabaseAccess();
_configDatabaseAccess.Init();
}
private virtual void InitMonitorDatabase()
{
_monitorDatabaseAccess = new MonitorDatabaseAccess();
_monitorDatabaseAccess.Init();
}
public void DoWork()
{
InitConfigDatabase();
InitMonitorDatabase();
DoCopyWork();
}
public void DoCopyWork()
{
var data = _configDatabaseAccess.GetData();
_monitorDatabaseAccess.CopyData(data);
}
}
```
## Good example
```csharp
public class ConfigDatabaseService
{
private IConfigDatabaseAccess _configDatabaseAccess;
public IConfigDatabaseAccess ConfigDatabaseAccess
{
get
{
lock (this)
{
if (_configDatabaseAccess == null)
{
_configDatabaseAccess = new ConfigDatabaseAccess();
_configDatabaseAccess.Init();
}
return _configDatabaseAccess;
}
}
}
}
public class MonitorDatabaseService
{
private IMonitorDatabaseAccess _monitorDatabaseAccess;
public IMonitorDatabaseAccess MonitorDatabaseAccess
{
get
{
lock (this)
{
if (_monitorDatabaseAccess == null)
{
_monitorDatabaseAccess = new MonitorDatabaseAccess();
_monitorDatabaseAccess.Init();
}
return _monitorDatabaseAccess;
}
}
}
}
public class MyWorkflow
{
private readonly ConfigDatabaseService _configDatabaseService;
private readonly MonitorDatabaseService _monitorDatabaseService;
public MyWorkflow(
ConfigDatabaseService configDatabaseService,
MonitorDatabaseService monitorDatabaseService)
{
_configDatabaseService = configDatabaseService;
_monitorDatabaseService = monitorDatabaseService;
}
public void IWorkflowStarted()
{
_configDatabaseService.ConfigDatabaseAccess.GetData();
_monitorDatabaseService.MonitorDatabaseAccess.CopyData();
}
}
第二种写法,将屎山 ConfigDatabaseAccess 隔离到了单独的服务中,而这个单独的服务是无状态的,从而使得使用者无需在意 ConfigDatabaseAccess 的细节,直接注入进来就可以使用。无需担心忘记调用里面有状态的:
```csharp
_configDatabaseAccess = new ConfigDatabaseAccess();
_configDatabaseAccess.Init();
```
这两个方法,从而使得整体变得更加无副作用。
# Demo 2 - No side effects in cache management
## Bad example
```csharp
public class ProductService
{
private Dictionary<int, Product> _cache = new Dictionary<int, Product>();
public Product GetProduct(int id)
{
if (_cache.ContainsKey(id))
return _cache[id];
var product = Database.GetProduct(id);
_cache[id] = product;
return product;
}
}
```
## Good example
```csharp
/// <summary>
/// Provides a service for caching data in memory.
/// </summary>
public class CacheService : ITransientDependency
{
private readonly IMemoryCache _cache;
private readonly ILogger<CacheService> _logger;
/// <summary>
/// Initializes a new instance of the CacheService class.
/// </summary>
/// <param name="cache">An instance of IMemoryCache used to store cached data.</param>
/// <param name="logger">An instance of ILogger used to log cache-related events.</param>
public CacheService(
IMemoryCache cache,
ILogger<CacheService> logger)
{
_cache = cache;
_logger = logger;
}
/// <summary>
/// Retrieves data from the cache if available; otherwise, retrieves data using a fallback function and caches the result.
/// </summary>
/// <typeparam name="T">The type of the cached data.</typeparam>
/// <param name="cacheKey">The key used to identify the cached data.</param>
/// <param name="fallback">A function used to retrieve the data if it is not available in the cache.</param>
/// <param name="cacheCondition">An optional predicate used to determine if the cached data is still valid.</param>
/// <param name="cachedMinutes">The number of minutes to cache the data for.</param>
/// <returns>The cached data, or the result of the fallback function if the data is not available in the cache.</returns>
public async Task<T> RunWithCache<T>(
string cacheKey,
Func<Task<T>> fallback,
Predicate<T>? cacheCondition = null,
Func<T, TimeSpan>? cachedMinutes = null)
{
cacheCondition ??= _ => true;
cachedMinutes ??= _ => TimeSpan.FromMinutes(20);
if (!_cache.TryGetValue(cacheKey, out T? resultValue) ||
resultValue == null ||
cacheCondition(resultValue) == false ||
cachedMinutes(resultValue) <= TimeSpan.Zero)
{
resultValue = await fallback();
var minutesShouldCache = cachedMinutes(resultValue);
if (minutesShouldCache > TimeSpan.Zero && cacheCondition(resultValue))
{
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(minutesShouldCache);
_cache.Set(cacheKey, resultValue, cacheEntryOptions);
_logger.LogTrace("Cache set for {CachedMinutes} minutes with cached key: {CacheKey}",
minutesShouldCache, cacheKey);
}
}
else
{
_logger.LogTrace("Cache was hit with cached key: {CacheKey}", cacheKey);
}
return resultValue;
}
/// <summary>
/// Retrieves data from the cache if available; otherwise, retrieves data using a fallback function, applies a selector function to the result, and caches the selected result.
/// </summary>
/// <typeparam name="T1">The type of the data retrieved using the fallback function.</typeparam>
/// <typeparam name="T2">The type of the cached data.</typeparam>
/// <param name="cacheKey">The key used to identify the cached data.</param>
/// <param name="fallback">A function used to retrieve the data if it is not available in the cache.</param>
/// <param name="selector">A function used to select the data to cache from the result of the fallback function.</param>
/// <param name="cacheCondition">An optional predicate used to determine if the cached data is still valid.</param>
/// <param name="cachedMinutes">The number of minutes to cache the data for.</param>
/// <returns>The selected cached data, or the result of the fallback function if the data is not available in the cache.</returns>
public async Task<T2?> QueryCacheWithSelector<T1, T2>(
string cacheKey,
Func<Task<T1>> fallback,
Func<T1, T2> selector,
Predicate<T1>? cacheCondition = null,
Func<T1, TimeSpan>? cachedMinutes = null)
{
cacheCondition ??= (_) => true;
cachedMinutes ??= _ => TimeSpan.FromMinutes(20);
if (!_cache.TryGetValue(cacheKey, out T1? resultValue) ||
resultValue == null ||
cacheCondition(resultValue) == false ||
cachedMinutes(resultValue) <= TimeSpan.Zero)
{
resultValue = await fallback();
if (resultValue == null)
{
return default;
}
var minutesShouldCache = cachedMinutes(resultValue);
if (minutesShouldCache > TimeSpan.Zero && cacheCondition(resultValue))
{
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(minutesShouldCache);
_cache.Set(cacheKey, resultValue, cacheEntryOptions);
_logger.LogTrace("Cache set for {CachedMinutes} minutes with cached key: {CacheKey}",
minutesShouldCache, cacheKey);
}
}
else
{
_logger.LogTrace("Cache was hit with cached key: {CacheKey}", cacheKey);
}
return selector(resultValue);
}
/// <summary>
/// Removes the cached data associated with the specified key.
/// </summary>
/// <param name="key">The key used to identify the cached data to remove.</param>
public void Clear(string key)
{
_cache.Remove(key);
}
}
public class ProductService
{
private readonly CacheService _cacheService;
private readonly IProductRepository _productRepository;
public ProductService(
CacheService cacheService,
IProductRepository productRepository)
{
_cacheService = cacheService;
_productRepository = productRepository;
}
public async Task<Product> GetProduct(int id)
{
return await _cacheService.RunWithCache(
$"Product_{id}",
async () => await _productRepository.GetProduct(id),
cachedMinutes: _ => TimeSpan.FromMinutes(5));
}
}
```
# Demo 3 - Dependency reversal
## Bad example
```csharp
public class MyService
{
private readonly Log _log = new Log();
public void DoWork()
{
_log.Write("Starting work");
}
}
```
## Good example
```csharp
public class MyService
{
private readonly ILogger _logger;
public MyService(ILogger logger)
{
_logger = logger;
}
public void DoWork()
{
_logger.Write("Starting work");
}
}
public interface ILogger
{
void Write(string message);
}
public class Log : ILogger
{
public void Write(string message)
{
Console.WriteLine(message);
}
}
```
Demo 1 - Wrap stateful objects in services
Bad example
public class MyWorkflow
{
private IConfigDatabaseAccess _configDatabaseAccess;
private IMonitorDatabaseAccess _monitorDatabaseAccess;
private virtual void InitConfigDatabase()
{
_configDatabaseAccess = new ConfigDatabaseAccess();
_configDatabaseAccess.Init();
}
private virtual void InitMonitorDatabase()
{
_monitorDatabaseAccess = new MonitorDatabaseAccess();
_monitorDatabaseAccess.Init();
}
public void DoWork()
{
InitConfigDatabase();
InitMonitorDatabase();
DoCopyWork();
}
public void DoCopyWork()
{
var data = _configDatabaseAccess.GetData();
_monitorDatabaseAccess.CopyData(data);
}
}
Good example
public class ConfigDatabaseService
{
private IConfigDatabaseAccess _configDatabaseAccess;
public IConfigDatabaseAccess ConfigDatabaseAccess
{
get
{
lock (this)
{
if (_configDatabaseAccess == null)
{
_configDatabaseAccess = new ConfigDatabaseAccess();
_configDatabaseAccess.Init();
}
return _configDatabaseAccess;
}
}
}
}
public class MonitorDatabaseService
{
private IMonitorDatabaseAccess _monitorDatabaseAccess;
public IMonitorDatabaseAccess MonitorDatabaseAccess
{
get
{
lock (this)
{
if (_monitorDatabaseAccess == null)
{
_monitorDatabaseAccess = new MonitorDatabaseAccess();
_monitorDatabaseAccess.Init();
}
return _monitorDatabaseAccess;
}
}
}
}
public class MyWorkflow
{
private readonly ConfigDatabaseService _configDatabaseService;
private readonly MonitorDatabaseService _monitorDatabaseService;
public MyWorkflow(
ConfigDatabaseService configDatabaseService,
MonitorDatabaseService monitorDatabaseService)
{
_configDatabaseService = configDatabaseService;
_monitorDatabaseService = monitorDatabaseService;
}
public void IWorkflowStarted()
{
_configDatabaseService.ConfigDatabaseAccess.GetData();
_monitorDatabaseService.MonitorDatabaseAccess.CopyData();
}
}
第二种写法,将屎山 ConfigDatabaseAccess 隔离到了单独的服务中,而这个单独的服务是无状态的,从而使得使用者无需在意 ConfigDatabaseAccess 的细节,直接注入进来就可以使用。无需担心忘记调用里面有状态的:
```csharp
_configDatabaseAccess = new ConfigDatabaseAccess();
_configDatabaseAccess.Init();
这两个方法,从而使得整体变得更加无副作用。
Demo 2 - No side effects in cache management
Bad example
public class ProductService
{
private Dictionary<int, Product> _cache = new Dictionary<int, Product>();
public Product GetProduct(int id)
{
if (_cache.ContainsKey(id))
return _cache[id];
var product = Database.GetProduct(id);
_cache[id] = product;
return product;
}
}
Good example
/// <summary>
/// Provides a service for caching data in memory.
/// </summary>
public class CacheService : ITransientDependency
{
private readonly IMemoryCache _cache;
private readonly ILogger<CacheService> _logger;
/// <summary>
/// Initializes a new instance of the CacheService class.
/// </summary>
/// <param name="cache">An instance of IMemoryCache used to store cached data.</param>
/// <param name="logger">An instance of ILogger used to log cache-related events.</param>
public CacheService(
IMemoryCache cache,
ILogger<CacheService> logger)
{
_cache = cache;
_logger = logger;
}
/// <summary>
/// Retrieves data from the cache if available; otherwise, retrieves data using a fallback function and caches the result.
/// </summary>
/// <typeparam name="T">The type of the cached data.</typeparam>
/// <param name="cacheKey">The key used to identify the cached data.</param>
/// <param name="fallback">A function used to retrieve the data if it is not available in the cache.</param>
/// <param name="cacheCondition">An optional predicate used to determine if the cached data is still valid.</param>
/// <param name="cachedMinutes">The number of minutes to cache the data for.</param>
/// <returns>The cached data, or the result of the fallback function if the data is not available in the cache.</returns>
public async Task<T> RunWithCache<T>(
string cacheKey,
Func<Task<T>> fallback,
Predicate<T>? cacheCondition = null,
Func<T, TimeSpan>? cachedMinutes = null)
{
cacheCondition ??= _ => true;
cachedMinutes ??= _ => TimeSpan.FromMinutes(20);
if (!_cache.TryGetValue(cacheKey, out T? resultValue) ||
resultValue == null ||
cacheCondition(resultValue) == false ||
cachedMinutes(resultValue) <= TimeSpan.Zero)
{
resultValue = await fallback();
var minutesShouldCache = cachedMinutes(resultValue);
if (minutesShouldCache > TimeSpan.Zero && cacheCondition(resultValue))
{
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(minutesShouldCache);
_cache.Set(cacheKey, resultValue, cacheEntryOptions);
_logger.LogTrace("Cache set for {CachedMinutes} minutes with cached key: {CacheKey}",
minutesShouldCache, cacheKey);
}
}
else
{
_logger.LogTrace("Cache was hit with cached key: {CacheKey}", cacheKey);
}
return resultValue;
}
/// <summary>
/// Retrieves data from the cache if available; otherwise, retrieves data using a fallback function, applies a selector function to the result, and caches the selected result.
/// </summary>
/// <typeparam name="T1">The type of the data retrieved using the fallback function.</typeparam>
/// <typeparam name="T2">The type of the cached data.</typeparam>
/// <param name="cacheKey">The key used to identify the cached data.</param>
/// <param name="fallback">A function used to retrieve the data if it is not available in the cache.</param>
/// <param name="selector">A function used to select the data to cache from the result of the fallback function.</param>
/// <param name="cacheCondition">An optional predicate used to determine if the cached data is still valid.</param>
/// <param name="cachedMinutes">The number of minutes to cache the data for.</param>
/// <returns>The selected cached data, or the result of the fallback function if the data is not available in the cache.</returns>
public async Task<T2?> QueryCacheWithSelector<T1, T2>(
string cacheKey,
Func<Task<T1>> fallback,
Func<T1, T2> selector,
Predicate<T1>? cacheCondition = null,
Func<T1, TimeSpan>? cachedMinutes = null)
{
cacheCondition ??= (_) => true;
cachedMinutes ??= _ => TimeSpan.FromMinutes(20);
if (!_cache.TryGetValue(cacheKey, out T1? resultValue) ||
resultValue == null ||
cacheCondition(resultValue) == false ||
cachedMinutes(resultValue) <= TimeSpan.Zero)
{
resultValue = await fallback();
if (resultValue == null)
{
return default;
}
var minutesShouldCache = cachedMinutes(resultValue);
if (minutesShouldCache > TimeSpan.Zero && cacheCondition(resultValue))
{
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(minutesShouldCache);
_cache.Set(cacheKey, resultValue, cacheEntryOptions);
_logger.LogTrace("Cache set for {CachedMinutes} minutes with cached key: {CacheKey}",
minutesShouldCache, cacheKey);
}
}
else
{
_logger.LogTrace("Cache was hit with cached key: {CacheKey}", cacheKey);
}
return selector(resultValue);
}
/// <summary>
/// Removes the cached data associated with the specified key.
/// </summary>
/// <param name="key">The key used to identify the cached data to remove.</param>
public void Clear(string key)
{
_cache.Remove(key);
}
}
public class ProductService
{
private readonly CacheService _cacheService;
private readonly IProductRepository _productRepository;
public ProductService(
CacheService cacheService,
IProductRepository productRepository)
{
_cacheService = cacheService;
_productRepository = productRepository;
}
public async Task<Product> GetProduct(int id)
{
return await _cacheService.RunWithCache(
$"Product_{id}",
async () => await _productRepository.GetProduct(id),
cachedMinutes: _ => TimeSpan.FromMinutes(5));
}
}
Demo 3 - Dependency reversal
Bad example
public class MyService
{
private readonly Log _log = new Log();
public void DoWork()
{
_log.Write("Starting work");
}
}
Good example
public class MyService
{
private readonly ILogger _logger;
public MyService(ILogger logger)
{
_logger = logger;
}
public void DoWork()
{
_logger.Write("Starting work");
}
}
public interface ILogger
{
void Write(string message);
}
public class Log : ILogger
{
public void Write(string message)
{
Console.WriteLine(message);
}
}