web-dev-qa-db-ja.com

アスペクト指向プログラミング:フレームワークの使用をいつ開始するか?

私はちょうど this talkGreg Young がKISSに警告するのを見た:Keep It Simple Stupid。

彼が提案したことの1つは、アスペクト指向プログラミングを行うために、1つしないフレームワークを必要とする

彼は強力な制約を作成することから始めます。つまり、すべてのメソッドがパラメータを1つだけ取るということです(ただし、 部分的なアプリケーションを使用して、これを少し遅くします )。

彼が与える例はインターフェースを定義することです:

public interface IConsumes<T>
{
    void Consume(T message);
}

コマンドを発行したい場合:

public class Command
{
    public string SomeInformation;
    public int ID;

    public override string ToString()
    {
       return ID + " : " + SomeInformation + Environment.NewLine;
    }
}

コマンドは次のように実装されます。

public class CommandService : IConsumes<Command>
{
    private IConsumes<Command> _next;

    public CommandService(IConsumes<Command> cmd = null)
    {
        _next = cmd;
    }
    public void Consume(Command message)
    {
       Console.WriteLine("Command complete!");
        if (_next != null)
            _next.Consume(message);
    }
}

コンソールへのロギングを行うには、次のように実装します。

public class Logger<T> : IConsumes<T>
{
    private readonly IConsumes<T> _next;

    public Logger(IConsumes<T> next)
    {
        _next = next;
    }
    public void Consume(T message)
    {
        Log(message);
        if (_next != null)
            _next.Consume(message);
    }

    private void Log(T message)
    {
        Console.WriteLine(message);
    }
}

次に、プレコマンドロギング、コマンドサービス、ポストコマンドロギングは次のようになります。

var log1 = new Logger<Command>(null);
var svr  = new CommandService(log);
var startOfChain = new Logger<Command>(svr);

そして、コマンドは以下によって実行されます:

var cmd = new Command();
startOfChain.Consume(cmd);

これをたとえば PostSharp で行うには、次のようにCommandServiceに注釈を付けます。

public class CommandService : IConsumes<Command>
{
    [Trace]
    public void Consume(Command message)
    {
       Console.WriteLine("Command complete!");
    }
}

そして、次のような属性クラスにロギングを実装する必要があります:

[Serializable]
public class TraceAttribute : OnMethodBoundaryAspect
{
    public override void OnEntry( MethodExecutionArgs args )
    {
        Console.WriteLine(args.Method.Name + " : Entered!" );   
    }

    public override void OnSuccess( MethodExecutionArgs args )
    {
        Console.WriteLine(args.Method.Name + " : Exited!" );
    }

    public override void OnException( MethodExecutionArgs args )
    {
        Console.WriteLine(args.Method.Name + " : EX : " + args.Exception.Message );
    }
}

Gregが使用する論拠は、属性から属性の実装への接続は「あまりにも多くの魔法」であり、ジュニア開発者に何が起こっているのかを説明できるということです。最初の例はすべて「単なるコード」であり、簡単に説明できます。

ですから、かなり時間のかかるビルドアップの後の問題は、いつGregの非フレームワークアプローチからAOPにPostSharpのようなものを使用するように切り替えるのですか?

22
Peter K.

彼は「TDWTFに直接」AOPフレームワークを作成しようとしていますか?私は真剣に彼のポイントが何であったかについての手掛かりがまだありません。 「すべてのメソッドは正確に1つのパラメーターを取る必要がある」と言うとすぐに、失敗しましたね。その段階で言うと、OKこれはソフトウェアを書く私の能力に深刻な人工的な制約を課します。これを今すぐ前に下げましょう。3か月後、完全な悪夢のコードベースを扱うことができます。

そして、あなたは何を知っていますか? Mono.Cecil を使用すると、単純な属性駆動型のILベースのロギングフレームワークを非常に簡単に作成できます。 (テストは少し複雑ですが...)

ああ、IMO、属性を使用していない場合、それはAOPではありません。ポストプロセッサの段階でメソッドの入り口/出口ログコードを実行することの全体的なポイントは、コードファイルとansを台無しにしないため、コードをリファクタリングするときにそれについて考える必要がないことです。それその力です。

すべてのグレッグはそれを愚かに保つ愚かなパラダイムがあることを実証しました。

17
user23157

私の神、その男は耐え難いほど研磨的です。その話を見るのではなく、質問のコードを読んでもらいたいと思います。

