차단 대 주입 : 프레임 워크 아키텍처 결정


28

내가 디자인하는 데 도움이되는이 프레임 워크가 있습니다. 몇 가지 일반적인 구성 요소를 사용하여 수행해야하는 일반적인 작업이 있습니다. 특히 로깅, 캐싱 및 이벤트 발생.

의존성 주입을 사용하는 것이 더 나은지 확실하지 않으며 (예를 들어 속성으로) 이러한 모든 구성 요소를 각 서비스에 도입하거나 서비스의 각 방법에 메타 데이터를 배치하고 이러한 공통 작업을 수행하는 데 인터셉트를 사용해야합니까? ?

다음은 두 가지 예입니다.

주입:

public class MyService
{
    public ILoggingService Logger { get; set; }

    public IEventBroker EventBroker { get; set; }

    public ICacheService Cache { get; set; }

    public void DoSomething()
    {
        Logger.Log(myMessage);
        EventBroker.Publish<EventType>();
        Cache.Add(myObject);
    }
}

다른 버전은 다음과 같습니다.

차단:

public class MyService
{
    [Log("My message")]
    [PublishEvent(typeof(EventType))]
    public void DoSomething()
    {

    }
}

내 질문은 다음과 같습니다.

  1. 복잡한 프레임 워크에 가장 적합한 솔루션은 무엇입니까?
  2. 차단이 성공하면 메소드의 내부 값과 상호 작용하는 옵션 (예 : 캐시 서비스와 함께 사용)은 무엇입니까? 이 동작을 구현하기 위해 속성 대신 다른 방법을 사용할 수 있습니까?
  3. 아니면 문제를 해결하기위한 다른 해결책이있을 수 있습니까?

2
1과 2에 대해서는 의견이 없지만 3에 관해서는 AoP ( Aspect-oriented programming )와 특히 Spring.NET을 살펴보십시오 .

명확히하기 위해 : 의존성 주입과 Aspect 지향 프로그래밍의 비교를 찾고 있습니까?
M.Babcock

@ M.Babcock 스스로 그렇게 보지 못했지만 맞습니다

답변:


38

교차 절단 로깅, 캐싱 등과 같은 문제 는 종속성이 아니므로 서비스에 주입해서는 안됩니다. 그러나 대부분의 사람들은 완전한 인터리빙 AOP 프레임 워크에 도달하는 것처럼 보이지만이를위한 멋진 디자인 패턴이 있습니다 : Decorator .

위의 예에서 MyService가 IMyService 인터페이스를 구현하도록하십시오.

public interface IMyService
{
    void DoSomething();
}

public class MyService : IMyService
{
    public void DoSomething()
    {
        // Implementation goes here...
    }
}

이렇게하면 MyService 클래스에 Cross-cutting Concerns가 완전히 없어지고 SRP ( Single Responsibility Principle )를 따릅니다.

로깅을 적용하기 위해 로깅 데코레이터를 추가 할 수 있습니다.

public class MyLogger : IMyService
{
    private readonly IMyService myService;
    private readonly ILoggingService logger;

    public MyLogger(IMyService myService, ILoggingService logger)
    {
        this.myService = myService;
        this.logger = logger;
    }

    public void DoSomething()
    {
        this.myService.DoSomething();
        this.logger.Log("something");
    }
}

캐싱, 미터링, 이벤트 등을 같은 방식으로 구현할 수 있습니다. 각 데코레이터는 정확히 하나의 작업을 수행하므로 SRP도 따르므로 임의로 복잡한 방식으로 구성 할 수 있습니다. 예 :

