รูปแบบการออกแบบ C # สำหรับคนงานที่มีพารามิเตอร์อินพุตแตกต่างกัน


14

ฉันไม่แน่ใจว่ารูปแบบการออกแบบใดที่สามารถช่วยแก้ไขปัญหานี้ได้

ฉันมีคลาส 'ผู้ประสานงาน' ซึ่งกำหนดว่าควรใช้คลาสใดของผู้ปฏิบัติงานโดยไม่ต้องทราบเกี่ยวกับประเภทของแรงงานที่มีทั้งหมด - เพียงแค่เรียกใช้ WorkerFactory และดำเนินการตามอินเทอร์เฟซ IWorker ทั่วไป

จากนั้นตั้งค่าผู้ปฏิบัติงานที่เหมาะสมให้ทำงานและส่งคืนผลลัพธ์ของวิธี 'DoWork'

นี่เป็นเรื่องปกติ ... จนถึงตอนนี้; เรามีข้อกำหนดใหม่สำหรับคลาส Worker ใหม่ "WorkerB" ซึ่งต้องการข้อมูลเพิ่มเติมเช่นพารามิเตอร์อินพุตเพิ่มเติมเพื่อให้สามารถใช้งานได้

มันเหมือนกับว่าเราต้องการวิธีการทำงานหนักที่มีพารามิเตอร์อินพุตเพิ่มเติม ... แต่จากนั้นคนงานที่มีอยู่ทั้งหมดจะต้องใช้วิธีการนั้น - ซึ่งดูเหมือนว่าผิดเพราะคนงานเหล่านั้นไม่ต้องการวิธีการนั้นจริงๆ

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

มีคนงานที่มีอยู่จำนวนมากอยู่แล้ว

ฉันไม่ต้องการเปลี่ยนคนงานคอนกรีตที่มีอยู่เพื่อรองรับความต้องการของคลาส WorkerB ใหม่

ฉันคิดว่าบางทีรูปแบบของ Decorator น่าจะดีที่นี่ แต่ฉันไม่เคยเห็น Decorators ตกแต่งวัตถุด้วยวิธีเดียวกัน แต่ใช้พารามิเตอร์ที่แตกต่างกันมาก่อน ...

สถานการณ์ในรหัส:

public class Coordinator
{
    public string GetWorkerResult(string workerName, int a, List<int> b, string c)
    {
        var workerFactor = new WorkerFactory();
        var worker = workerFactor.GetWorker(workerName);

        if(worker!=null)
            return worker.DoWork(a, b);
        else
            return string.Empty;
    }
}

public class WorkerFactory
{
    public IWorker GetWorker(string workerName)
    {
        switch (workerName)
        {
            case "WorkerA":
                return new ConcreteWorkerA();
            case "WorkerB":
                return new ConcreteWorkerB();
            default:
                return null;
        }
    }
}

public interface IWorker
{
    string DoWork(int a, List<int> b);
}

public class ConcreteWorkerA : IWorker
{
    public string DoWork(int a, List<int> b)
    {
        // does the required work
        return "some A worker result";
    }
}

public class ConcreteWorkerB : IWorker
{
    public string DoWork(int a, List<int> b, string c)
    {
        // does some different work based on the value of 'c'
        return "some B worker result";
    }

    public string DoWork(int a, List<int> b)
    {
        // this method isn't really relevant to WorkerB as it is missing variable 'c'
        return "some B worker result";
    }    
}

เป็นIWorkerอินเตอร์เฟซที่จดทะเบียนรุ่นเก่าหรือว่าเป็นรุ่นใหม่ที่มีพารามิเตอร์เพิ่ม?
JamesFaix

สถานที่ในฐานรหัสของคุณที่กำลังใช้ IWorker โดยที่มีพารามิเตอร์ 2 ตัวจะต้องเสียบพารามิเตอร์ตัวที่ 3 หรือจะเป็นเพียงไซต์โทรใหม่เท่านั้นที่จะใช้พารามิเตอร์ตัวที่ 3?
JamesFaix

