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


22

グレッグ・ヤングがKISSに人々に警告するこの講演を見ました:Keep It Simple Stupid。

彼が提案したことの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の非フレームワークアプローチからPostSharp for AOPのようなものを使用するように切り替えるのはいつですか。


3
+1:間違いなく良い質問です。「...それなしで解決策をすでに理解しているとき」と単純に言うかもしれません。
スティーブンエバーズ

1
たぶん私はそのスタイルに慣れていないだけかもしれませんが、このようなアプリケーション全体を書くという考えは、まったく正気でないように思います。むしろメソッドインターセプターを使用します。
アーロンノート

@Aaronaught:はい、それが私がここに投稿したかった理由の一部です。Gregの説明では、システム構成は、すべての異なるIConsumes部分を通常のコードで接続しているだけです。外部XMLやFluentインターフェイスを使用する必要はありません---もう1つ覚えておく必要があります。この方法論も「学ぶべき別のこと」であると主張することができます。
ピーターK.

私はまだその動機を理解できません。AOPなどの概念の本質は、構成によって懸念を宣言的に表現できることです。私にとって、これは単なる四角い車輪の再発明です。あなたやあなたの質問に対する批判ではありませんが、唯一の賢明な答えは「他のすべてのオプションが失敗しない限り、グレッグのアプローチを使用することは決してないだろう」と思います。
アーロンノート

それがまったく気になるわけではありませんが、これはもう少しスタックオーバーフローの質問ではないでしょうか?
宮坂

回答:


17

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

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

ああ、IMO、属性を使用していない場合、AOPではありません。ポストプロセッサの段階でメソッドエントリ/終了ログコードを実行することの全体的なポイントは、コードファイルansを混乱させないようにするためです。それその力です。

すべてのグレッグは、馬鹿げた馬鹿げたパラダイムを維持することを実証しました。


6
愚かな愚かさを保つために+1。アインシュタインの有名な引用を思い出させます:「すべてをできるだけシンプルにしますが、シンプルではありません。」
宮坂

FWIW、F#には同じ制限があり、各メソッドは最大で1つの引数を取ります。
R0MANARMY

1
let concat (x : string) y = x + y;; concat "Hello, " "World!";;それは2つの引数を取るように見えますが、私は何が欠けていますか?

2
@The Mouth-実際に起こっているのは、concat "Hello, "あなたが実際にちょうどを取りyx「Hello、」であるローカルバインディングとして事前定義された関数を作成しているということです。この中間関数が表示される場合、次のようになりlet concat_x y = "Hello, " + yます。そして、それに続いて、あなたはを呼び出していconcat_x "World!"ます。構文によりわかりにくくなりますが、これにより、たとえば、などの新しい関数を「ベイク」できますlet printstrln = print "%s\n" ;; printstrln "woof"。また、のような操作を行ってもlet f(x,y) = x + y、実際には1つのタプル引数にすぎません。
宮坂

1
関数型プログラミングを初めてやったのは大学のミランダにいたとき、F#を見てみる必要があります。面白いですね。

8

なんてこった、あの男は耐えられないほどすり減っている。私はその話を見たのではなく、あなたの質問のコードを読んでほしいと思います。

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 Commandsを使用していた場合などです。

それ以外の場合は、フレームワークまたは何らかのリフレクションを使用します。PostSharpは、SilverlightおよびCompact Frameworkでも動作します。そのため、彼が「マジック」と呼ぶものは、まったく魔法はありません。

また、後輩に物事を説明できるようにするためにフレームワークを避けることに同意しません。良いことをしていません。グレッグが後輩を太った頭のばかみたいに扱うように勧めるなら、彼の上級開発者もあまり偉大ではないのではないかと思う。ジュニア年。


5

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

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


+1「宣言的アプローチをレビューするよりも、単一パラメータアプローチを理解しようとしてより多くの時間を費やした」IConsume<T>達成されていることのために、例が過度に複雑であることがわかりました。
スコットホイットロック

4

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

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

私は本番コードで両方のテクニックを使用しますが、それらを適用できる場所には多少のオーバーラップがありますが、ほとんどの場合、実際には異なるシナリオを対象にしていると思います。


4

彼の代替案について-行って、それをやった。1行の属性の可読性と比較できるものはありません。

AOPで物事がどのように機能するかを説明する新しい人に短い講義をします。


4

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

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

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

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);
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.