var service = new MyLogger(
    new LoggingService(),
    new CachingService(
        new Cache(),
        new MyService());

5
데코레이터 패턴은 이러한 문제를 개별적으로 유지하는 좋은 방법이지만 많은 서비스가있는 경우 PostSharp 또는 Castle.DynamicProxy와 같은 AOP 도구를 사용합니다. 그렇지 않으면 각 서비스 클래스 인터페이스에 대해 클래스를 코딩해야합니다 그리고 로거 데코레이터와 각 데코레이터는 잠재적으로 매우 유사한 상용구 코드 일 수 있습니다 (즉, 모듈화 / 캡슐화가 향상되었지만 여전히 많이 반복하고 있습니다).
Matthew Groves

4
동의했다. 작년에 데코레이터에서 AOP로 옮기는 방법을 설명하는 강연을했습니다 : channel9.msdn.com/Events/GOTO/GOTO-2011-Copenhagen/…
Mark Seemann

이 프로그램을 기반으로 간단한 구현을 코딩했습니다 .good.net
2015 /

의존성 주입으로 서비스 및 데코레이터를 어떻게 주입 할 수 있습니까?
TIKSN

@TIKSN 짧은 대답은 위와 같습니다 . 그러나 당신이 요구하고 있기 때문에, 당신은 다른 것에 대한 답을 찾고 있어야하지만, 그것이 무엇인지 추측 할 수는 없습니다. 사이트에서 자세한 내용을 문의하거나 새로운 질문을 하시겠습니까?
Mark Seemann

6

소수의 서비스에 대해서는 Mark의 대답이 좋다고 생각합니다. 새로운 타사 종속성을 배우거나 소개 할 필요가 없으며 여전히 우수한 SOLID 원칙을 따르게됩니다.

많은 서비스의 경우 PostSharp 또는 Castle DynamicProxy와 같은 AOP 도구를 권장합니다. PostSharp는 무료 (맥주에서와 같이) 버전을 가지고 있으며 최근에 PostSharp Toolkit for Diagnostics (맥주 및 음성에서 무료로) 출시했습니다 .


2

나는 프레임 워크의 디자인이이 질문과 직교하는 것을 발견했다. 당신은 먼저 프레임 워크의 인터페이스에 초점을 두어야하며, 아마도 배경적인 정신적 프로세스로서 누군가가 실제로 그것을 소비하는 방법을 고려해야한다. 영리한 방식으로 사용되는 것을 방해 하는 것을 원하지는 않지만 프레임 워크 디자인에 대한 입력일뿐입니다. 많은 중 하나.


1

나는이 문제를 여러 번 직면했으며 간단한 해결책을 생각해 냈다고 생각합니다.

처음에는 데코레이터 패턴을 사용하고 각 방법을 수동으로 구현했습니다. 수백 가지의 방법이 있으면 매우 지루합니다.

그런 다음 PostSharp를 사용하기로 결정했지만 간단한 코드로 많은 작업을 수행하기 위해 전체 라이브러리를 포함시키는 아이디어가 마음에 들지 않았습니다.

그런 다음 재미 있지만 런타임에 동적으로 IL을 방출하는 투명한 프록시 경로를 내려 갔으며 프로덕션 환경에서하고 싶지 않은 작업이었습니다.

최근에는 디자인 타임에 데코레이터 패턴을 자동으로 구현하기 위해 T4 템플릿을 사용하기로 결정했습니다 .T4 템플릿은 실제로 작업하기가 매우 어려우며 신속하게 완료해야 코드를 만들었습니다. 빠르고 더러 우며 속성을 지원하지 않지만 누군가가 유용하게 사용할 수 있기를 바랍니다.

코드는 다음과 같습니다.

        var linesToUse = code.Split(Environment.NewLine.ToCharArray()).Where(l => !string.IsNullOrWhiteSpace(l));
        string classLine = linesToUse.First();

        // Remove the first line this is just the class declaration, also remove its closing brace
        linesToUse = linesToUse.Skip(1).Take(linesToUse.Count() - 2);
        code = string.Join(Environment.NewLine, linesToUse).Trim()
            .TrimStart("{".ToCharArray()); // Depending on the formatting this may be left over from removing the class

        code = Regex.Replace(
            code,
            @"public\s+?(?'Type'[\w<>]+?)\s(?'Name'\w+?)\s*\((?'Args'[^\)]*?)\)\s*?\{\s*?(throw new NotImplementedException\(\);)",
            new MatchEvaluator(
                match =>
                    {
                        string start = string.Format(
                            "public {0} {1}({2})\r\n{{",
                            match.Groups["Type"].Value,
                            match.Groups["Name"].Value,
                            match.Groups["Args"].Value);

                        var args =
                            match.Groups["Args"].Value.Split(",".ToCharArray())
                                .Select(s => s.Trim().Split(" ".ToCharArray()))
                                .ToDictionary(s => s.Last(), s => s.First());

                        string call = "_decorated." + match.Groups["Name"].Value + "(" + string.Join(",", args.Keys) + ");";
                        if (match.Groups["Type"].Value != "void")
                        {
                            call = "return " + call;
                        }

                        string argsStr = args.Keys.Any(s => s.Length > 0) ? ("," + string.Join(",", args.Keys)) : string.Empty;
                        string loggedCall = string.Format(
                            "using (BuildLogger(\"{0}\"{1})){{\r\n{2}\r\n}}",
                            match.Groups["Name"].Value,
                            argsStr,
                            call);
                        return start + "\r\n" + loggedCall;
                    }));
        code = classLine.Trim().TrimEnd("{".ToCharArray()) + "\n{\n" + code + "\n}\n";

예를 들면 다음과 같습니다.

public interface ITestAdapter : IDisposable
{
    string TestMethod1();

    IEnumerable<string> TestMethod2(int a);

    void TestMethod3(List<string[]>  a, Object b);
}

그런 다음 ITestAdapter를 구현하는 LoggingTestAdapter라는 클래스를 작성하고 Visual Studio에서 모든 메소드를 자동으로 구현 한 다음 위의 코드를 통해 실행하십시오. 그러면 다음과 같은 것이 있어야합니다.

public class LoggingTestAdapter : ITestAdapter
{

    public void Dispose()
    {
        using (BuildLogger("Dispose"))
        {
            _decorated.Dispose();
        }
    }
    public string TestMethod1()
    {
        using (BuildLogger("TestMethod1"))
        {
            return _decorated.TestMethod1();
        }
    }
    public IEnumerable<string> TestMethod2(int a)
    {
        using (BuildLogger("TestMethod2", a))
        {
            return _decorated.TestMethod2(a);
        }
    }
    public void TestMethod3(List<string[]> a, object b)
    {
        using (BuildLogger("TestMethod3", a, b))
        {
            _decorated.TestMethod3(a, b);
        }
    }
}

다음은 지원 코드입니다.

public class DebugLogger : ILogger
{
    private Stopwatch _stopwatch;
    public DebugLogger()
    {
        _stopwatch = new Stopwatch();
        _stopwatch.Start();
    }
    public void Dispose()
    {
        _stopwatch.Stop();
        string argsStr = string.Empty;
        if (Args.FirstOrDefault() != null)
        {
            argsStr = string.Join(",",Args.Select(a => (a ?? (object)"null").ToString()));
        }

        System.Diagnostics.Debug.WriteLine(string.Format("{0}({1}) @ {2}ms", Name, argsStr, _stopwatch.ElapsedMilliseconds));
    }

    public string Name { get; set; }

    public object[] Args { get; set; }
}

public interface ILogger : IDisposable
{
    string Name { get; set; }
    object[] Args { get; set; }
}


public class LoggingTestAdapter<TLogger> : ITestAdapter where TLogger : ILogger,new()
{
    private readonly ITestAdapter _decorated;

    public LoggingTestAdapter(ITestAdapter toDecorate)
    {
        _decorated = toDecorate;
    }

    private ILogger BuildLogger(string name, params object[] args)
    {
        return new TLogger { Name = name, Args = args };
    }

    public void Dispose()
    {
        _decorated.Dispose();
    }

    public string TestMethod1()
    {
        using (BuildLogger("TestMethod1"))
        {
            return _decorated.TestMethod1();
        }
    }
    public IEnumerable<string> TestMethod2(int a)
    {
        using (BuildLogger("TestMethod2", a))
        {
            return _decorated.TestMethod2(a);
        }
    }
    public void TestMethod3(List<string[]> a, object b)
    {
        using (BuildLogger("TestMethod3", a, b))
        {
            _decorated.TestMethod3(a, b);
        }
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.