2
แทนที่จะไปซื้อของในรูปแบบให้ลองเน้นไปที่การออกแบบโดยรวมโดยไม่คำนึงว่าจะใช้รูปแบบนั้นหรือไม่ การอ่านที่แนะนำ: คำถามแบบ "ช็อปปิ้งสำหรับรูปแบบ" เลวแค่ไหน?

1
ตามรหัสของคุณคุณรู้อยู่แล้วว่าพารามิเตอร์ทั้งหมดที่จำเป็นก่อนที่จะสร้างอินสแตนซ์ของ IWorker ดังนั้นคุณควรส่งอาร์กิวเมนต์เหล่านี้ไปยัง Constructor และไม่ใช่ DoWork method IOW ใช้ประโยชน์จากคลาสโรงงานของคุณ การซ่อนรายละเอียดของการสร้างอินสแตนซ์นั้นเป็นเหตุผลหลักสำหรับการมีอยู่ของคลาสโรงงาน หากคุณใช้วิธีการนั้นวิธีการแก้ปัญหานั้นเล็กน้อย นอกจากนี้สิ่งที่คุณพยายามทำให้สำเร็จในแบบที่คุณกำลังพยายามทำให้สำเร็จคือ OO ที่ไม่ดี มันละเมิดหลักการทดแทน Liskov
Dunk

1
ฉันคิดว่าคุณต้องกลับไปอีกระดับ Coordinatorต้องมีการเปลี่ยนแปลงเพื่อรองรับพารามิเตอร์พิเศษนั้นในGetWorkerResultฟังก์ชั่นซึ่งหมายความว่าละเมิดหลักการเปิดของโซลิด ดังนั้นการเรียกรหัสทั้งหมดCoordinator.GetWorkerResultจึงต้องเปลี่ยนด้วย ลองดูสถานที่ที่คุณเรียกฟังก์ชั่นนั้น: คุณตัดสินใจได้อย่างไรว่า IWorker จะขออะไร นั่นอาจนำไปสู่ทางออกที่ดีกว่า
Bernhard Hiller

คำตอบ:


9

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

public interface IArgs
{
    //Can be empty
}

public interface IWorker
{
    string DoWork(IArgs args);
}

public class ConcreteArgsA : IArgs
{
    public int a;
    public List<int> b;
}

public class ConcreteArgsB : IArgs
{
    public int a;
    public List<int> b;
    public string c;
}

public class ConcreteWorkerA : IWorker
{
    public string DoWork(IArgs args)
    {
        var ConcreteArgs = args as ConcreteArgsA;
        if (args == null) throw new ArgumentException();
        return "some A worker result";
    }
}

public class ConcreteWorkerB : IWorker
{
    public string DoWork(IArgs args)
    {
        var ConcreteArgs = args as ConcreteArgsB;
        if (args == null) throw new ArgumentException();
        return "some B worker result";
    }
} 

หมายเหตุการตรวจสอบค่าว่าง ... เนื่องจากระบบของคุณมีความยืดหยุ่นและไม่ปลอดภัยจึงไม่ปลอดภัยดังนั้นคุณจะต้องตรวจสอบการส่งของคุณเพื่อให้แน่ใจว่าอาร์กิวเมนต์ที่ส่งผ่านนั้นถูกต้อง

หากคุณไม่ต้องการสร้างวัตถุที่เป็นรูปธรรมสำหรับการรวมกันที่เป็นไปได้ของการขัดแย้งคุณสามารถใช้tupleแทน (ไม่ใช่ตัวเลือกแรกของฉัน)

public string GetWorkerResult(string workerName, object args)
{
    var workerFactor = new WorkerFactory();
    var worker = workerFactor.GetWorker(workerName);

    if(worker!=null)
        return worker.DoWork(args);
    else
        return string.Empty;
}

//Sample call
var args = new Tuple<int, List<int>, string>(1234, 
                                             new List<int>(){1,2}, 
                                             "A string");    
GetWorkerResult("MyWorkerName", args);

1
สิ่งนี้คล้ายกับวิธีที่แอปพลิเคชัน Windows Forms จัดการกับกิจกรรม 1 พารามิเตอร์ "args" และพารามิเตอร์ "แหล่งที่มาของเหตุการณ์" "args" ทั้งหมดถูก subclassed จาก EventArgs: msdn.microsoft.com/en-us/library/ ...... -> ฉันว่ารูปแบบนี้ใช้งานได้ดีมาก ฉันไม่ชอบคำแนะนำ "Tuple"
Machado

