Skip to content

Instantly share code, notes, and snippets.

@runceel
Last active April 21, 2022 12:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save runceel/1a3179250cf5306d8ea6306afcfdeb07 to your computer and use it in GitHub Desktop.
Save runceel/1a3179250cf5306d8ea6306afcfdeb07 to your computer and use it in GitHub Desktop.
Redux パターンのなるべくタイプセーフな雰囲気実装
// ライブラリ使う側のコード
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());
}
}
// ライブラリ側のコード
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