วิธีการใช้รูปแบบวิธีการจากโรงงานใน C ++ อย่างถูกต้อง


329

มีสิ่งหนึ่งใน C ++ ที่ทำให้ฉันรู้สึกไม่สบายตัวเป็นเวลานานเพราะฉันไม่รู้วิธีการทำอย่างสุจริตแม้ว่ามันจะฟังดูง่าย:

ฉันจะใช้วิธีการโรงงานใน C ++ ได้อย่างถูกต้องได้อย่างไร

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

โดย "รูปแบบวิธีการของโรงงาน" ฉันหมายถึงทั้งวิธีคงที่จากโรงงานภายในวัตถุหรือวิธีที่กำหนดไว้ในคลาสอื่นหรือฟังก์ชั่นระดับโลก เพียงแค่โดยทั่วไป "แนวคิดของการเปลี่ยนเส้นทางวิธีปกติของการสร้างอินสแตนซ์ของคลาส X ไปที่อื่นนอกเหนือจากตัวสร้าง"

ให้ฉันอ่านคำตอบที่เป็นไปได้ที่ฉันคิด


0) อย่าสร้างโรงงานสร้างสิ่งก่อสร้าง

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

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

struct Vec2 {
    Vec2(float x, float y);
    Vec2(float angle, float magnitude); // not a valid overload!
    // ...
};

วิธีคิดตามธรรมชาติของฉันคือ:

struct Vec2 {
    static Vec2 fromLinear(float x, float y);
    static Vec2 fromPolar(float angle, float magnitude);
    // ...
};

ซึ่งแทนที่ constructors ทำให้ฉันใช้วิธีการคงที่จากโรงงาน ... ซึ่งหมายความว่าฉันใช้รูปแบบโรงงานในทางใดทางหนึ่ง ("ชั้นกลายเป็นโรงงานของตัวเอง") นี่ดูดี (และจะเหมาะกับกรณีนี้) แต่ล้มเหลวในบางกรณีซึ่งฉันจะอธิบายในจุดที่ 2 อ่านต่อ

อีกกรณีหนึ่ง: พยายามโอเวอร์โหลดโดย typedef ทึบสองตัวของ API บางตัว (เช่น GUID ของโดเมนที่ไม่เกี่ยวข้องกัน, หรือ GUID และ bitfield), พิมพ์ semantically ต่างกันโดยสิ้นเชิง (ดังนั้น - ในทางทฤษฎี - overloads ที่ถูกต้อง) สิ่งเดียวกัน - เช่น ints ที่ไม่ได้ลงชื่อหรือตัวชี้โมฆะ


1) วิธี Java

Java ทำได้ง่ายเนื่องจากเรามีเพียงวัตถุที่จัดสรรแบบไดนามิก การทำให้โรงงานมีความสำคัญเหมือน:

class FooFactory {
    public Foo createFooInSomeWay() {
        // can be a static method as well,
        //  if we don't need the factory to provide its own object semantics
        //  and just serve as a group of methods
        return new Foo(some, args);
    }
}

ใน C ++ สิ่งนี้แปลเป็น:

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
};

เย็น? บ่อยครั้งแน่นอน แต่ - สิ่งนี้บังคับให้ผู้ใช้ใช้การจัดสรรแบบไดนามิกเท่านั้น การจัดสรรแบบคงที่คือสิ่งที่ทำให้ C ++ ซับซ้อน แต่ก็เป็นสิ่งที่ทำให้มีประสิทธิภาพ นอกจากนี้ฉันเชื่อว่ามีเป้าหมายบางอย่าง (คำหลัก: ฝังตัว) ซึ่งไม่อนุญาตให้มีการจัดสรรแบบไดนามิก และนั่นไม่ได้หมายความว่าผู้ใช้แพลตฟอร์มเหล่านั้นชอบเขียน OOP ที่สะอาด

อย่างไรก็ตามปรัชญากัน: ในกรณีทั่วไปฉันไม่ต้องการบังคับให้ผู้ใช้ของโรงงานถูก จำกัด การจัดสรรแบบไดนามิก


2) ผลตอบแทนต่อมูลค่า

ตกลงเรารู้ว่า 1) ยอดเยี่ยมเมื่อเราต้องการการจัดสรรแบบไดนามิก ทำไมเราจะไม่เพิ่มการจัดสรรแบบคงที่ด้านบนของสิ่งนั้น?

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooInSomeWay() {
        return Foo(some, args);
    }
};