if (args == null) throw new ArgumentException();ตอนนี้ผู้บริโภคของ IWorker ทุกคนต้องรู้รูปแบบที่เป็นรูปธรรม - และอินเทอร์เฟซนั้นไม่มีประโยชน์คุณสามารถกำจัดมันและใช้รูปแบบคอนกรีตแทนได้ และนั่นเป็นความคิดที่ไม่ดีใช่ไหม
Bernhard Hiller

จำเป็นต้องใช้อินเทอร์เฟซ IWorker เนื่องจากสถาปัตยกรรมแบบเสียบได้ ( WorkerFactory.GetWorkerสามารถมีประเภทส่งคืนได้เพียงประเภทเดียวเท่านั้น) ในขณะที่อยู่นอกขอบเขตของตัวอย่างนี้เรารู้ว่าผู้โทรมาด้วยworkerName; สันนิษฐานว่ามันสามารถเกิดขึ้นกับข้อโต้แย้งที่เหมาะสมเช่นกัน
John Wu

2

ฉันได้ออกแบบโซลูชันขึ้นใหม่ตามความคิดเห็นของ @ Dunk:

... คุณรู้พารามิเตอร์ทั้งหมดที่จำเป็นก่อนที่จะสร้างอินสแตนซ์ของ IWorker ดังนั้นคุณควรส่งอาร์กิวเมนต์เหล่านี้ไปยัง Constructor และไม่ใช่ DoWork method IOW ใช้ประโยชน์จากคลาสโรงงานของคุณ การซ่อนรายละเอียดของการสร้างอินสแตนซ์นั้นเป็นเหตุผลหลักสำหรับการมีอยู่ของคลาสโรงงาน

ดังนั้นฉันจึงเปลี่ยนอาร์กิวเมนต์ที่เป็นไปได้ทั้งหมดที่จำเป็นในการสร้าง IWorker เป็นวิธี IWorerFactory.GetWorker จากนั้นผู้ปฏิบัติงานแต่ละคนมีสิ่งที่ต้องการแล้วและผู้ประสานงานสามารถโทรหาผู้ปฏิบัติงานได้ ();

    public interface IWorkerFactory
    {
        IWorker GetWorker(string workerName, int a, List<int> b, string c);
    }

    public class WorkerFactory : IWorkerFactory
    {
        public IWorker GetWorker(string workerName, int a, List<int> b, string c)
        {
            switch (workerName)
            {
                case "WorkerA":
                    return new ConcreteWorkerA(a, b);
                case "WorkerB":
                    return new ConcreteWorkerB(a, b, c);
                default:
                    return null;
            }
        }
    }

    public class Coordinator
    {
        private readonly IWorkerFactory _workerFactory;

        public Coordinator(IWorkerFactory workerFactory)
        {
            _workerFactory = workerFactory;
        }

        // Adding 'c' breaks Open/Closed principal for the Coordinator and WorkerFactory; but this has to happen somewhere...
        public string GetWorkerResult(string workerName, int a, List<int> b, string c)
        {
            var worker = _workerFactory.GetWorker(workerName, a, b, c);

            if (worker != null)
                return worker.DoWork();
            else
                return string.Empty;
        }
    }

    public interface IWorker
    {
        string DoWork();
    }

    public class ConcreteWorkerA : IWorker
    {
        private readonly int _a;
        private readonly List<int> _b;

        public ConcreteWorkerA(int a, List<int> b)
        {
            _a = a;
            _b = b;
        }

        public string DoWork()
        {
            // does the required work based on 'a' and 'b'
            return "some A worker result";
        }
    }

    public class ConcreteWorkerB : IWorker
    {
        private readonly int _a;
        private readonly List<int> _b;
        private readonly string _c;

        public ConcreteWorkerB(int a, List<int> b, string c)
        {
            _a = a;
            _b = b;
            _c = c;
        }

        public string DoWork()
        {
            // does some different work based on the value of 'a', 'b' and 'c'
            return "some B worker result";
        }
    }