AOPを使用する目的でのみこのアプローチを使用することはないと思います。グレッグはそれは単純な状況に適していると言います。これが私が簡単な状況で行うことです:

public void DeactivateInventoryItem(CommandServices cs, Guid item, string reason)
{
    cs.Log.Write("Deactivated: {0} ({1})", item, reason);
    repo.Deactivate(item, reason);
}

ええ、私はそれをしました、私はAOPを完全に取り除きました!どうして? 単純な状況ではAOPは必要ないため

関数型プログラミングの観点からは、関数ごとにパラメーターを1つだけ許可しても、恐ろしいことではありません。それにもかかわらず、これは本当にC#でうまく機能するデザインではありません。そして、あなたの言語の粒度に逆らうことはKISS何もしません。

私がこのアプローチを使用するのは、最初にコマンドモデルを作成する必要がある場合、たとえば、元に戻すスタックが必要な場合、または WPFコマンド を使用している場合のみです。

それ以外の場合は、フレームワークまたは反射を使用します。 PostSharpはSilverlightとCompact Frameworkでも機能します。つまり、彼が "マジック"と呼ぶものは、実際には魔法ではありません

また、後輩に物事を説明できるようにするためにフレームワークを避けることに同意しません。うまくいっていません。グレッグが後輩を太った頭のバカのように、彼が提案する方法で扱うとすれば、彼の上級開発者もあまり多くはないでしょう。ジュニア年。

8
Rei Miyasaka

私はAOPについて大学で独立した研究をしました。私は実際に、Eclipseプラグインを使用してAOPをモデル化するアプローチに関する論文を書きました。それは実際にはいくぶん無関係です。重要な点は、1)私は若くて経験が浅く、2)AspectJで働いていました。ほとんどのAOPフレームワークの「魔法」はそれほど複雑ではありません。私は実際に、ハッシュテーブルを使用して単一パラメーターアプローチを実行しようとしているのと同じ頃にプロジェクトに取り組みました。 IMO、単一パラメーターアプローチは実際にはフレームワークであり、侵襲的です。この投稿でも、宣言的アプローチをレビューするよりも、単一パラメーターアプローチを理解するために多くの時間を費やしました。私は映画を見たことがないという警告を追加します。そのため、このアプローチの「魔法」は部分的なアプリケーションの使用にあるかもしれません。

グレッグがあなたの質問に答えたと思います。ジュニア開発者にAOPフレームワークの説明に過度の時間を費やしていると思われる場合は、このアプローチに切り替える必要があります。 IMO、あなたがこのボートに乗っているなら、おそらく間違ったジュニア開発者を雇っているでしょう。 AOPには宣言型のアプローチが必要だとは思いませんが、私にとっては、デザインの観点から見ると、はるかに明確で非侵襲的です。

5
kakridge

グレッグが述べていることは絶対に合理的です。そしてそこにも美しさがあります。この概念は、純粋なオブジェクト指向とは異なるパラダイムに適用できます。これは、手続き型アプローチまたはフロー指向の設計アプローチです。したがって、レガシーコードを使用している場合、多くのリファクタリングが必要になる可能性があるため、この概念を適用するのは非常に困難です。

別の例を挙げましょう。完璧ではないかもしれませんが、ポイントがより明確になることを願っています。

したがって、リポジトリを使用する製品サービスがあります(この場合はスタブを使用します)。サービスは製品のリストを取得します。

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }

    public override string ToString() { return String.Format("{0}, {1}", Name, Price); }
}

public static class ProductService
{
    public static IEnumerable<Product> GetAllProducts(ProductRepositoryStub repository)
    {
        return repository.GetAll();
    }
}

public class ProductRepositoryStub
{
    public ProductRepositoryStub(string connStr) {}

    public IEnumerable<Product> GetAll()
    {
        return new List<Product>
        {
            new Product {Name = "Cd Player", Price = 49.99m},
            new Product {Name = "Yacht", Price = 2999999m }
        };
    }
}

もちろん、サービスにインターフェースを渡すこともできます。

次に、ビューに製品のリストを表示します。したがって、インターフェースが必要です

public interface Handles<T>
{
    void Handle(T message);
}

製品のリストを保持するコマンド

public class ShowProductsCommand
{
    public IEnumerable<Product> Products { get; set; }
}

そして眺め

public class View : Handles<ShowProductsCommand>
{
    public void Handle(ShowProductsCommand cmd)
    {
        cmd.Products.ToList().ForEach(x => Console.WriteLine(x.ToString()));
    }
}