อะไร? เราไม่สามารถโอเวอร์โหลดตามประเภทการคืนได้หรือไม่ โอ้แน่นอนเราทำไม่ได้ ดังนั้นขอเปลี่ยนชื่อเมธอดเพื่อสะท้อนสิ่งนั้น และใช่ฉันได้เขียนตัวอย่างโค้ดที่ไม่ถูกต้องด้านบนเพื่อเน้นว่าฉันไม่ชอบความต้องการเปลี่ยนชื่อเมธอดมากแค่ไหนเนื่องจากเราไม่สามารถใช้การออกแบบโรงงานผู้ไม่เชื่อเรื่องภาษาได้อย่างถูกต้องเนื่องจากเราต้องเปลี่ยนชื่อ - และ ผู้ใช้ทุกคนของรหัสนี้จะต้องจำความแตกต่างของการใช้งานจากข้อกำหนดที่

class FooFactory {
public:
    Foo* createDynamicFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooObjectInSomeWay() {
        return Foo(some, args);
    }
};

ตกลง ... เรามีมัน มันน่าเกลียดเพราะเราต้องเปลี่ยนชื่อวิธี มันไม่สมบูรณ์เนื่องจากเราจำเป็นต้องเขียนรหัสเดียวกันสองครั้ง แต่เมื่อเสร็จแล้วก็ใช้งานได้ ขวา?

ปกติแล้ว แต่บางครั้งมันก็ไม่ได้ เมื่อสร้าง Foo เราต้องพึ่งพาคอมไพเลอร์เพื่อทำการเพิ่มประสิทธิภาพค่าส่งคืนสำหรับเราเนื่องจากมาตรฐาน C ++ นั้นมีความสำคัญมากพอสำหรับผู้จำหน่ายคอมไพเลอร์ที่จะไม่ระบุว่าเมื่อใดที่วัตถุจะถูกสร้างขึ้นและจะถูกคัดลอกเมื่อส่งคืน วัตถุชั่วคราวโดยค่าใน C ++ ดังนั้นหาก Foo มีราคาแพงในการคัดลอกแนวทางนี้มีความเสี่ยง

และจะทำอย่างไรถ้า Foo นั้นไม่สามารถจัดการได้เลย? เอ่อ ( โปรดทราบว่าใน C ++ 17 พร้อมการคัดลอกการรับประกันการทำสำเนาไม่ถูกต้องไม่มีปัญหาสำหรับรหัสด้านบนอีกต่อไป )

สรุป: การสร้างโรงงานโดยส่งคืนวัตถุนั้นเป็นวิธีแก้ปัญหาสำหรับบางกรณี (เช่นเวกเตอร์ 2 มิติที่กล่าวถึงก่อนหน้านี้) แต่ก็ยังไม่ใช่การทดแทนโดยทั่วไปสำหรับตัวสร้าง


3) การก่อสร้างสองเฟส

อีกสิ่งที่ใครบางคนอาจจะเกิดขึ้นก็คือการแยกปัญหาของการจัดสรรวัตถุและการเริ่มต้น ซึ่งมักจะส่งผลในรหัสเช่นนี้:

class Foo {
public:
    Foo() {
        // empty or almost empty
    }
    // ...
};

class FooFactory {
public:
    void createFooInSomeWay(Foo& foo, some, args);
};

void clientCode() {
    Foo staticFoo;
    auto_ptr<Foo> dynamicFoo = new Foo();
    FooFactory factory;
    factory.createFooInSomeWay(&staticFoo);
    factory.createFooInSomeWay(&dynamicFoo.get());
    // ...
}

บางคนอาจคิดว่ามันใช้งานได้อย่างมีเสน่ห์ ราคาเดียวที่เราจ่ายในรหัสของเรา ...

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

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

ต้องลดการประชุมและเปลี่ยนการออกแบบวัตถุของฉันเพียงเพื่อวัตถุประสงค์ในการทำให้โรงงานของมันคือ .. ดีอุ้ยอ้าย

ฉันรู้ว่าข้างต้นจะไม่โน้มน้าวใจใครหลายคนดังนั้นฉันจะให้ข้อโต้แย้งที่มั่นคงมากขึ้น เมื่อใช้การก่อสร้างแบบสองเฟสคุณจะไม่สามารถ:

  • เริ่มต้นconstหรือตัวแปรสมาชิกอ้างอิง
  • ส่งอาร์กิวเมนต์ไปยังตัวสร้างคลาสพื้นฐานและตัวสร้างวัตถุสมาชิก

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

ดังนั้น: ไม่ใกล้กับโซลูชันทั่วไปที่ดีสำหรับการใช้งานโรงงาน


สรุป:

