Open15

oopで関数合成ライクなことが実現したい(のと、少しaopについて

pacypacy

発端
https://twitter.com/0xpacy/status/1656701970193989632

アスペクト指向プログラミング (AOP) の核心は、横断的な関心事です。作成者は、ロギング、認可、認証、トランザクション管理などの懸念事項を認識していました。彼らは、なぜこれらをプログラムを通じて広める必要があるのか​​と尋ねました。

それらの実装はどこでもほぼ同じです
これらはビジネス ロジックに固有のものではありません。それらは関数の意味上の明確さから目をそらしてしまいます
これらはシステムのほとんどのコンポーネントで必要となります。
このアイデアは、これらの横断的な懸念事項を、他のロジック フローに対する一種のアドオンまたはインターセプターとして実行することです。これにより、横断的な懸念事項を一元的に定義し、コア ロジックとは別に展開し、コア ロジックに焦点を当てたままにすることができます。

pacypacy

c#の簡単な例
loggingの共通部分として想定される部分

try
{
    //Do something here
 
    logger.LogSuccess(//..
}
catch (Exception ex)
{
    logger.LogError(throw;
}
pacypacy

LoggingAwareDocumentSourceというデコレーターを作る

public class LoggingAwareDocumentSource : IDocumentSource 
{
    private readonly IDocumentSource  decoratedDocumentSource;
    //..
 
    public Document[] GetDocuments(string format)
    {
        try
        {
            var documents = decoratedDocumentSource.GetDocuments(format);
 
            logger.LogSuccess(
                "Obtained " + documents.Length + " documents of type " + format);
 
            return documents;
        }
        catch (Exception ex)
        {
            logger.LogError(
                "Error obtaining documents of type " + format, ex);
 
            throw;
        }
    }
}
var source =
    new LoggingAwareDocumentSource (
        new DocumentSource (connectionString),
        new Logger());
pacypacy

これだと汎用性が足りない、デコレーターをたくさん作る羽目になる
あと詳細の情報が抜け落ちる

  1. インターフェースを少数にする
  2. 動的プロキシを使う

で解決する

pacypacy
  1. インターフェースを少数にする

ただこれだと、インターフェースが制限されて不便
それと詳細情報はまだ抜け落ちてる

public interface IQueryHandler<TQuery, TResult>
{
    TResult Handle(TQuery query);
}
public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}
public class DocumentSource : IQueryHandler<GetDocumentsQuery, GetDocumentsResult>
{
    //..
    public GetDocumentsResult Handle(GetDocumentsQuery query)
    {
        using (var context = CreateEFContext())
        {
            return
                new GetDocumentsResult(
                    context
                        .Documents
                        .Where(c => c.Name.EndsWith("." + query.Format))
                        .ToArray());
        }
    }
    //..
}
public class LoggingAwareQueryHandler<TQuery, TResult> : IQueryHandler<TQuery,TResult>
{
    private readonly IQueryHandler<TQuery, TResult> decoratedHandler;
    //..
 
    public TResult Handle(TQuery query)
    {
        try
        {
            var result = decoratedHandler.Handle(query);
 
            logger.LogSuccess(...);
 
            return result;
        }
        catch (Exception ex)
        {
            logger.LogError(..., ex);
            throw;
        }
    }
}
pacypacy
  1. 動的プロキシを使う

設計時に 100 個のデコレータを作成する代わりに、1 つの汎用アスペクトを作成し、それを使用して実行時に 100 個のデコレータを生成

良さそうに見えるが、リフレクションに依存していて性能に悪い

namespace Application.CastleDynamicProxy
{
    class Program
    {
        static void Main(string[] args)
        {
            var finder =
                new FakeDocumentSource("connectionString1")
                .AsLoggable<IDocumentSource>(
                    new LoggingData { Name = "connectionString", Value = "connectionString1"},
                    new LoggingData { Name = "Department", Value = "A" });

            var result = finder.GetDocuments("docx");

            Console.ReadLine();
        }
    }
}
namespace Application.CastleDynamicProxy
{
    public static class ExtensionMethods
    {
        public static T AsLoggable<T>(
            this T instance, params LoggingData[] constantLoggingData)
        {
            if(!typeof(T).IsInterface)
                throw new Exception("T should be an interface");

            ProxyGenerator proxyGenerator = new ProxyGenerator();

            return
                (T)proxyGenerator.CreateInterfaceProxyWithTarget(
                    typeof(T),
                    instance,
                    new LoggingAspect(
                        
                        new ConsoleLogger(),
                        new CompositeMethodLoggingDataExtractor(
                            new MethodDescriptionLoggingDataExtractor(),
                            new ConstantMethodLoggingDataExtractor(constantLoggingData)), 
                        new CompositeLoggingDataExtractor(
                            new LogCountAttributeBasedLoggingDataExtractor(),
                            new LogAttributeBasedLoggingDataExtractor()),
                        new CompositeLoggingDataExtractor(
                            new LogCountAttributeBasedLoggingDataExtractor(),
                            new LogAttributeBasedLoggingDataExtractor())));
        }
    }
}
pacypacy

pythonの@から始まる関数デコレーターとかあれば、function渡して引数とかを受け取れるようにinterceptすれば、任意のfunctionをloggingとかできるかも

pacypacy
def log(func):
    """関数の呼び出しをログ出力するデコレーター"""
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"Result: {result}")
        return result
    return wrapper

@log
def greet(name):
    return f"Hello {name}!"

greet("Alice")  # Calling greet with args=('Alice',) kwargs={} / Result: Hello Alice!
pacypacy
def log(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == 'info':
                print(f"[INFO] Calling {func.__name__} with args={args} kwargs={kwargs}")
            elif level == 'debug':
                print(f"[DEBUG] Calling {func.__name__} with args={args} kwargs={kwargs}")
            result = func(*args, **kwargs)
            if level == 'info':
                print(f"[INFO] Result: {result}")
            elif level == 'debug':
                print(f"[DEBUG] Result: {result}")
            return result
        return wrapper
    return decorator

@log(level='info')
def greet(name):
    return f"Hello {name}!"

greet("Alice")  # [INFO] Calling greet with args=('Alice',) kwargs={} / [INFO] Result: Hello Alice!
pacypacy

ビンゴで無名関数というかオブジェクトに紐づかない関数/関数を受け取る関数を作れる言語だと大丈夫そう?

pacypacy

余談だけど、oopのメソッドチェーンがfpの|>に該当すると思っていて、ビジュアルプログラミングのnodeを繋げていくのも似ている気がする

pacypacy

デコレーター/関数合成を使ってビジュアルプログラミング的な考え方をプログラム言語にも導入できそう