これを実行するコードが必要です。これは、Applicationというクラスで行います。 Run()メソッドは、ビジネスロジックを含まないか、少なくとも非常に少ない統合メソッドです。依存関係はメソッドとしてコンストラクタに注入されます。

public class Application
{
    private readonly Func<IEnumerable<Product>> _getAllProducts;
    private readonly Action<ShowProductsCommand> _showProducts;

    public Application(Func<IEnumerable<Product>> getAllProducts, Action<ShowProductsCommand> showProducts)
    {
        _getAllProducts = getAllProducts;
        _showProducts = showProducts;
    }

    public void Run()
    {
        var products = _getAllProducts();
        var cmd = new ShowProductsCommand { Products = products };
        _showProducts(cmd);
    }
}

最後に、mainメソッドでアプリケーションを作成します。

static void Main(string[] args)
{
    // composition
    Func<IEnumerable<Product>> getAllProducts = () => ProductService.GetAllProducts(new ProductRepositoryStub(""));
    Action<ShowProductsCommand> showProducts = (x) => new View().Handle(x);
    var app = new Application(getAllProducts, showProducts);

    app.Run();
}

ここで素晴らしいのは、既存のコードに手を加えたり、フレームワークや注釈を付けたりせずに、ロギングや例外処理などの側面を追加できることです。例外処理用。新しいクラスを追加するだけです。

public class ExceptionHandler<T> : Handles<T>
{
    private readonly Handles<T> _next;

    public ExceptionHandler(Handles<T> next) { _next = next; }

    public void Handle(T message)
    {
        try
        {
            _next.Handle(message);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

そして、アプリケーションのエントリポイントでコンポジション中にプラグインします。 Applicationクラスのコードに手を加える必要さえありません。 1行だけを置き換えます。

Action<ShowProductsCommand> showProducts = (x) => new ExceptionHandler<ShowProductsCommand>(new View()).Handle(x);

再開するには、フロー指向のデザインがある場合、新しいクラス内に機能を追加することでアスペクトを追加できます。次に、コンポジションメソッドの1行を変更する必要があります。それだけです。

ですから、あなたの質問に対する答えは、あるアプローチから別のアプローチに簡単に切り替えることはできないが、プロジェクトでどのようなアーキテクチャアプローチを採用するかを決定する必要があるということだと思います。

編集:実際、製品サービスで使用される部分的なアプリケーションパターンにより、状況が少し複雑になることに気づきました。ここにもアスペクトを追加できるように、製品サービスメソッドの周りに別のクラスをラップする必要があります。これは次のようなものになる可能性があります。

public class ProductQueries : Queries<IEnumerable<Product>>
{
    private readonly Func<IEnumerable<Product>> _query;

    public ProductQueries(Func<IEnumerable<Product>> query)
    {
        _query = query;
    }

    public IEnumerable<Product> Query()
    {
        return _query();
    }
}

public interface Queries<TResult>
{
    TResult Query();
}

次に、構成を次のように変更する必要があります。

Func<IEnumerable<Product>> getAllProducts = () => ProductService.GetAllProducts(new ProductRepositoryStub(""));
Func<IEnumerable<Product>> queryAllProducts = new ProductQueries(getAllProducts).Query;
Action<ShowProductsCommand> showProducts = (x) => new ExceptionHandler<ShowProductsCommand>(new View()).Handle(x);
var app = new Application(queryAllProducts, showProducts);
4
leifbattermann

私が何かを見逃していない限り、あなたが示したコードは「責任の連鎖」の設計パターンです。ランタイム。

追加したい動作がコンパイル時にわかっている場合は、PostSharpを使用したAOPが適しています。 PostSharpのコードウィービングは、実行時のオーバーヘッドがほとんどなく、コードを非常にクリーンに保つことを意味します(特に、マルチキャストアスペクトなどの使用を開始する場合)。 PostSharpの基本的な使い方を説明するのは特に複雑だとは思いません。 PostSharpの欠点は、コンパイル時間が大幅に増えることです。

私は本番コードで両方の手法を使用しており、それらを適用できる場所にはいくつかの重複がありますが、ほとんどの場合、実際にはさまざまなシナリオを対象としたものだと思います。

4
FinnNk

彼の代替案について-そこにいた、それを行った。 1行の属性の読みやすさに匹敵するものはありません。

新しい人たちに、AOPでどのように機能するかを簡単に説明します。

4
Danny Varod