ฉันควรใช้วิธีการจากโรงงานแทนที่จะเป็นตัวสร้าง ฉันสามารถเปลี่ยนสิ่งนั้นและยังเข้ากันได้แบบย้อนหลังได้หรือไม่


15

ปัญหา

สมมติว่าฉันมีคลาสที่เรียกว่าDataSourceมีReadDataวิธี (และบางทีคนอื่น ๆ แต่ให้สิ่งที่ง่าย) เพื่ออ่านข้อมูลจาก.mdbไฟล์:

var source = new DataSource("myFile.mdb");
var data = source.ReadData();

ไม่กี่ปีต่อมาฉันตัดสินใจว่าฉันต้องการให้สามารถรองรับ.xmlไฟล์เพิ่มเติมจาก.mdbไฟล์เป็นแหล่งข้อมูล การนำไปใช้สำหรับ "การอ่านข้อมูล" นั้นค่อนข้างแตกต่างกันสำหรับ.xmlและ.mdbไฟล์ ดังนั้นถ้าฉันจะออกแบบระบบตั้งแต่เริ่มต้นฉันจะนิยามมันดังนี้:

abstract class DataSource {
    abstract Data ReadData();
    static DataSource OpenDataSource(string fileName) {
        // return MdbDataSource or XmlDataSource, as appropriate
    }
}

class MdbDataSource : DataSource {
    override Data ReadData() { /* implementation 1 */ }
}

class XmlDataSource : DataSource {
    override Data ReadData() { /* implementation 2 */ }
}

เยี่ยมมากการใช้รูปแบบวิธีการของโรงงานอย่างสมบูรณ์แบบ น่าเสียดายที่DataSourceตั้งอยู่ในห้องสมุดและเปลี่ยนรหัสใหม่เช่นนี้จะทำให้การโทรที่มีอยู่ทั้งหมดของ

var source = new DataSource("myFile.mdb");

ในไคลเอนต์ต่าง ๆ ที่ใช้ไลบรารี ทำไมฉันไม่ใช้วิธีการจากโรงงานในตอนแรก?


โซลูชั่น

นี่เป็นวิธีแก้ปัญหาที่ฉันสามารถทำได้:

  1. ทำให้ตัวสร้างแหล่งข้อมูลส่งคืนชนิดย่อย ( MdbDataSourceหรือXmlDataSource) นั่นจะแก้ปัญหาทั้งหมดของฉัน น่าเสียดายที่ C # ไม่รองรับสิ่งนั้น

  2. ใช้ชื่ออื่น:

    abstract class DataSourceBase { ... }    // corresponds to DataSource in the example above
    
    class DataSource : DataSourceBase {      // corresponds to MdbDataSource in the example above
        [Obsolete("New code should use DataSourceBase.OpenDataSource instead")]
        DataSource(string fileName) { ... }
        ...
    }
    
    class XmlDataSource : DataSourceBase { ... }

    นั่นคือสิ่งที่ฉันสิ้นสุดที่ใช้เพราะมันช่วยให้ย้อนกลับเข้ากันได้กับรหัส (เช่นการโทรไปnew DataSource("myFile.mdb")ยังที่ทำงาน) ข้อเสียเปรียบ: ชื่อไม่ได้อธิบายอย่างที่ควรจะเป็น

  3. สร้างDataSource"wrapper" สำหรับการนำไปใช้งานจริง:

    class DataSource {
        private DataSourceImpl impl;
    
        DataSource(string fileName) {
            impl = ... ? new MdbDataSourceImpl(fileName) : new XmlDataSourceImpl(fileName);
        }
    
        Data ReadData() {
            return impl.ReadData();
        }
    
        abstract private class DataSourceImpl { ... }
        private class MdbDataSourceImpl : DataSourceImpl { ... }
        private class XmlDataSourceImpl : DataSourceImpl { ... }
    }

    ข้อเสียเปรียบ: ทุกวิธีแหล่งข้อมูล (เช่นReadData) ต้องถูกกำหนดเส้นทางโดยรหัสสำเร็จรูป ฉันไม่ชอบรหัสสำเร็จรูป มันซ้ำซ้อนและ clutters รหัส

มีผู้ใดที่สง่างามวิธีการแก้ปัญหาที่ฉันได้พลาด?


5
คุณสามารถอธิบายปัญหาของคุณด้วย # 3 ในรายละเอียดเพิ่มเติมได้หรือไม่ นั่นดูสง่างามสำหรับฉัน (หรือเป็นสง่างามที่คุณได้รับในขณะที่รักษากันไปข้างหลัง.)
สาธารณรัฐประชาธิปไตยประชาชนลาว

