การสกัดกั้น vs การฉีด: การตัดสินใจด้านสถาปัตยกรรมของกรอบงาน


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 ( การเขียนโปรแกรมเชิงลักษณะ ) และโดยเฉพาะเข้าSpring.NET

เพื่อชี้แจง: คุณกำลังมองหาการเปรียบเทียบระหว่างการพึ่งพาการฉีดและการเขียนโปรแกรมเชิง Oriented, ถูกต้อง?
M.Babcock

@ M.Babcock ยังไม่ได้เห็นมันเป็นอย่างนั้น แต่ถูกต้อง

คำตอบ:


38

ข้อกังวลข้ามเช่นการบันทึกการแคชเป็นต้นไม่ใช่การพึ่งพาดังนั้นจึงไม่ควรถูกฉีดเข้าไปในบริการ อย่างไรก็ตามในขณะที่คนส่วนใหญ่แล้วดูเหมือนจะเข้าถึงสำหรับกรอบ AOP เต็ม interleaving มีรูปแบบการออกแบบที่ดีสำหรับการนี้: มัณฑนากร

ในตัวอย่างข้างต้นให้ MyService ใช้อินเตอร์เฟส IMyService:

public interface IMyService
{
    void DoSomething();
}

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

สิ่งนี้ทำให้คลาส MyService ปลอดจากข้อกังวลข้ามดังนั้นการปฏิบัติตามหลักการความรับผิดชอบเดียว (SRP)

หากต้องการใช้การบันทึกคุณสามารถเพิ่มตัวตกแต่งการบันทึกได้:

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
ลวดลายมัณฑนากรเป็นวิธีที่ดีในการแยกความกังวลเหล่านั้นออกจากกัน แต่ถ้าคุณมีบริการจำนวนมากนั่นคือสิ่งที่ฉันจะใช้เครื่องมือ AOP เช่น PostSharp หรือ Castle.DynamicProxy มิฉะนั้นสำหรับแต่ละอินเทอร์เฟซของคลาสบริการ และมัณฑนากรคนตัดไม้และนักตกแต่งภายในแต่ละคนอาจเป็นรหัสสำเร็จรูปที่คล้ายกันมาก (เช่นคุณได้รับการปรับปรุงแบบแยกส่วน / การห่อหุ้ม แต่คุณยังคงทำซ้ำตัวเองมาก)
Matthew Groves

4
ตกลง ฉันได้พูดคุยเมื่อปีที่แล้วซึ่งอธิบายวิธีย้ายจากนักตกแต่งไปยัง AOP: channel9.msdn.com/Events/GOTO/GOTO-2011-Copenhagen/ …
Mark Seemann

ฉันเขียนโปรแกรมใช้งานง่ายตามโปรแกรมนี้good.net/2015/09/08/DecoratorSpike.aspx
Dave Mateer

เราจะฉีดบริการและตกแต่งภายในด้วยการฉีดพึ่งพาได้อย่างไร
TIKSN

@TIKSN คำตอบสั้น ๆ คือที่แสดงข้างต้น อย่างไรก็ตามเมื่อคุณถามคุณจะต้องหาคำตอบจากสิ่งอื่น แต่ฉันไม่สามารถเดาได้ว่ามันคืออะไร คุณสามารถอธิบายรายละเอียดเพิ่มเติมหรืออาจถามคำถามใหม่ที่นี่ในเว็บไซต์?
Mark Seemann

6

สำหรับบริการจำนวนหนึ่งฉันคิดว่าคำตอบของ Mark นั้นดี: คุณไม่ต้องเรียนรู้หรือแนะนำการพึ่งพาบุคคลที่สามใหม่และคุณจะยังคงปฏิบัติตามหลักการ SOLID ที่ดี

สำหรับบริการจำนวนมากฉันขอแนะนำเครื่องมือ AOP เช่น PostSharp หรือ Castle DynamicProxy PostSharp มีรุ่นฟรี (เหมือนในเบียร์) และพวกเขาเพิ่งเปิดตัวPostSharp Toolkit สำหรับการวินิจฉัย (ฟรีเหมือนในเบียร์และคำพูด) ซึ่งจะให้คุณสมบัติการบันทึกบางอย่างนอกกรอบ


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

จากนั้นสร้างคลาสที่ชื่อว่า LoggingTestAdapter ซึ่งใช้ ITestAdapter รับ 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.