คุณใช้ประโยชน์จากหลักการแบบเปิดหรือไม่


12

หลักการ open-closed (OCP) ระบุว่าควรเปิดวัตถุสำหรับส่วนขยาย แต่ปิดเพื่อทำการปรับเปลี่ยน ฉันเชื่อว่าฉันเข้าใจและใช้งานร่วมกับ SRP เพื่อสร้างคลาสที่ทำสิ่งเดียวเท่านั้น และฉันพยายามสร้างวิธีการขนาดเล็กจำนวนมากที่ทำให้สามารถแยกการควบคุมพฤติกรรมทั้งหมดออกเป็นวิธีที่อาจขยายหรือแทนที่ในคลาสย่อยบางประเภท ดังนั้นฉันจบลงด้วยคลาสที่มีคะแนนส่วนขยายมากมายไม่ว่าจะเป็น: การฉีดและองค์ประกอบ, เหตุการณ์, การมอบหมาย ฯลฯ

พิจารณาคลาสที่เรียบง่ายและขยายได้ดังต่อไปนี้

class PaycheckCalculator {
    // ...
    protected decimal GetOvertimeFactor() { return 2.0M; }
}

ตอนนี้พูดเช่นว่าการOvertimeFactorเปลี่ยนแปลง 1.5 ตั้งแต่ระดับดังกล่าวข้างต้นได้รับการออกแบบมาเพื่อขยายฉันสามารถ subclass OvertimeFactorและกลับที่แตกต่างกัน

แต่ ... แม้จะมีคลาสที่ถูกออกแบบมาสำหรับการขยายและการยึดติดกับ OCP ฉันจะแก้ไขวิธีการที่เป็นปัญหามากกว่าซับคลาสและแทนที่วิธีการที่มีปัญหา

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

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

คำตอบ:


10

หากคุณกำลังแก้ไขคลาสพื้นฐานดังนั้นมันจะไม่ปิดจริง ๆ !

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

จริง ๆ แล้วการปิดคลาส แต่เปิดคุณควรดึงค่าล่วงเวลาจากแหล่งอื่น (อาจเป็นไฟล์ config) หรือพิสูจน์วิธีเสมือนที่สามารถแทนที่ได้?

ถ้าชั้นถูกปิดอย่างแท้จริงแล้วหลังการเปลี่ยนแปลงของคุณไม่กรณีทดสอบจะล้มเหลว (สมมติว่าคุณมีความคุ้มครอง 100% กับทุกกรณีทดสอบของคุณ) GetOvertimeFactor() == 2.0Mและฉันจะคิดว่ามีกรณีทดสอบการตรวจสอบว่า

อย่าผ่านวิศวกร

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

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


"หลักการปิดไม่ได้ขัดขวางคุณจากการปรับแก้วัตถุอีกครั้ง" ที่จริงมันไม่ หากคุณอ่านหนังสือที่เสนอหลักการแบบเปิดปิดเป็นครั้งแรกหรือบทความที่แนะนำตัวย่อ "OCP" คุณจะเห็นว่ามันบอกว่า "ไม่มีใครได้รับอนุญาตให้ทำการเปลี่ยนแปลงซอร์สโค้ดกับมัน" (ยกเว้นข้อผิดพลาด แก้ไข)
Rogério

@ Rogério: นั่นอาจเป็นจริง (ย้อนกลับไปในปี 1988) แต่คำจำกัดความปัจจุบัน (ได้รับความนิยมในปี 1990 เมื่อ OO เป็นที่นิยม) ทั้งหมดเกี่ยวกับการรักษาส่วนต่อประสานสาธารณะที่สอดคล้องกัน During the 1990s, the open/closed principle became popularly redefined to refer to the use of abstracted interfaces, where the implementations can be changed and multiple implementations could be created and polymorphically substituted for each other. en.wikipedia.org/wiki/Open/closed_principle
Martin York

ขอบคุณสำหรับการอ้างอิง Wikipedia แต่ฉันไม่แน่ใจว่าคำจำกัดความ "ปัจจุบัน" นั้นแตกต่างกันจริงๆเนื่องจากยังคงต้องอาศัยการสืบทอดประเภท (คลาสหรืออินเทอร์เฟซ) และคำพูดที่ว่า "ไม่มีการเปลี่ยนแปลงซอร์สโค้ด" ที่ฉันพูดถึงมาจากบทความ OCP 1996 ของ Robert Martin ซึ่งเป็น (ตามที่คาดคะเน) ซึ่งสอดคล้องกับ "ข้อกำหนดปัจจุบัน" โดยส่วนตัวแล้วฉันคิดว่าหลักการแบบเปิดปิดจะถูกลืมในตอนนี้หากมาร์ตินไม่ได้ให้คำย่อที่เห็นได้ชัดว่ามีคุณค่าทางการตลาดมากมาย หลักการนั้นล้าสมัยและเป็นอันตราย IMO
Rogério

3

ดังนั้นการเปิดปิดหลักการเป็น gotcha ... โดยเฉพาะอย่างยิ่งถ้าคุณพยายามที่จะใช้มันในเวลาเดียวกันเป็นYAGNI ฉันจะปฏิบัติตามทั้งสองอย่างในเวลาเดียวกันได้อย่างไร ใช้กฎสามข้อ ครั้งแรกที่คุณทำการเปลี่ยนแปลงให้ทำการเปลี่ยนแปลงโดยตรง และครั้งที่สองเช่นกัน ครั้งที่สามก็ถึงเวลาที่จะเปลี่ยนเป็นนามธรรม

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