1
คุณมีวิธีการจากโรงงานที่รับ 3 พารามิเตอร์แม้ว่าจะไม่ได้ใช้ 3 ข้อในทุกสถานการณ์ คุณจะทำอย่างไรถ้าคุณมีวัตถุ C ที่ต้องการพารามิเตอร์มากขึ้น คุณจะเพิ่มพวกเขาไปยังลายเซ็นวิธีหรือไม่ การแก้ปัญหานี้ไม่สามารถขยายได้และไม่แนะนำ IMO
Amorphis

3
ถ้าฉันต้องการ ConcreteWorkerC ใหม่ที่ต้องการอาร์กิวเมนต์มากกว่าใช่แล้วพวกเขาจะถูกเพิ่มในวิธี GetWorker ใช่โรงงานไม่เป็นไปตามหลักการเปิด / ปิด - แต่บางสิ่งบางอย่างจะต้องเป็นเช่นนี้และโรงงานในความคิดของฉันเป็นตัวเลือกที่ดีที่สุด ข้อเสนอแนะของฉันคือ: แทนที่จะบอกว่าไม่เหมาะสมคุณจะช่วยชุมชนโดยโพสต์วิธีแก้ปัญหาอื่น ๆ แทน
JTech

1

ฉันจะแนะนำหนึ่งในหลายสิ่ง

หากคุณต้องการรักษาการห่อหุ้มเพื่อให้เว็บไซต์ไม่ต้องรู้อะไรเกี่ยวกับการทำงานภายในของคนงานหรือโรงงานคนงานคุณจะต้องเปลี่ยนอินเทอร์เฟซเพื่อให้มีพารามิเตอร์พิเศษ พารามิเตอร์สามารถมีค่าเริ่มต้นเพื่อให้บางไซต์ยังสามารถใช้ 2 พารามิเตอร์เท่านั้น สิ่งนี้จะทำให้ไลบรารีที่ใช้งานซ้ำได้รับการคอมไพล์ใหม่

ตัวเลือกอื่น ๆ ที่ฉันอยากจะแนะนำเพราะมันทำลาย encapsulation และเป็นเพียง OOP ที่ไม่ดี นอกจากนี้ยังต้องว่าอย่างน้อยคุณสามารถปรับเปลี่ยน callsites ConcreteWorkerBทั้งหมดสำหรับ คุณสามารถสร้างคลาสที่ใช้IWorkerอินเทอร์เฟซ แต่ยังมีDoWorkวิธีการที่มีพารามิเตอร์พิเศษ จากนั้นใน callsites ของคุณพยายามที่จะใช้IWorkerกับvar workerB = myIWorker as ConcreteWorkerB;และจากนั้นใช้พารามิเตอร์ทั้งสามDoWorkในประเภทที่เป็นรูปธรรม อีกครั้งนี่เป็นความคิดที่ไม่ดี แต่มันเป็นสิ่งที่คุณสามารถทำได้


0

@Jtech คุณได้พิจารณาการใช้paramsอาร์กิวเมนต์หรือไม่? สิ่งนี้อนุญาตให้พารามิเตอร์จำนวนตัวแปรถูกส่งผ่าน

https://msdn.microsoft.com/en-us/library/w5zay9db(v=vs.71).aspx


คำหลัก params อาจสมเหตุสมผลถ้าวิธี DoWork ทำสิ่งเดียวกันกับแต่ละอาร์กิวเมนต์และถ้าแต่ละอาร์กิวเมนต์เป็นประเภทเดียวกัน มิฉะนั้นวิธีการ DoWork จะต้องตรวจสอบว่าแต่ละอาร์กิวเมนต์ในอาร์เรย์ params เป็นประเภทที่ถูกต้อง - แต่ให้บอกว่าเรามีสองสายในนั้นและแต่ละคนถูกนำมาใช้เพื่อวัตถุประสงค์ที่แตกต่างกันอย่างไร DoWork สามารถมั่นใจได้ว่ามันถูกต้อง หนึ่ง ... มันจะต้องสมมติตามตำแหน่งในอาเรย์ ทั้งหมดหลวมเกินไปสำหรับความชอบของฉัน ฉันรู้สึกว่าทางออกของ @ JohnWu นั้นแน่นกว่า
JTech
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.