ประเภทข้อมูลความรับผิดชอบเดี่ยวและกำหนดเอง


10

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

มันเกี่ยวกับหลักการความรับผิดชอบเดี่ยว (SRP) โดยทั่วไปฉันมีคลาสข้อมูลFontที่ไม่เพียง แต่มีฟังก์ชั่นสำหรับจัดการข้อมูล แต่ยังสำหรับการโหลด ฉันบอกว่าทั้งสองควรแยกจากกันฟังก์ชั่นการโหลดควรอยู่ในคลาสของโรงงาน ฉันคิดว่านี่เป็นความผิดพลาดของ SRP ...

ชิ้นส่วนจากคลาสแบบอักษรของฉัน

class Font
{
  public:
    bool isLoaded() const;
    void loadFromFile(const std::string& file);
    void loadFromMemory(const void* buffer, std::size_t size);
    void free();

    void some();
    void another();
};

การออกแบบที่แนะนำ

class Font
{
  public:
    void some();
    void another();
};


class FontFactory
{
  public:
    virtual std::unique_ptr<Font> createFromFile(...) = 0;
    virtual std::unique_ptr<Font> createFromMemory(...) = 0;
};

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

นี่คือเหตุผลที่ฉันคิดว่าวิธีการของฉันดีกว่า:

  • Fontเป็นแบบพอเพียง - การแบบพอเพียงง่ายต่อการเข้าใจและบำรุงรักษา นอกจากนี้คุณสามารถใช้คลาสได้โดยไม่ต้องรวมอะไรไว้ อย่างไรก็ตามหากคุณพบว่าคุณต้องการการจัดการทรัพยากรที่ซับซ้อนมากขึ้น (โรงงาน) คุณสามารถทำสิ่งนั้นได้ง่ายเช่นกัน (ภายหลังฉันจะพูดถึงโรงงานของตัวเองResourceManager<Font>)

  • เป็นไปตามไลบรารี่มาตรฐาน - ฉันเชื่อว่าประเภทที่ผู้ใช้กำหนดควรพยายามคัดลอกพฤติกรรมของประเภทมาตรฐานในภาษานั้น ๆ ให้มากที่สุด std::fstreamเป็นตัวเองเพียงพอและจะให้ทำงานที่ชอบและopen closeการติดตามห้องสมุดมาตรฐานหมายความว่าไม่จำเป็นต้องใช้ความพยายามในการเรียนรู้อีกวิธีหนึ่งในการทำสิ่งต่าง ๆ นอกจากนี้โดยทั่วไปแล้วคณะกรรมการมาตรฐาน C ++ อาจรู้เรื่องการออกแบบมากกว่าใครที่นี่ดังนั้นหากมีข้อสงสัยให้คัดลอกสิ่งที่พวกเขาทำ

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

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

ฉันก็มีโรงงานเช่นกัน

(เพราะการออกแบบของFontช่วยให้ฉันไป.)

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

template<class T>
class ResourceManager
{
  public:
    ResourcePtr<T> acquire(const std::string& file);
    ResourcePtr<T> acquire(const void* buffer, std::size_t size);
};

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

void test(ResourceManager<Font>* rm)
{
    // The same file isn't loaded twice into memory.
    // I can still have as many Fonts using that file as I want, though.
    ResourcePtr<Font> font1 = rm->acquire("fonts/arial.ttf");
    ResourcePtr<Font> font2 = rm->acquire("fonts/arial.ttf");

    // Print something with the two fonts...
}

บรรทัดล่าง ...

(มันอยากจะใส่ tl; dr ที่นี่ แต่ฉันคิดไม่ออกเลย: \) เอา
ล่ะคุณมีแล้วฉันทำดีที่สุดเท่าที่จะทำได้ กรุณาโพสต์ข้อโต้แย้งใด ๆ ที่คุณมีและข้อดีที่คุณคิดว่าการออกแบบที่แนะนำมีมากกว่าการออกแบบของฉันเอง โดยพื้นฐานแล้วพยายามแสดงให้ฉันเห็นว่าฉันผิด :)


2
ทำให้ผมนึกถึงมาร์ตินฟาวเลอร์ของActiveRecord VS DataMapper
ผู้ใช้

ให้ความสะดวกสบาย (การออกแบบปัจจุบันของคุณ) ในส่วนต่อประสานภายนอกที่ผู้ใช้หันเข้าหา ใช้ SRP ภายในเพื่อให้การเปลี่ยนแปลงในการใช้งานในอนาคตง่ายขึ้น ฉันสามารถนึกถึงการปรุงแต่งตัวโหลดแบบอักษรที่ข้ามตัวเอียงและตัวหนา ที่โหลด Unicode BMP เท่านั้นเป็นต้น
rwong


@rwong ฉันรู้ว่างานนำเสนอนั้นฉันมีที่คั่นหน้าไว้ ( วิดีโอ ) :) แต่ฉันไม่เข้าใจสิ่งที่คุณกำลังพูดในความคิดเห็นอื่น ๆ ของคุณ ...
Paul

1
@rwong มันไม่ใช่สายการบินเดียวใช่ไหม? คุณต้องการเพียงหนึ่งบรรทัดไม่ว่าคุณจะโหลดฟอนต์โดยตรงหรือผ่าน ResourceManager และอะไรที่ทำให้ฉันหยุดการปรับใช้ RM อีกครั้งหากผู้ใช้บ่น
Paul

คำตอบ:


7

ในความคิดของฉันไม่มีอะไรผิดปกติกับสิ่งที่คุณต้องการในลักษณะที่สมเหตุสมผลและง่ายต่อการดูแลรักษา

อย่างไรก็ตามปัญหาที่คุณมีกับรหัสนี้คือว่าถ้าคุณต้องการจะทำอะไรก็ได้ที่คุณกำลังจะมีการเปลี่ยนแปลงทั้งหมด

ประเด็นของ SRP คือถ้าคุณมีส่วนประกอบเดียว 'CompA' ที่ทำอัลกอริทึม A () และคุณต้องเปลี่ยนอัลกอริทึม A () คุณไม่ควรเปลี่ยน 'CompB' เช่นกัน

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

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


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

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

คำถามที่ยอดเยี่ยมและคำตอบที่ดีและสิ่งที่ดีที่สุดคือผู้พัฒนาหลายคนสามารถเรียนรู้ได้ นี่คือเหตุผลที่ฉันชอบอยู่ที่นี่ :) โอ้ดังนั้นความคิดเห็นของฉันจึงไม่ซ้ำซ้อน SRP อาจเป็นเรื่องยุ่งยากเล็กน้อยเพราะคุณต้องถามตัวเองว่า "เกิดอะไรขึ้น" ซึ่งอาจดูขัดแย้งกับ: 'การปรับให้เหมาะสมก่อนกำหนดเป็นรากของความชั่วร้ายทั้งหมด' หรือ ' ปรัชญาของ YAGNI ไม่มีคำตอบขาวดำ!
Martijn Verburg

0

สิ่งหนึ่งที่ทำให้ฉันสับสนเกี่ยวกับชั้นเรียนของคุณคือคุณมีloadFromMemoryและloadFromFileวิธีการ เป็นการดีที่คุณควรมีloadFromMemoryวิธีการเท่านั้น แบบอักษรไม่ควรสนใจว่าข้อมูลในหน่วยความจำมาเป็นอย่างไร อีกสิ่งหนึ่งคือคุณควรใช้ Constructor / Destructor แทนการโหลดและfreeเมธอด ดังนั้นloadFromMemoryก็จะกลายเป็นFont(const void *buf, int len)และจะกลายเป็นfree()~Font()


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