2

ในตัวอย่างที่คุณโพสต์ปัจจัยการทำงานล่วงเวลาควรเป็นตัวแปรหรือค่าคงที่ * (ตัวอย่าง Java)

class PaycheckCalculator {
   float overtimeFactor;

   protected float setOvertimeFactor(float overtimeFactor) {
      this.overtimeFactor = overtimeFactor;
   }

   protected float getOvertimeFactor() {
      return overtimeFactor;
   }
}

หรือ

class PaycheckCalculator {
   public static final float OVERTIME_FACTOR = 1.5f;
}

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

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

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

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


นี่เป็นการเปลี่ยนแปลงข้อมูลไม่ใช่การเปลี่ยนแปลงรหัส อัตราการทำงานล่วงเวลาไม่ควรมีการเข้ารหัสอย่างหนัก
Jim C

คุณดูเหมือนจะมีการได้รับและการตั้งค่าของคุณไปข้างหลัง
Mason Wheeler

อ๊ะ! ควรทดสอบ ...
Michael K

2

ฉันไม่เห็นตัวอย่างของคุณในฐานะตัวแทนที่ยอดเยี่ยมของ OCP ฉันคิดว่าสิ่งที่กฎจริงๆหมายถึงคือ:

เมื่อคุณต้องการเพิ่มคุณสมบัติคุณควรเพิ่มคลาสหนึ่งคลาสเท่านั้นและคุณไม่จำเป็นต้องแก้ไขคลาสอื่น ๆ (แต่อาจเป็นไฟล์กำหนดค่า)

การใช้งานที่ไม่ดีด้านล่าง ทุกครั้งที่คุณเพิ่มเกมคุณจะต้องแก้ไขคลาส GamePlayer

class GamePlayer
{
   public void PlayGame(string game)
   {
      switch(game)
      {
          case "Poker":
              PlayPoker();
              break;

          case "Gin": 
              PlayGin();
              break;

          ...
      }
   }

   ...
}

ไม่จำเป็นต้องแก้ไขคลาส GamePlayer

class GamePlayer
{
    ...

    public void PlayGame(string game)
    {
        Game g = GameFactory.GetByName(game); 
        g.Play();   
    }

    ...
}

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

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

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

ในฐานะที่เป็นตัวอย่างในโลกแห่งความจริงฉันได้ใช้ตัวอย่างที่หลากหลายและหลักการแบบเปิดปิดจริงๆ ฟังก์ชั่นการใช้งานนั้นเพิ่มได้ง่ายเพราะฉันต้องมาจากคลาสฐานที่เป็นนามธรรมและ "โรงงาน" ของฉันหยิบมันขึ้นมาโดยอัตโนมัติและ "ผู้เล่น" ไม่สนใจว่าการใช้งานที่เป็นรูปธรรมของโรงงานจะกลับมาเป็นอย่างไร


1

ในตัวอย่างนี้คุณมีสิ่งที่เรียกว่า "Magic Value" โดยพื้นฐานแล้วเป็นค่าฮาร์ดโค้ดที่อาจมีหรือไม่มีการเปลี่ยนแปลงเมื่อเวลาผ่านไป ฉันจะพยายามไขปริศนาที่คุณแสดงออกโดยทั่วไป แต่นี่เป็นตัวอย่างของประเภทของสิ่งที่การสร้างคลาสย่อยนั้นทำงานได้ดีกว่าการเปลี่ยนค่าในชั้นเรียน

ยิ่งกว่านั้นคุณได้ระบุพฤติกรรมที่เร็วเกินไปในลำดับชั้นของคลาส

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

ในPaycheckCalculatorคลาสฐานคุณทำให้เป็นนามธรรมและระบุวิธีการที่คุณคาดหวัง การคำนวณหลักเหมือนกันมันเป็นเพียงแค่ว่าปัจจัยบางอย่างถูกคำนวณต่างกัน HourlyPaycheckCalculatorจากนั้นคุณจะใช้getOvertimeFactorวิธีการนั้นและส่งกลับ 1.5 หรือ 2.0 แล้วแต่กรณีของคุณ คุณStraightTimePaycheckCalculatorจะใช้getOvertimeFactorเพื่อส่งคืน 1.0 ในที่สุดการใช้งานที่สามจะเป็นสิ่งNoOvertimePaycheckCalculatorที่จะใช้getOvertimeFactorเพื่อกลับ 0

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

อาหารสำหรับความคิด: เมื่อชั้นเรียนของเราสรุปปัจจัยข้อมูลบางอย่างเช่นOvertimeFactorในตัวอย่างของคุณคุณอาจต้องการวิธีดึงข้อมูลจากแหล่งอื่น ตัวอย่างเช่นไฟล์คุณสมบัติ (เนื่องจากดูเหมือนว่า Java) หรือฐานข้อมูลจะเก็บค่าไว้และคุณPaycheckCalculatorจะใช้ data access object เพื่อดึงค่าของคุณ สิ่งนี้ทำให้คนที่เหมาะสมสามารถเปลี่ยนพฤติกรรมของระบบได้โดยไม่ต้องมีการเขียนรหัสใหม่

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