เราต้องการมีวิธีสร้างอินสแตนซ์ของวัตถุซึ่งจะ:

  • อนุญาตให้มีการสร้างอินสแตนซ์เครื่องแบบโดยไม่คำนึงถึงการจัดสรร
  • ให้ชื่อที่แตกต่างและมีความหมายกับวิธีการก่อสร้าง (ดังนั้นไม่ต้องพึ่งพาการโต้แย้งมากไป)
  • ไม่แนะนำการโจมตีที่มีประสิทธิภาพอย่างมากและโดยเฉพาะอย่างยิ่งการเผยแพร่โค้ดที่มีความหมายโดยเฉพาะที่ฝั่งไคลเอ็นต์
  • ทั่วไปใน: เป็นไปได้ที่จะได้รับการแนะนำสำหรับชั้นเรียนใด ๆ

ฉันเชื่อว่าฉันได้พิสูจน์แล้วว่าวิธีการที่กล่าวถึงไม่เป็นไปตามข้อกำหนดเหล่านั้น

คำใบ้ใด ๆ โปรดให้วิธีการแก้ปัญหาแก่ฉันฉันไม่ต้องการคิดว่าภาษานี้จะไม่อนุญาตให้ฉันนำแนวคิดดังกล่าวไปใช้อย่างไม่เหมาะสม


7
@Zac ถึงแม้ว่าชื่อจะคล้ายกันมาก แต่คำถามที่เกิดขึ้นจริงนั้นแตกต่างกันไปตาม IMHO
PéterTörök

2
ดีที่ซ้ำกัน แต่ข้อความของนี้เป็นคำถามที่มีคุณค่าในตัวของมันเอง
dmckee --- ผู้ดูแลอดีตลูกแมว

7
สองปีหลังจากถามสิ่งนี้ฉันมีบางประเด็นที่ต้องเพิ่ม: 1)คำถามนี้เกี่ยวข้องกับรูปแบบการออกแบบ ([นามธรรม] โรงงานผู้สร้างคุณชื่อมันฉันไม่ชอบการขุดในอนุกรมวิธานของพวกเขา) 2)ปัญหาจริงที่ถูกกล่าวถึงในที่นี้คือ "จะแยกการจัดสรรพื้นที่เก็บข้อมูลวัตถุออกจากการสร้างวัตถุได้อย่างไร
คอส

1
@Dennis: เฉพาะในกรณีที่คุณทำไม่deleteได้ วิธีการเหล่านี้ใช้ได้อย่างสมบูรณ์ตราบใดที่มันเป็น "เอกสาร" (ซอร์สโค้ดคือเอกสาร ;-)) ที่ผู้เรียกใช้เป็นเจ้าของตัวชี้ (อ่าน: มีหน้าที่รับผิดชอบในการลบเมื่อเหมาะสม)
Boris Dalstein

1
@Boris @Dennis คุณยังสามารถทำให้มันชัดเจนมากโดยกลับแทนunique_ptr<T> T*
Kos

คำตอบ:


107

ประการแรกมีกรณีที่การก่อสร้างวัตถุเป็นงานที่ซับซ้อนพอที่จะพิสูจน์การแยกไปยังชั้นอื่น

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

Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!

มีวิธีแก้ปัญหาง่าย ๆ สำหรับสิ่งนี้:

struct Cartesian {
  inline Cartesian(float x, float y): x(x), y(y) {}
  float x, y;
};
struct Polar {
  inline Polar(float angle, float magnitude): angle(angle), magnitude(magnitude) {}
  float angle, magnitude;
};
Vec2(const Cartesian &cartesian);
Vec2(const Polar &polar);

ข้อเสียเดียวคือมันดู verbose เล็กน้อย:

Vec2 v2(Vec2::Cartesian(3.0f, 4.0f));

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

สำหรับประเภทการจัดสรรเหตุผลหลักในการใช้รูปแบบโรงงานมักจะมีความหลากหลาย คอนสตรัคเตอร์ไม่สามารถเป็นเสมือนจริงและแม้ว่าพวกเขาจะทำได้มันก็ไม่สมเหตุสมผลนัก เมื่อใช้การจัดสรรแบบสแตติกหรือสแต็กคุณไม่สามารถสร้างวัตถุในแบบ polymorphic ได้เนื่องจากคอมไพเลอร์จำเป็นต้องรู้ขนาดที่แน่นอน ดังนั้นจึงใช้ได้กับพอยน์เตอร์และการอ้างอิงเท่านั้น และการส่งคืนการอ้างอิงจากโรงงานก็ใช้งานไม่ได้เช่นกันเพราะในขณะที่วัตถุสามารถลบโดยการอ้างอิงได้อาจทำให้เกิดความสับสนและข้อผิดพลาดได้ง่ายดูการฝึกปฏิบัติในการส่งคืนตัวแปรอ้างอิง C ++ ใช่ไหม?ตัวอย่างเช่น. ดังนั้นพอยน์เตอร์จึงเป็นสิ่งเดียวที่เหลืออยู่และนั่นก็รวมถึงพอยน์เตอร์อัจฉริยะเช่นกัน กล่าวอีกนัยหนึ่งโรงงานจะมีประโยชน์มากที่สุดเมื่อใช้กับการจัดสรรแบบไดนามิกดังนั้นคุณสามารถทำสิ่งนี้:

