Остання активність 1743215572

anduin's Avatar anduin ревизій цього gist 1743215572. До ревизії

1 file changed, 0 insertions, 0 deletions

Write stateless C# перейменовано в Write stateless C#.md

Файл перейменовано без змін

anduin's Avatar anduin ревизій цього gist 1743215566. До ревизії

1 file changed, 332 insertions

Write stateless C# (файл створено)

@@ -0,0 +1,332 @@
1 + # Demo 1 - Wrap stateful objects in services
2 +
3 + ## Bad example
4 +
5 + ```csharp
6 + public class MyWorkflow
7 + {
8 + private IConfigDatabaseAccess _configDatabaseAccess;
9 + private IMonitorDatabaseAccess _monitorDatabaseAccess;
10 +
11 + private virtual void InitConfigDatabase()
12 + {
13 + _configDatabaseAccess = new ConfigDatabaseAccess();
14 + _configDatabaseAccess.Init();
15 + }
16 +
17 + private virtual void InitMonitorDatabase()
18 + {
19 + _monitorDatabaseAccess = new MonitorDatabaseAccess();
20 + _monitorDatabaseAccess.Init();
21 + }
22 +
23 + public void DoWork()
24 + {
25 + InitConfigDatabase();
26 + InitMonitorDatabase();
27 + DoCopyWork();
28 + }
29 +
30 + public void DoCopyWork()
31 + {
32 + var data = _configDatabaseAccess.GetData();
33 + _monitorDatabaseAccess.CopyData(data);
34 + }
35 + }
36 + ```
37 +
38 + ## Good example
39 +
40 + ```csharp
41 + public class ConfigDatabaseService
42 + {
43 + private IConfigDatabaseAccess _configDatabaseAccess;
44 +
45 + public IConfigDatabaseAccess ConfigDatabaseAccess
46 + {
47 + get
48 + {
49 + lock (this)
50 + {
51 + if (_configDatabaseAccess == null)
52 + {
53 + _configDatabaseAccess = new ConfigDatabaseAccess();
54 + _configDatabaseAccess.Init();
55 + }
56 +
57 + return _configDatabaseAccess;
58 + }
59 + }
60 + }
61 + }
62 +
63 + public class MonitorDatabaseService
64 + {
65 + private IMonitorDatabaseAccess _monitorDatabaseAccess;
66 +
67 + public IMonitorDatabaseAccess MonitorDatabaseAccess
68 + {
69 + get
70 + {
71 + lock (this)
72 + {
73 + if (_monitorDatabaseAccess == null)
74 + {
75 + _monitorDatabaseAccess = new MonitorDatabaseAccess();
76 + _monitorDatabaseAccess.Init();
77 + }
78 +
79 + return _monitorDatabaseAccess;
80 + }
81 + }
82 + }
83 + }
84 +
85 + public class MyWorkflow
86 + {
87 + private readonly ConfigDatabaseService _configDatabaseService;
88 + private readonly MonitorDatabaseService _monitorDatabaseService;
89 +
90 + public MyWorkflow(
91 + ConfigDatabaseService configDatabaseService,
92 + MonitorDatabaseService monitorDatabaseService)
93 + {
94 + _configDatabaseService = configDatabaseService;
95 + _monitorDatabaseService = monitorDatabaseService;
96 + }
97 +
98 + public void IWorkflowStarted()
99 + {
100 + _configDatabaseService.ConfigDatabaseAccess.GetData();
101 + _monitorDatabaseService.MonitorDatabaseAccess.CopyData();
102 + }
103 + }
104 +
105 + 第二种写法,将屎山 ConfigDatabaseAccess 隔离到了单独的服务中,而这个单独的服务是无状态的,从而使得使用者无需在意 ConfigDatabaseAccess 的细节,直接注入进来就可以使用。无需担心忘记调用里面有状态的:
106 +
107 + ```csharp
108 + _configDatabaseAccess = new ConfigDatabaseAccess();
109 + _configDatabaseAccess.Init();
110 + ```
111 +
112 + 这两个方法,从而使得整体变得更加无副作用。
113 +
114 + # Demo 2 - No side effects in cache management
115 +
116 + ## Bad example
117 +
118 + ```csharp
119 + public class ProductService
120 + {
121 + private Dictionary<int, Product> _cache = new Dictionary<int, Product>();
122 +
123 + public Product GetProduct(int id)
124 + {
125 + if (_cache.ContainsKey(id))
126 + return _cache[id];
127 +
128 + var product = Database.GetProduct(id);
129 + _cache[id] = product;
130 + return product;
131 + }
132 + }
133 + ```
134 +
135 + ## Good example
136 +
137 + ```csharp
138 + /// <summary>
139 + /// Provides a service for caching data in memory.
140 + /// </summary>
141 + public class CacheService : ITransientDependency
142 + {
143 + private readonly IMemoryCache _cache;
144 + private readonly ILogger<CacheService> _logger;
145 +
146 + /// <summary>
147 + /// Initializes a new instance of the CacheService class.
148 + /// </summary>
149 + /// <param name="cache">An instance of IMemoryCache used to store cached data.</param>
150 + /// <param name="logger">An instance of ILogger used to log cache-related events.</param>
151 + public CacheService(
152 + IMemoryCache cache,
153 + ILogger<CacheService> logger)
154 + {
155 + _cache = cache;
156 + _logger = logger;
157 + }
158 +
159 + /// <summary>
160 + /// Retrieves data from the cache if available; otherwise, retrieves data using a fallback function and caches the result.
161 + /// </summary>
162 + /// <typeparam name="T">The type of the cached data.</typeparam>
163 + /// <param name="cacheKey">The key used to identify the cached data.</param>
164 + /// <param name="fallback">A function used to retrieve the data if it is not available in the cache.</param>
165 + /// <param name="cacheCondition">An optional predicate used to determine if the cached data is still valid.</param>
166 + /// <param name="cachedMinutes">The number of minutes to cache the data for.</param>
167 + /// <returns>The cached data, or the result of the fallback function if the data is not available in the cache.</returns>
168 + public async Task<T> RunWithCache<T>(
169 + string cacheKey,
170 + Func<Task<T>> fallback,
171 + Predicate<T>? cacheCondition = null,
172 + Func<T, TimeSpan>? cachedMinutes = null)
173 + {
174 + cacheCondition ??= _ => true;
175 + cachedMinutes ??= _ => TimeSpan.FromMinutes(20);
176 +
177 + if (!_cache.TryGetValue(cacheKey, out T? resultValue) ||
178 + resultValue == null ||
179 + cacheCondition(resultValue) == false ||
180 + cachedMinutes(resultValue) <= TimeSpan.Zero)
181 + {
182 + resultValue = await fallback();
183 + var minutesShouldCache = cachedMinutes(resultValue);
184 + if (minutesShouldCache > TimeSpan.Zero && cacheCondition(resultValue))
185 + {
186 + var cacheEntryOptions = new MemoryCacheEntryOptions()
187 + .SetAbsoluteExpiration(minutesShouldCache);
188 +
189 + _cache.Set(cacheKey, resultValue, cacheEntryOptions);
190 + _logger.LogTrace("Cache set for {CachedMinutes} minutes with cached key: {CacheKey}",
191 + minutesShouldCache, cacheKey);
192 + }
193 + }
194 + else
195 + {
196 + _logger.LogTrace("Cache was hit with cached key: {CacheKey}", cacheKey);
197 + }
198 +
199 + return resultValue;
200 + }
201 +
202 + /// <summary>
203 + /// 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.
204 + /// </summary>
205 + /// <typeparam name="T1">The type of the data retrieved using the fallback function.</typeparam>
206 + /// <typeparam name="T2">The type of the cached data.</typeparam>
207 + /// <param name="cacheKey">The key used to identify the cached data.</param>
208 + /// <param name="fallback">A function used to retrieve the data if it is not available in the cache.</param>
209 + /// <param name="selector">A function used to select the data to cache from the result of the fallback function.</param>
210 + /// <param name="cacheCondition">An optional predicate used to determine if the cached data is still valid.</param>
211 + /// <param name="cachedMinutes">The number of minutes to cache the data for.</param>
212 + /// <returns>The selected cached data, or the result of the fallback function if the data is not available in the cache.</returns>
213 + public async Task<T2?> QueryCacheWithSelector<T1, T2>(
214 + string cacheKey,
215 + Func<Task<T1>> fallback,
216 + Func<T1, T2> selector,
217 + Predicate<T1>? cacheCondition = null,
218 + Func<T1, TimeSpan>? cachedMinutes = null)
219 + {
220 + cacheCondition ??= (_) => true;
221 + cachedMinutes ??= _ => TimeSpan.FromMinutes(20);
222 +
223 + if (!_cache.TryGetValue(cacheKey, out T1? resultValue) ||
224 + resultValue == null ||
225 + cacheCondition(resultValue) == false ||
226 + cachedMinutes(resultValue) <= TimeSpan.Zero)
227 + {
228 + resultValue = await fallback();
229 + if (resultValue == null)
230 + {
231 + return default;
232 + }
233 +
234 + var minutesShouldCache = cachedMinutes(resultValue);
235 + if (minutesShouldCache > TimeSpan.Zero && cacheCondition(resultValue))
236 + {
237 + var cacheEntryOptions = new MemoryCacheEntryOptions()
238 + .SetSlidingExpiration(minutesShouldCache);
239 +
240 + _cache.Set(cacheKey, resultValue, cacheEntryOptions);
241 + _logger.LogTrace("Cache set for {CachedMinutes} minutes with cached key: {CacheKey}",
242 + minutesShouldCache, cacheKey);
243 + }
244 + }
245 + else
246 + {
247 + _logger.LogTrace("Cache was hit with cached key: {CacheKey}", cacheKey);
248 + }
249 +
250 + return selector(resultValue);
251 + }
252 +
253 + /// <summary>
254 + /// Removes the cached data associated with the specified key.
255 + /// </summary>
256 + /// <param name="key">The key used to identify the cached data to remove.</param>
257 + public void Clear(string key)
258 + {
259 + _cache.Remove(key);
260 + }
261 + }
262 +
263 + public class ProductService
264 + {
265 + private readonly CacheService _cacheService;
266 + private readonly IProductRepository _productRepository;
267 +
268 + public ProductService(
269 + CacheService cacheService,
270 + IProductRepository productRepository)
271 + {
272 + _cacheService = cacheService;
273 + _productRepository = productRepository;
274 + }
275 +
276 + public async Task<Product> GetProduct(int id)
277 + {
278 + return await _cacheService.RunWithCache(
279 + $"Product_{id}",
280 + async () => await _productRepository.GetProduct(id),
281 + cachedMinutes: _ => TimeSpan.FromMinutes(5));
282 + }
283 + }
284 + ```
285 +
286 + # Demo 3 - Dependency reversal
287 +
288 + ## Bad example
289 +
290 + ```csharp
291 + public class MyService
292 + {
293 + private readonly Log _log = new Log();
294 +
295 + public void DoWork()
296 + {
297 + _log.Write("Starting work");
298 + }
299 + }
300 + ```
301 +
302 + ## Good example
303 +
304 + ```csharp
305 + public class MyService
306 + {
307 + private readonly ILogger _logger;
308 +
309 + public MyService(ILogger logger)
310 + {
311 + _logger = logger;
312 + }
313 +
314 + public void DoWork()
315 + {
316 + _logger.Write("Starting work");
317 + }
318 + }
319 +
320 + public interface ILogger
321 + {
322 + void Write(string message);
323 + }
324 +
325 + public class Log : ILogger
326 + {
327 + public void Write(string message)
328 + {
329 + Console.WriteLine(message);
330 + }
331 + }
332 + ```
Новіше Пізніше