Last active
April 21, 2022 12:53
-
-
Save runceel/1a3179250cf5306d8ea6306afcfdeb07 to your computer and use it in GitHub Desktop.
Redux パターンのなるべくタイプセーフな雰囲気実装
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ライブラリ使う側のコード | |
using ReduxDemo; | |
using Microsoft.Extensions.DependencyInjection; | |
using Reactive.Bindings; | |
var services = new ServiceCollection(); | |
// アプリの初期ステータスの設定と必須サービスの登録 | |
services.AddRedux<AppStore>(new AppStore()); | |
// Action と Reducer の登録 | |
services.AddActions<Actions>(); | |
services.AddReducers<Reducers>(); | |
var p = services.BuildServiceProvider(); | |
// アプリケーションのストアをとってきて、とりあえず変更があったら標準出力に出す | |
var store = p.GetRequiredService<IReactiveProperty<AppStore>>(); | |
store.Subscribe(x => Console.WriteLine(x)); | |
// Dispatcher を取得して適当にディスパッチ | |
var dispatcher = p.GetRequiredService<IDispatcher>(); | |
// これはただのアクション | |
dispatcher.Dispatch(new IncrementAction()); | |
dispatcher.Dispatch(new IncrementAction()); | |
dispatcher.Dispatch(new IncrementAction()); | |
dispatcher.Dispatch(new DecrementAction()); | |
dispatcher.Dispatch(new DecrementAction()); | |
dispatcher.Dispatch(new DecrementAction()); | |
// これは Reducer | |
dispatcher.Dispatch(new WaitAndIncrementAction()); | |
Console.ReadKey(); | |
// アプリケーションのステータス | |
record AppStore | |
{ | |
public int Count { get; init; } | |
} | |
// Actions | |
record IncrementAction; | |
record DecrementAction; | |
record WaitAndIncrementAction; | |
class Actions : | |
IActionHandler<AppStore, IncrementAction>, | |
IActionHandler<AppStore, DecrementAction> | |
{ | |
public AppStore Invoke(AppStore store, IncrementAction action) | |
{ | |
return store with { Count = store.Count + 1 }; | |
} | |
public AppStore Invoke(AppStore store, DecrementAction action) | |
{ | |
return store with { Count = store.Count - 1 }; | |
} | |
} | |
// Reducer | |
class Reducers : | |
IReducer<WaitAndIncrementAction> | |
{ | |
public async ValueTask HandleAsync(WaitAndIncrementAction action, IDispatcher dispatcher) | |
{ | |
Console.WriteLine("Waiting..."); | |
await Task.Delay(1000); | |
dispatcher.Dispatch(new IncrementAction()); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ライブラリ側のコード | |
using Microsoft.Extensions.DependencyInjection; | |
using Reactive.Bindings; | |
namespace ReduxDemo; | |
// Action 用のインターフェース | |
interface IActionHandler { } | |
interface IActionHandler<TStore, TAction> : IActionHandler | |
where TStore : class | |
{ | |
TStore Invoke(TStore store, TAction action); | |
} | |
// Reducer 用のインターフェース | |
interface IReducer { } | |
interface IReducer<TAction> : IReducer | |
{ | |
ValueTask HandleAsync(TAction action, IDispatcher dispatcher); | |
} | |
// Dispatcher 用のインターフェース | |
interface IDispatcher | |
{ | |
void Dispatch<TAction>(TAction action); | |
} | |
// Dispatcher の実装 | |
class Dispatcher<TStore> : IDispatcher | |
where TStore : class | |
{ | |
private readonly IReactiveProperty<TStore> _root; // ReactiveProperty にする必要はないけど… | |
private readonly IServiceProvider _serviceProvider; | |
public Dispatcher(IReactiveProperty<TStore> root, IServiceProvider serviceProvider) | |
{ | |
_root = root; | |
_serviceProvider = serviceProvider; | |
} | |
public void Dispatch<TAction>(TAction action) | |
{ | |
var reducer = _serviceProvider.GetService<IReducer<TAction>>(); | |
if (reducer is not null) | |
{ | |
// Reducer があればそっちを呼ぶ | |
_ = reducer.HandleAsync(action, this); | |
} | |
else | |
{ | |
// 無ければ Action | |
var x = _serviceProvider.GetRequiredService<IActionHandler<TStore, TAction>>(); | |
_root.Value = x.Invoke(_root.Value, action); | |
} | |
} | |
} | |
static class IServiceCollectionExtensions | |
{ | |
public static IServiceCollection AddRedux<TStore>( | |
this IServiceCollection self, | |
TStore initialState) | |
where TStore : class | |
{ | |
self.AddSingleton<IReactiveProperty<TStore>>(_ => | |
new ReactivePropertySlim<TStore>(initialState)); | |
self.AddSingleton<IDispatcher, Dispatcher<TStore>>(); | |
return self; | |
} | |
// アクションを登録してまわる | |
public static IServiceCollection AddActions<TActionHandler>(this IServiceCollection self) | |
where TActionHandler : class, IActionHandler | |
{ | |
self.AddSingleton<TActionHandler>(); | |
var serviceDescriptors = typeof(TActionHandler) | |
.GetInterfaces() | |
.Where(x => x.IsAssignableTo(typeof(IActionHandler)) && x.GenericTypeArguments.Length == 2) | |
.Select(x => ServiceDescriptor.Singleton(x, p => p.GetRequiredService<TActionHandler>())); | |
foreach (var serviceDescriptor in serviceDescriptors) | |
{ | |
self.Add(serviceDescriptor); | |
} | |
return self; | |
} | |
// Reducer を登録してまわる | |
public static IServiceCollection AddReducers<TReducer>(this IServiceCollection self) | |
where TReducer : class, IReducer | |
{ | |
self.AddSingleton<TReducer>(); | |
var serviceDescriptors = typeof(TReducer) | |
.GetInterfaces() | |
.Where(x => x.IsAssignableTo(typeof(IReducer)) && x.GenericTypeArguments.Length == 1) | |
.Select(x => ServiceDescriptor.Singleton(x, p => p.GetRequiredService<TReducer>())); | |
foreach (var serviceDescriptor in serviceDescriptors) | |
{ | |
self.Add(serviceDescriptor); | |
} | |
return self; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment