Interception vs Injection: keputusan kerangka arsitektur

28

Ada kerangka kerja yang saya bantu desain. Ada beberapa tugas umum yang harus dilakukan dengan menggunakan beberapa komponen umum: Logging, Caching dan meningkatkan peristiwa pada khususnya.

Saya tidak yakin apakah lebih baik menggunakan injeksi ketergantungan dan memperkenalkan semua komponen ini ke setiap layanan (seperti properti misalnya) atau haruskah saya menempatkan beberapa jenis meta data pada setiap metode layanan saya dan menggunakan intersepsi untuk melakukan tugas-tugas umum ini ?

Berikut ini contoh keduanya:

Injeksi:

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);
    }
}

dan inilah versi lainnya:

Penangkapan:

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

    }
}

Ini pertanyaan saya:

  1. Solusi mana yang terbaik untuk kerangka kerja yang rumit?
  2. Jika intersepsi menang, apa opsi saya untuk berinteraksi dengan nilai internal suatu metode (misalnya untuk digunakan dengan layanan cache?)? Bisakah saya menggunakan cara lain daripada atribut untuk menerapkan perilaku ini?
  3. Atau mungkin ada solusi lain untuk menyelesaikan masalah?
Beatles1692
sumber
2
Saya tidak memiliki pendapat tentang 1 dan 2, tetapi mengenai 3: pertimbangkan untuk melihat ke AoP ( pemrograman berorientasi aspek ) dan khususnya ke Spring.NET .
Untuk memperjelas: Anda mencari perbandingan antara Injeksi Ketergantungan dan Pemrograman Berorientasi Aspek, benar?
M.Babcock
@ M.Babcock Belum pernah melihatnya seperti itu tetapi itu benar

Jawaban:

38

Masalah lintas sektoral seperti logging, caching dll. Bukan ketergantungan, jadi jangan disuntikkan ke layanan. Namun, sementara kebanyakan orang tampaknya meraih kerangka kerja AOP penuh interleaving, ada pola desain yang bagus untuk ini: Dekorator .

Pada contoh di atas, biarkan MyService mengimplementasikan antarmuka IMyService:

public interface IMyService
{
    void DoSomething();
}

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

Ini membuat kelas MyService benar-benar bebas dari Masalah Lintas Sektor, sehingga mengikuti Prinsip Tanggung Jawab Tunggal (SRP).

Untuk menerapkan logging, Anda dapat menambahkan Decorator logging:

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");
    }
}

Anda dapat menerapkan caching, pengukuran, acara, dll. Dengan cara yang sama. Setiap Dekorator melakukan tepat satu hal, sehingga mereka juga mengikuti SRP, dan Anda dapat menyusunnya dengan cara yang rumit dan sewenang-wenang. Misalnya

var service = new MyLogger(
    new LoggingService(),
    new CachingService(
        new Cache(),
        new MyService());
Mark Seemann
sumber
5
Pola dekorator adalah cara yang bagus untuk memisahkan masalah-masalah tersebut, tetapi jika Anda memiliki BANYAK layanan, di situlah saya akan menggunakan alat AOP seperti PostSharp atau Castle. DAN dekorator logger, dan masing-masing dekorator tersebut berpotensi menjadi kode boilerplate yang sangat mirip (yaitu Anda mendapatkan modularisasi / enkapsulasi yang ditingkatkan, tetapi Anda masih sering mengulangi sendiri).
Matthew Groves
4
Sepakat. Saya memberikan ceramah tahun lalu yang menjelaskan cara pindah dari Dekorator ke AOP: channel9.msdn.com/Events/GOTO/GOTO-2011-Copenhagen/…
Mark Seemann
Saya membuat kode implementasi sederhana berdasarkan program inigood.net/2015/09/09/08/DecoratorSpike.aspx
Dave Mateer
Bagaimana kita bisa menyuntikkan layanan dan dekorator dengan suntikan ketergantungan?
TIKSN
@TIKSN Jawaban singkatnya adalah: seperti yang ditunjukkan di atas . Namun, karena Anda bertanya, Anda harus mencari jawaban untuk sesuatu yang lain, tetapi saya tidak dapat menebak apa itu. Bisakah Anda menguraikan, atau mungkin mengajukan pertanyaan baru di sini di situs?
Mark Seemann
6

Untuk beberapa layanan, saya pikir jawaban Mark baik: Anda tidak perlu belajar atau memperkenalkan dependensi pihak ketiga yang baru dan Anda masih akan mengikuti prinsip-prinsip SOLID yang baik.

Untuk sejumlah besar layanan, saya akan merekomendasikan alat AOP seperti PostSharp atau Castle DynamicProxy. PostSharp memiliki versi gratis (seperti bir), dan mereka baru-baru ini merilis PostSharp Toolkit untuk Diagnostik , (gratis seperti dalam bir DAN pidato) yang akan memberi Anda beberapa fitur logging di luar kotak.

Matthew Groves
sumber
2

Saya menemukan desain kerangka kerja sebagian besar ortogonal untuk pertanyaan ini - Anda harus fokus pada antarmuka kerangka kerja Anda terlebih dahulu, dan mungkin sebagai latar belakang proses mental mempertimbangkan bagaimana seseorang mungkin benar-benar mengkonsumsinya. Anda tidak ingin melakukan sesuatu yang mencegahnya digunakan dengan cara yang cerdik, tetapi seharusnya hanya menjadi input ke dalam desain kerangka kerja Anda; satu di antara banyak.


sumber
1

Saya sering menghadapi masalah ini dan saya pikir saya telah menemukan solusi sederhana.

Awalnya saya pergi dengan pola dekorator dan secara manual menerapkan setiap metode, ketika Anda memiliki ratusan metode ini menjadi sangat membosankan.

Saya kemudian memutuskan untuk menggunakan PostSharp tetapi saya tidak suka gagasan menyertakan seluruh perpustakaan hanya untuk melakukan sesuatu yang dapat saya capai dengan (banyak) kode sederhana.

Saya kemudian pergi melalui rute proksi transparan yang menyenangkan tetapi melibatkan memancarkan IL secara dinamis pada waktu berjalan dan tidak akan menjadi sesuatu yang ingin saya lakukan dalam lingkungan produksi.

Saya baru-baru ini memutuskan untuk menggunakan template T4 untuk secara otomatis menerapkan pola dekorator pada waktu desain, ternyata template T4 sebenarnya cukup sulit untuk dikerjakan dan saya membutuhkan ini dilakukan dengan cepat sehingga saya membuat kode di bawah ini. Cepat dan kotor (dan itu tidak mendukung properti) tetapi mudah-mudahan seseorang akan merasakan manfaatnya.

Ini kodenya:

        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";

Berikut ini sebuah contoh:

public interface ITestAdapter : IDisposable
{
    string TestMethod1();

    IEnumerable<string> TestMethod2(int a);

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

Kemudian buat kelas yang disebut LoggingTestAdapter yang mengimplementasikan ITestAdapter, dapatkan visual studio untuk secara otomatis mengimplementasikan semua metode dan kemudian jalankan melalui kode di atas. Anda kemudian harus memiliki sesuatu seperti ini:

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);
        }
    }
}

Ini dia dengan kode pendukung:

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);
        }
    }
}
JoeS
sumber