Aspect Oriented Programming : 언제 프레임 워크를 사용하기 시작합니까?


22

방금 Greg Young이 사람들에게 KISS에 경고하는 이야기 를 보았습니다 .

그가 제안한 것 중 하나는 Aspect 지향 프로그래밍을하기 위해서는 프레임 워크가 필요 하지 않다는 것 입니다.

그는 모든 제약 조건이 하나의 매개 변수 만 취해야한다는 강력한 제약 조건으로 시작합니다 ( 부분적 적용을 사용하여 조금 나중에 이완 하지만 ).

그가 제공하는 예는 인터페이스를 정의하는 것입니다.

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와 같은 것을 사용하도록 전환 할 것인가?


3
+1 : 확실히 좋은 질문입니다. "... 솔루션이없는 솔루션을 이미 이해하고있을 때"라고 간단히 말할 수 있습니다.
Steven Evers 2016 년

1
어쩌면 나는 그 스타일에 익숙하지 않지만, 이와 같은 전체 응용 프로그램을 작성한다는 아이디어는 완전히 미쳤습니다. 차라리 메소드 인터셉터를 사용하고 싶습니다.
Aaronaught

@Aaronaught : 예, 여기에 게시하고 싶었던 이유 중 하나입니다. Greg의 설명은 시스템 구성이 다른 모든 코드에서 IN NORMAL CODE를 연결하는 것 IConsumes입니다. 외부 XML 또는 일부 Fluent 인터페이스를 사용하지 않고 배우는 것이 좋습니다. 이 방법론은 "배워야 할 또 다른 것"이라고 주장 할 수 있습니다.
Peter K.

나는 여전히 내가 동기 부여를 이해하고 있는지 확신하지 못한다. AOP와 같은 개념의 본질은 구성을 통해 우려를 선언적 으로 표현할 수 있어야한다 . 나에게 이것은 단지 사각 바퀴를 재발 명하고 있습니다. 당신이나 당신의 질문에 대한 비판은 아니지만, 유일한 합리적인 대답은 "다른 옵션이 모두 실패하지 않으면 결코 Greg의 접근법을 사용하지 않을 것"이라고 생각합니다.
Aaronaught

그것은 나를 전혀 귀찮게하지는 않지만 이것이 스택 오버플로 질문에 조금 더 가깝지 않습니까?
Rey Miyasaka

답변:


17

"TDWTF에 대한 스트레이트"AOP 프레임 워크를 작성하려고합니까? 나는 아직도 그의 요점이 무엇인지 전혀 모른다. "모든 메소드는 정확히 하나의 매개 변수를 가져와야합니다"라고 말하자마자 실패하지 않습니까? 이 단계에서 여러분은 이것이 소프트웨어를 작성하는 능력에 심각한 인공 구속 조건을 부과한다고 말하고 있습니다. 3 개월 전에 완전히 악몽을 꾸려야 할 코드베이스를 갖추 었습니다.

그리고 당신은 무엇을 알고 있습니까? Mono.Cecil을 사용 하면 간단한 속성 기반 IL 기반 로깅 프레임 워크를 쉽게 작성할 수 있습니다. (테스트는 약간 더 복잡하지만 ...)

아 그리고 IMO, 속성을 사용하지 않으면 AOP가 아닙니다. 포스트 프로세서 단계에서 메소드 시작 / 종료 로깅 코드를 수행하는 요점은 코드 파일을 엉망으로 만들지 않기 때문에 코드를 리팩터링 할 때 생각할 필요가 없습니다. 그게 입니다 전력.

그렉은 모두 바보 같은 패러다임 유지라는 것을 보여주었습니다.


6
멍청한 짓을 유지하기 위해 +1. 아인슈타인의 유명한 인용문을 상기시켜줍니다.
Rei Miyasaka

FWIW, F #의 제한은 동일하며 각 메서드는 최대 하나의 인수를 사용합니다.
R0MANARMY

1
let concat (x : string) y = x + y;; concat "Hello, " "World!";;두 가지 주장이 필요한 것 같습니다. 무엇을 놓치고 있습니까?

2
@The 입 - 실제로 일어나고있는 일이다 그와 concat "Hello, "당신이 실제로 단지 취하는 함수 만드는 y하고있다 x로컬이 "안녕하세요"로 바인딩으로 미리 정의합니다. 이 중간 기능을 볼 수 있다면 다음과 같습니다 let concat_x y = "Hello, " + y. 그런 다음을 호출 concat_x "World!"합니다. 예를 들어 - 구문은 덜 분명하게, 그러나 이것은 당신이 새로운 기능을 "구워"할 수 있습니다 let printstrln = print "%s\n" ;; printstrln "woof". 또한와 같은 작업을 수행하더라도 let f(x,y) = x + y실제로는 하나의 튜플 인수입니다.
Rey Miyasaka

1
함수 프로그래밍이 대학교의 미란다에서 돌아 왔을 때 F #을 살펴 봐야 할 것 같습니다.

8

맙소사, 그 남자는 참을 수없는 연마입니다. 그 대화를 보지 않고 질문의 코드를 읽었 으면 좋겠습니다.

나는 AOP를 사용하기 위해서만이 접근법을 사용하지 않았다고 생각합니다. Greg는 간단한 상황에 적합하다고 말합니다. 간단한 상황에서 내가 할 일은 다음과 같습니다.

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