class Abstract {
  public:
    virtual void do() = 0;
};

class Factory {
  public:
    Abstract *create();
};

Factory f;
Abstract *a = f.create();
a->do();

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


21
+1 สำหรับโครงสร้างคาร์ทีเซียนและขั้วโลก เป็นการดีที่สุดในการสร้างคลาสและโครงสร้างซึ่งแสดงข้อมูลโดยตรงที่พวกเขามีจุดประสงค์เพื่อ (ตรงข้ามกับโครงสร้าง Vec ทั่วไป) โรงงานของคุณเป็นตัวอย่างที่ดีเช่นกัน แต่ตัวอย่างของคุณไม่แสดงให้เห็นว่าใครเป็นเจ้าของตัวชี้ 'a' หาก Factory 'f' เป็นเจ้าของมันอาจจะถูกทำลายเมื่อ 'f' ออกจากขอบเขต แต่ถ้า 'f' ไม่ได้เป็นเจ้าของมันเป็นสิ่งสำคัญสำหรับนักพัฒนาที่ต้องจำหน่วยความจำนั้นให้ว่าง เกิดขึ้น
David Peterson

1
แน่นอนว่าวัตถุสามารถลบได้โดยการอ้างอิง! ดูstackoverflow.com/a/752699/404734แน่นอนว่าทำให้เกิดคำถามหากฉลาดในการส่งคืนหน่วยความจำแบบไดนามิกโดยการอ้างอิงเนื่องจากปัญหาของการกำหนดค่าส่งคืนโดยการคัดลอก (ผู้โทรสามารถทำบางสิ่งได้เช่นกัน ชอบ int a = * returnAPoninterToInt () และจากนั้นจะเผชิญกับปัญหาเดียวกันหากหน่วยความจำที่ได้รับการจัดสรรแบบไดนามิกได้รับคืนเช่นการอ้างอิง แต่ในรุ่นพอยน์เตอร์ผู้ใช้จะต้องอ้างอิงอย่างชัดเจนแทนที่จะลืมอ้างอิงอย่างชัดเจนผิด .
Kaiserludi

1
@ Kaiserludi จุดดี ฉันไม่ได้คิดอย่างนั้น แต่มันก็ยังเป็นวิธีที่ "ชั่วร้าย" ในการทำสิ่งต่าง ๆ แก้ไขคำตอบของฉันเพื่อสะท้อนว่า
Sergei Tachenov

แล้วการสร้างคลาสที่ไม่ใช่ polymorphic ที่ไม่เปลี่ยนรูปนั้นล่ะ รูปแบบของโรงงานนั้นเหมาะสมที่จะใช้ใน C ++ หรือไม่
daaxix

@daaxix ทำไมคุณต้องใช้โรงงานในการสร้างอินสแตนซ์ของคลาสที่ไม่ใช่ polymorphic ฉันไม่เห็นสิ่งที่ไม่สามารถเปลี่ยนแปลงได้นั้นเกี่ยวกับเรื่องนี้
Sergei Tachenov

49

ตัวอย่างง่ายๆจากโรงงาน:

// Factory returns object and ownership
// Caller responsible for deletion.
#include <memory>
class FactoryReleaseOwnership{
  public:
    std::unique_ptr<Foo> createFooInSomeWay(){
      return std::unique_ptr<Foo>(new Foo(some, args));
    }
};

// Factory retains object ownership
// Thus returning a reference.
#include <boost/ptr_container/ptr_vector.hpp>
class FactoryRetainOwnership{
  boost::ptr_vector<Foo>  myFoo;
  public:
    Foo& createFooInSomeWay(){
      // Must take care that factory last longer than all references.
      // Could make myFoo static so it last as long as the application.
      myFoo.push_back(new Foo(some, args));
      return myFoo.back();
    }
};

2
@ LokiAstari เนื่องจากการใช้ตัวชี้อัจฉริยะเป็นวิธีที่ง่ายที่สุดในการลดการควบคุมหน่วยความจำ การควบคุมว่า C / C ++ langs เป็นที่รู้จักกันดีที่สุดเมื่อเทียบกับภาษาอื่น ๆ และจากการที่พวกเขาได้รับประโยชน์มากที่สุด ไม่พูดถึงความจริงที่ว่าพอยน์เตอร์อัจฉริยะผลิตโอเวอร์เฮดของหน่วยความจำคล้ายกับภาษาที่มีการจัดการอื่น ๆ หากคุณต้องการความสะดวกสบายของการจัดการหน่วยความจำอัตโนมัติเริ่มต้นการเขียนโปรแกรมใน Java หรือ C # แต่อย่าทำให้เกิดความสับสนใน C / C ++
ลุค 1985

45
@ lukasz1985 unique_ptrในตัวอย่างนั้นไม่มีค่าใช้จ่ายด้านประสิทธิภาพ การจัดการทรัพยากรรวมถึงหน่วยความจำเป็นหนึ่งในข้อดีที่สุดของ C ++ เหนือภาษาอื่น ๆ เพราะคุณสามารถทำได้โดยไม่ต้องเสียค่าปรับประสิทธิภาพและกำหนดค่าได้โดยไม่สูญเสียการควบคุม แต่คุณพูดตรงกันข้าม บางคนไม่ชอบสิ่งที่ C ++ ทำโดยปริยายเช่นการจัดการหน่วยความจำผ่านพอยน์เตอร์อัจฉริยะ แต่ถ้าคุณต้องการให้ทุกอย่างกระจ่างแจ้งให้ใช้ C; ข้อเสียคือคำสั่งที่มีปัญหาน้อยลง ฉันคิดว่ามันไม่ยุติธรรมเลยที่คุณจะโหวตข้อเสนอแนะที่ดี
TheCppZoo

1
@EmMaster: ฉันไม่ได้ตอบสนองก่อนหน้านี้เพราะเห็นได้ชัดว่าเขาหลอก โปรดอย่าป้อนโทรลล์
Martin York

17
@ LokiAstari เขาอาจจะหมุนรอบ แต่สิ่งที่เขาพูดอาจทำให้ผู้คนสับสน
TheCppZoo

1
@yau: ใช่ แต่: boost::ptr_vector<>มีประสิทธิภาพมากกว่านิดหน่อยเพราะเข้าใจว่ามันเป็นเจ้าของตัวชี้แทนที่จะมอบหมายงานให้กับคลาสย่อย แต่ข้อได้เปรียบหลักของboost::ptr_vector<>มันคือการเปิดเผยสมาชิกโดยอ้างอิง (ไม่ใช่ตัวชี้) ดังนั้นจึงเป็นเรื่องง่ายที่จะใช้กับอัลกอริทึมในไลบรารีมาตรฐาน
Martin York

41

คุณเคยคิดที่จะไม่ใช้โรงงานหรือเปล่าและใช้ประโยชน์จากระบบการพิมพ์แทน ฉันนึกถึงสองแนวทางที่แตกต่างกันซึ่งทำสิ่งนี้:

ตัวเลือกที่ 1:

struct linear {
    linear(float x, float y) : x_(x), y_(y){}
    float x_;
    float y_;
};

struct polar {
    polar(float angle, float magnitude) : angle_(angle),  magnitude_(magnitude) {}
    float angle_;
    float magnitude_;
};


struct Vec2 {
    explicit Vec2(const linear &l) { /* ... */ }
    explicit Vec2(const polar &p) { /* ... */ }
};

ซึ่งให้คุณเขียนสิ่งต่าง ๆ เช่น:

Vec2 v(linear(1.0, 2.0));

ตัวเลือก 2:

คุณสามารถใช้ "แท็ก" อย่างที่ STL ทำกับตัววนซ้ำและเช่นนั้น ตัวอย่างเช่น:

struct linear_coord_tag linear_coord {}; // declare type and a global
struct polar_coord_tag polar_coord {};

struct Vec2 {
    Vec2(float x, float y, const linear_coord_tag &) { /* ... */ }
    Vec2(float angle, float magnitude, const polar_coord_tag &) { /* ... */ }
};

วิธีที่สองนี้ให้คุณเขียนโค้ดที่มีลักษณะดังนี้:

Vec2 v(1.0, 2.0, linear_coord);

ซึ่งก็ดีและแสดงออกในขณะที่ให้คุณมีต้นแบบเฉพาะสำหรับตัวสร้างแต่ละตัว


29

คุณสามารถอ่านวิธีแก้ปัญหาที่ดีมากใน: http://www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus

ทางออกที่ดีที่สุดคือ "ความคิดเห็นและการอภิปราย" ดู "ไม่จำเป็นต้องมีวิธีการสร้างแบบคงที่"

จากความคิดนี้ฉันได้ทำโรงงาน โปรดทราบว่าฉันกำลังใช้ Qt แต่คุณสามารถเปลี่ยน QMap และ QString สำหรับ std ที่เทียบเท่าได้

#ifndef FACTORY_H
#define FACTORY_H

#include <QMap>
#include <QString>

template <typename T>
class Factory
{
public:
    template <typename TDerived>
    void registerType(QString name)
    {
        static_assert(std::is_base_of<T, TDerived>::value, "Factory::registerType doesn't accept this type because doesn't derive from base class");
        _createFuncs[name] = &createFunc<TDerived>;
    }

    T* create(QString name) {
        typename QMap<QString,PCreateFunc>::const_iterator it = _createFuncs.find(name);
        if (it != _createFuncs.end()) {
            return it.value()();
        }
        return nullptr;
    }

private:
    template <typename TDerived>
    static T* createFunc()
    {
        return new TDerived();
    }

    typedef T* (*PCreateFunc)();
    QMap<QString,PCreateFunc> _createFuncs;
};

#endif // FACTORY_H

ตัวอย่างการใช้งาน:

Factory<BaseClass> f;
f.registerType<Descendant1>("Descendant1");
f.registerType<Descendant2>("Descendant2");
Descendant1* d1 = static_cast<Descendant1*>(f.create("Descendant1"));
Descendant2* d2 = static_cast<Descendant2*>(f.create("Descendant2"));
BaseClass *b1 = f.create("Descendant1");
BaseClass *b2 = f.create("Descendant2");

17

ฉันส่วนใหญ่เห็นด้วยกับคำตอบที่ยอมรับ แต่มีตัวเลือก C ++ 11 ที่ไม่ได้ครอบคลุมในคำตอบที่มีอยู่:

  • ส่งคืนผลลัพธ์วิธีการจากโรงงานด้วยค่าและ
  • ให้ราคาถูกคอนสตรัคย้าย

ตัวอย่าง:

struct sandwich {
  // Factory methods.
  static sandwich ham();
  static sandwich spam();
  // Move constructor.
  sandwich(sandwich &&);
  // etc.
};

จากนั้นคุณสามารถสร้างวัตถุบนสแต็ก:

sandwich mine{sandwich::ham()};

เป็น subobjects ของสิ่งอื่น ๆ :

auto lunch = std::make_pair(sandwich::spam(), apple{});

หรือจัดสรรแบบไดนามิก:

auto ptr = std::make_shared<sandwich>(sandwich::ham());

ฉันจะใช้สิ่งนี้เมื่อใด

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

ฉันบอกว่า ' อาจ ' เพราะมันขึ้นอยู่กับวิธีการที่ให้โค้ดที่ชัดเจนที่สุดโดยไม่ไร้ประสิทธิภาพโดยไม่จำเป็น


1
ฉันใช้สิ่งนี้อย่างกว้างขวางเมื่อห่อทรัพยากร OpenGL ลบตัวสร้างสำเนาและคัดลอกการกำหนดให้บังคับใช้การย้ายความหมาย ฉันสร้างวิธีการแบบสแตติกจำนวนมากจากโรงงานเพื่อสร้างทรัพยากรแต่ละประเภท นี่เป็นสิ่งที่อ่านได้ง่ายกว่า OpenGL ของการส่งรันไทม์แบบอิง enum ซึ่งมักจะมีพารามิเตอร์ฟังก์ชันที่ซ้ำซ้อนขึ้นอยู่กับ enum ที่ส่งผ่าน มันเป็นรูปแบบที่มีประโยชน์มากแปลกใจที่คำตอบนี้ไม่ได้สูงขึ้น
Fibbles

11

โลกิมีทั้งโรงงานวิธีการและโรงงานบทคัดย่อ ทั้งสองมีเอกสาร (อย่างกว้างขวาง) ในการออกแบบ C ++ สมัยใหม่โดย Andei Alexandrescu วิธีการจากโรงงานอาจใกล้เคียงกับสิ่งที่คุณดูเหมือนจะเป็นมาแม้ว่ามันจะยังคงแตกต่างกันเล็กน้อย (อย่างน้อยถ้าหน่วยความจำทำหน้าที่คุณต้องลงทะเบียนประเภทก่อนที่โรงงานจะสามารถสร้างวัตถุประเภทนั้นได้)


1
แม้ว่ามันจะล้าสมัย (ซึ่งฉันโต้แย้ง) มันยังคงเป็นประโยชน์อย่างสมบูรณ์ ฉันยังคงใช้ Factory ที่ใช้ MC ++ D ในโครงการ C ++ 14 ใหม่เพื่อผลที่ยอดเยี่ยม! ยิ่งไปกว่านั้นรูปแบบ Factory และ Singleton อาจเป็นชิ้นส่วนที่ล้าสมัยที่สุด ในขณะที่ชิ้นส่วนของโลกิชอบFunctionและประเภทการเปลี่ยนรูปแบบสามารถถูกแทนที่ด้วยstd::functionและ<type_traits>ในขณะที่ lambdas การทำเกลียวการอ้างอิงค่า rvalue มีผลกระทบที่อาจต้องใช้การปรับแต่งเล็กน้อยบางอย่าง
โลหะ

5

ฉันไม่พยายามตอบคำถามทั้งหมดของฉันเพราะฉันเชื่อว่ากว้างเกินไป เพียงไม่กี่บันทึกย่อ:

มีบางกรณีที่การสร้างวัตถุเป็นงานที่ซับซ้อนมากพอที่จะแสดงให้เห็นถึงการแยกไปยังชั้นอื่น

ในความเป็นจริงแล้วคลาสนั้นเป็นผู้สร้างมากกว่าโรงงาน

ในกรณีทั่วไปฉันไม่ต้องการบังคับให้ผู้ใช้ของโรงงานถูก จำกัด การจัดสรรแบบไดนามิก

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

นอกจากนี้ยังช่วยลดปัญหาที่เกี่ยวข้องกับการส่งคืนตามค่า

สรุป: การสร้างโรงงานโดยส่งคืนวัตถุนั้นเป็นวิธีแก้ปัญหาสำหรับบางกรณี (เช่นเวกเตอร์ 2 มิติที่กล่าวถึงก่อนหน้านี้) แต่ก็ยังไม่ใช่การทดแทนโดยทั่วไปสำหรับตัวสร้าง

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

หากคุณอยู่หลังการดำเนินการโรงงาน "สมบูรณ์แบบ" ขอให้โชคดี


ขอบคุณสำหรับคำตอบ! แต่คุณสามารถอธิบายได้ว่าการใช้ตัวชี้สมาร์ทจะปล่อยข้อ จำกัด ของการจัดสรรแบบไดนามิกได้อย่างไร ฉันไม่ได้รับส่วนนี้มากนัก
Kos

@Kos พร้อมด้วยสมาร์ทพอยน์เตอร์คุณสามารถซ่อนการจัดสรร / การจัดสรรคืนของวัตถุจริงจากผู้ใช้ของคุณ พวกเขาเห็นเพียงตัวชี้สมาร์ท encapsulating ซึ่งไปยังโลกภายนอกทำตัวเหมือนวัตถุที่จัดสรรแบบคง
PéterTörök

@ คอสไม่ได้อยู่ในความหมายที่เข้มงวด AFAIR คุณผ่านวัตถุที่จะห่อซึ่งคุณอาจได้รับการจัดสรรแบบไดนามิกในบางจุด จากนั้นตัวชี้สมาร์ทจะเป็นเจ้าของมันและทำให้แน่ใจว่ามันถูกทำลายอย่างถูกต้องเมื่อไม่ต้องการอีกต่อไป (เวลาที่ตัดสินใจแตกต่างกันไปสำหรับตัวชี้สมาร์ทประเภทต่างๆ)
PéterTörök

3

นี่คือโซลูชันสไตล์ c ++ 11 ของฉัน พารามิเตอร์ 'base' ใช้สำหรับคลาสฐานของคลาสย่อยทั้งหมด ผู้สร้างเป็นวัตถุ std :: function เพื่อสร้างอินสแตนซ์ของคลาสย่อยอาจเป็นผลผูกพันกับคลาส 'ฟังก์ชั่นสมาชิกคง' คลาสย่อยของคุณสร้าง (บางส่วน args) ' นี่อาจจะไม่สมบูรณ์แบบ แต่ใช้ได้สำหรับฉัน และมันก็เป็นทางออก 'ทั่วไป'

template <class base, class... params> class factory {
public:
  factory() {}
  factory(const factory &) = delete;
  factory &operator=(const factory &) = delete;

  auto create(const std::string name, params... args) {
    auto key = your_hash_func(name.c_str(), name.size());
    return std::move(create(key, args...));
  }

  auto create(key_t key, params... args) {
    std::unique_ptr<base> obj{creators_[key](args...)};
    return obj;
  }

  void register_creator(const std::string name,
                        std::function<base *(params...)> &&creator) {
    auto key = your_hash_func(name.c_str(), name.size());
    creators_[key] = std::move(creator);
  }

protected:
  std::unordered_map<key_t, std::function<base *(params...)>> creators_;
};

ตัวอย่างการใช้งาน

class base {
public:
  base(int val) : val_(val) {}

  virtual ~base() { std::cout << "base destroyed\n"; }

protected:
  int val_ = 0;
};

class foo : public base {
public:
  foo(int val) : base(val) { std::cout << "foo " << val << " \n"; }

  static foo *create(int val) { return new foo(val); }

  virtual ~foo() { std::cout << "foo destroyed\n"; }
};

class bar : public base {
public:
  bar(int val) : base(val) { std::cout << "bar " << val << "\n"; }

  static bar *create(int val) { return new bar(val); }

  virtual ~bar() { std::cout << "bar destroyed\n"; }
};

int main() {
  common::factory<base, int> factory;

  auto foo_creator = std::bind(&foo::create, std::placeholders::_1);
  auto bar_creator = std::bind(&bar::create, std::placeholders::_1);

  factory.register_creator("foo", foo_creator);
  factory.register_creator("bar", bar_creator);

  {
    auto foo_obj = std::move(factory.create("foo", 80));
    foo_obj.reset();
  }

  {
    auto bar_obj = std::move(factory.create("bar", 90));
    bar_obj.reset();
  }
}

ดูดีสำหรับฉัน คุณจะใช้การลงทะเบียนแบบคงที่ (อาจเป็นเวทย์มนตร์มาโคร) ได้อย่างไร? แค่คิดว่าคลาสฐานเป็นคลาสบริการสำหรับวัตถุ คลาสที่ได้รับมอบการบริการพิเศษแก่วัตถุเหล่านั้น และคุณต้องการเพิ่มบริการประเภทต่าง ๆ อย่างต่อเนื่องโดยการเพิ่มคลาสที่ได้มาจากฐานสำหรับบริการแต่ละประเภทดังกล่าว
St0fF

2

รูปแบบโรงงาน

class Point
{
public:
  static Point Cartesian(double x, double y);
private:
};

และถ้าคุณคอมไพเลอร์ไม่สนับสนุน Return Value Optimization ให้ทิ้งมันอาจไม่ได้มีการเพิ่มประสิทธิภาพมากนัก ...


นี่สามารถนำมาใช้เป็นรูปแบบของโรงงานได้หรือไม่?
Dennis

1
@Dennis: เป็นกรณีที่เลวลงฉันจะคิดอย่างนั้น ปัญหาFactoryคือมันค่อนข้างทั่วไปและครอบคลุมพื้นดินมาก โรงงานสามารถเพิ่มข้อโต้แย้ง (ขึ้นอยู่กับสภาพแวดล้อม / การตั้งค่า) หรือให้การแคชบางอย่าง (เกี่ยวข้องกับ Flyweight / Pools) แต่กรณีเหล่านี้เหมาะสมในบางสถานการณ์เท่านั้น
Matthieu M.

ถ้าเพียง แต่เปลี่ยนคอมไพเลอร์จะง่ายอย่างที่คุณทำให้เสียง :)
Rozina