ฉันจะกำหนดอินเทอร์เฟซที่เผยแพร่ API จากนั้นนำวิธีการที่มีอยู่มาใช้ใหม่โดยให้โรงงานสร้างตัวคลุมรอบโค้ดเก่าและอันใหม่ของคุณโดยให้โรงงานสร้างอินสแตนซ์ที่สอดคล้องกันของคลาสที่ใช้อินเทอร์เฟซ
โทมัส

@pdr: 1. ต้องมีการเปลี่ยนแปลงลายเซ็นของเมธอดทุกครั้งในที่เดียว 2. ฉันสามารถทำให้คลาส Impl ทั้งภายในและเป็นส่วนตัวซึ่งไม่สะดวกถ้าไคลเอนต์ต้องการเข้าถึงฟังก์ชันการทำงานเฉพาะที่มีอยู่ในเท่านั้นเช่นแหล่งข้อมูล Xml หรือฉันสามารถทำให้พวกเขาเป็นสาธารณะซึ่งหมายความว่าลูกค้ามีสองวิธีที่แตกต่างกันในการทำสิ่งเดียวกัน
Heinzi

2
@ Heinzi: ฉันชอบตัวเลือกที่ 3 นี่เป็นรูปแบบ "Facade" มาตรฐาน คุณควรตรวจสอบว่าคุณมีการมอบหมายวิธีการทุกแหล่งข้อมูลให้กับการนำไปใช้จริงหรือเพียงบางวิธี บางทียังมีรหัสทั่วไปบางอย่างที่ยังคงอยู่ใน "แหล่งข้อมูล"?
Doc Brown

มันเป็นความอัปยศที่newไม่ใช่วิธีการของวัตถุคลาส (เพื่อให้คุณสามารถคลาสย่อยตัวเอง - เทคนิคที่รู้จักกันในชื่อ metaclasses - และควบคุมสิ่งที่newเกิดขึ้นจริง) แต่นั่นไม่ใช่วิธีการทำงานของ C # (หรือ Java หรือ C ++)
Donal Fellows

คำตอบ:


12

ฉันจะไปสำหรับตัวแปรตัวเลือกที่สองของคุณที่ช่วยให้คุณระยะออกเก่าเกินไปทั่วไปชื่อDataSource:

abstract class AbstractDataSource { ... } // corresponds to the abstract DataSource in the ideal solution

class XmlDataSource : AbstractDataSource { ... }
class MdbDataSource : AbstractDataSource { ... } // contains all the code of the existing DataSource class

[Obsolete("New code should use AbstractDataSource instead")]
class DataSource : MdbDataSource { // an 'empty shell' to keep old code working.
    DataSource(string fileName) { ... }
}

ข้อเสียเปรียบเพียงอย่างเดียวที่นี่คือคลาสฐานใหม่ไม่สามารถมีชื่อที่ชัดเจนที่สุดได้เนื่องจากชื่อนั้นได้รับการอ้างสิทธิ์แล้วสำหรับคลาสดั้งเดิม คลาสอื่น ๆ ทั้งหมดมีชื่อที่สื่อความหมาย


1
+1 นั่นคือสิ่งที่อยู่ในใจของฉันเมื่อฉันอ่านคำถาม แม้ว่าฉันจะชอบตัวเลือก 3 ของ OP เพิ่มเติม
Doc Brown

คลาสฐานอาจมีชื่อที่ชัดเจนที่สุดหากวางโค้ดใหม่ทั้งหมดลงในเนมสเปซใหม่ แต่ฉันไม่แน่ใจว่าเป็นความคิดที่ดี
svick

คลาสฐานควรมีคำต่อท้าย "ฐาน" คลาส DataSourceBase
Stephen

6

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

class DataSource {
    private Reader reader;

    DataSource(string fileName) {
        reader = ... ? new MdbReader(fileName) : new XmlReader(fileName);
    }

    Data ReadData() {
        return reader.next();
    }

    abstract private class Reader { ... }
    private class MdbReader : Reader { ... }
    private class XmlReader : Reader { ... }
}

วิธีนี้คุณหลีกเลี่ยงรหัสที่ซ้ำกันและเปิดให้มีการขยายเพิ่มเติม


+1 ตัวเลือกที่ดี ฉันยังคงต้องการตัวเลือกที่ 2 แต่เนื่องจากมันช่วยให้ฉันเพื่อเข้าถึงฟังก์ชันการทำงาน XmlDataSource เฉพาะโดยชัดเจน XmlDataSourceinstantiating
Heinzi
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.