그래, 나는 그것을했다, 나는 AOP를 완전히 제거했다! 왜? 간단한 상황에서는 AOP가 필요하지 않기 때문 입니다.

함수형 프로그래밍 관점에서 함수 당 하나의 매개 변수 만 허용해도 실제로 두렵지 않습니다. 그럼에도 불구하고 이것은 실제로 C #에서 잘 작동하는 디자인이 아니며 언어에 영향을 미치는 것은 KISS가 아닙니다.

예를 들어 실행 취소 스택이 필요하거나 WPF 명령 으로 작업하는 경우와 같이 명령 모델을 시작해야하는 경우에만이 방법을 사용합니다 .

그렇지 않으면 프레임 워크 또는 일부 반사를 사용합니다. 그래서 그는 "마법"이라고 부르는 정말 마법 아니다 - PostSharp도 실버 라이트와 Compact Framework에서 작동 전혀 .

또한 후배들에게 설명 할 수 있도록 프레임 워크를 피하는 것에 동의하지 않습니다. 그들에게 좋은 일이 아닙니다. Greg가 그의 후배들에게 멍청한 바보처럼 대우받는 것을 제안하는 방식으로 대우를 처리한다면, 그의 선임 개발자들은 아마도 그들이 배우는 동안 아무것도 배울 기회가 많지 않았기 때문에 그다지 훌륭하지 않다고 생각합니다. 주니어 년.


5

나는 대학에서 AOP에 대한 독립적 인 연구를했다. 실제로 Eclipse 플러그인을 사용하여 AOP를 모델링하는 방법에 대한 논문을 작성했습니다. 그것은 실제로 내가 생각하기에 다소 관련이 없습니다. 요점은 1) 나는 젊고 경험이 없었으며 2) AspectJ와 함께 일하고 있었다. 대부분의 AOP 프레임 워크의 "마법"이 그렇게 복잡하지 않다는 것을 알 수 있습니다. 실제로 해시 테이블을 사용하여 단일 매개 변수 접근 방식을 시도하는 것과 거의 같은 시간에 프로젝트를 수행했습니다. 단일 매개 변수 접근 방식 인 IMO는 실제로 프레임 워크이며 침입 적입니다. 이 게시물에서도 선언적 접근 방식을 검토하는 것보다 단일 매개 변수 접근 방식을 이해하는 데 더 많은 시간을 보냈습니다. 영화를 보지 않은 경고를 추가 할 것이므로이 방법의 "마법"은 부분 응용 프로그램을 사용하는 것일 수 있습니다.

나는 그렉이 당신의 질문에 대답했다고 생각합니다. 주니어 개발자에게 AOP 프레임 워크를 설명하는 데 시간이 너무 많이 걸린다고 생각되면이 방법으로 전환해야합니다. IMO,이 보트에 있다면 잘못된 주니어 개발자를 고용하고있을 것입니다. AOP에 선언적 접근 방식이 필요하다고 생각하지는 않지만 디자인 관점에서 볼 때 훨씬 명확하고 비 침습적입니다.


"선언적 접근 방식을 검토하는 것보다 단일 매개 변수 접근 방식을 이해하는 데 더 많은 시간을 소비했습니다." 나는 IConsume<T>성취되고있는 것에 대해 지나치게 복잡한 예를 발견했다 .
Scott Whitlock

4

내가 놓친 코드가 없으면 '책임 체인'디자인 패턴으로 객체에 대한 일련의 작업 (예 : 일련의 명령 처리기를 통과하는 명령)을 연결 해야하는 경우에 좋습니다. 실행 시간.

컴파일 할 때 추가 할 동작이 무엇인지 알고 있으면 PostSharp를 사용하는 AOP가 좋습니다. PostSharp의 코드 직조는 런타임 오버 헤드가없고, 특히 멀티 캐스트 측면과 같은 것을 사용하기 시작할 때 코드를 매우 깨끗하게 유지한다는 것을 의미합니다. PostSharp의 기본 사용법은 특히 설명하기가 복잡하지 않다고 생각합니다. PostSharp의 단점은 컴파일 시간이 현저하게 증가한다는 것입니다.

나는 프로덕션 코드에서 두 가지 기술을 모두 사용하지만 적용 할 수있는 부분에 약간의 겹침이 있지만 실제로 다른 시나리오를 목표로한다고 생각합니다.


4

그의 대안에 관해서는-거기에있었습니다. 한 줄 속성의 가독성과 비교되는 것은 없습니다.

새로운 사람들에게 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);
    }
}

마지막으로 우리는 기본 방법으로 응용 프로그램을 작성합니다.

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 클래스의 코드를 건드릴 필요조차 없습니다. 한 줄만 바꿉니다.

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

다시 시작하기 위해 : 흐름 지향 디자인이 있으면 새 클래스에 기능을 추가하여 측면을 추가 할 수 있습니다. 그런 다음 컴포지션 방법에서 한 줄을 변경해야합니다.

따라서 귀하의 질문에 대한 대답은 한 접근법에서 다른 접근법으로 쉽게 전환 할 수 없지만 프로젝트에서 어떤 종류의 건축 접근법을 결정해야하는지입니다.

편집 : 실제로 제품 서비스와 함께 사용되는 부분 응용 프로그램 패턴으로 인해 작업이 좀 더 복잡하다는 것을 알았습니다. 여기에 측면을 추가하려면 제품 서비스 방법을 다른 클래스로 포장해야합니다. 다음과 같이 될 수 있습니다.

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.