@rozina: :) มันทำงานได้ดีใน Linux (gcc / clang เข้ากันได้อย่างน่าทึ่ง); ฉันยอมรับว่า Windows ยังคงปิดอยู่แม้ว่ามันควรจะดีขึ้นบนแพลตฟอร์ม 64 บิต (จดสิทธิบัตรน้อยลงหากฉันจำได้ถูกต้อง)
Matthieu M.

แล้วคุณก็มีโลกที่ฝังตัวพร้อมคอมไพเลอร์ย่อยบางส่วน .. :) ฉันกำลังทำงานกับสิ่งที่ไม่มีการเพิ่มประสิทธิภาพของค่าตอบแทน ฉันหวังว่ามันจะมี น่าเสียดายที่การสลับมันไม่ได้เป็นตัวเลือกในขณะนี้ หวังว่าในอนาคตจะได้รับการอัปเดตหรือเราจะทำการเปลี่ยนให้กับ sth else :)
rozina

1

ฉันรู้ว่าคำถามนี้ได้รับคำตอบเมื่อ 3 ปีที่แล้ว แต่นี่อาจเป็นสิ่งที่คุณต้องการ

Google ได้เปิดตัวห้องสมุดเมื่อไม่กี่สัปดาห์ที่ผ่านมาซึ่งทำให้สามารถจัดสรรวัตถุแบบไดนามิกได้ง่ายและยืดหยุ่น นี่คือ: http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html

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