คุณจะย่อยสลายนวกรรมิกได้อย่างไร?


21

ให้บอกว่าฉันมีคลาสศัตรูและตัวสร้างจะมีลักษณะดังนี้:

public Enemy(String name, float width, float height, Vector2 position, 
             float speed, int maxHp, int attackDamage, int defense... etc.){}

สิ่งนี้ดูไม่ดีนักเนื่องจากตัวสร้างมีพารามิเตอร์มากมาย แต่เมื่อฉันสร้างอินสแตนซ์ของศัตรูฉันต้องระบุทุกสิ่งเหล่านี้ ฉันต้องการคุณลักษณะเหล่านี้ในคลาส Enemy เพื่อให้สามารถวนซ้ำรายการของพวกเขาและรับ / ตั้งค่าพารามิเตอร์เหล่านี้ ฉันคิดว่าอาจจะ subclassing Enemy เป็น EnemyB, EnemyA ในขณะที่ hardcoding maxHp ของพวกเขาและคุณลักษณะเฉพาะอื่น ๆ แต่จากนั้นฉันก็จะสูญเสียการเข้าถึงคุณลักษณะ hardcoded ของพวกเขาหากฉันต้องการย้ำผ่านรายการ EnemyA ของ EnemyA และ EnemyB EnemyC ของ)

ฉันแค่พยายามเรียนรู้วิธีการเขียนโค้ดให้สะอาด ถ้ามันสร้างความแตกต่างฉันทำงานใน Java / C ++ / C # จุดใดในทิศทางที่ถูกต้องจะชื่นชม


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

1
ฉันต้องถามว่าคุณตั้งใจสร้างวัตถุของศัตรูด้วยรหัสโดยใช้ตัวอักษรหรือไม่ หากคุณไม่ได้และฉันไม่เห็นว่าทำไมคุณจะสร้างคอนสตรัคเตอร์ที่ดึงข้อมูลจากอินเทอร์เฟซฐานข้อมูลหรือสตริงการทำให้เป็นอนุกรมหรือ ...
Zan Lynx


คำตอบ:


58

การแก้ปัญหาคือการรวมพารามิเตอร์เป็นประเภทคอมโพสิต ความกว้างและความสูงเกี่ยวข้องกับแนวคิด - พวกเขาระบุขนาดของศัตรูและมักจะต้องการร่วมกัน พวกเขาอาจถูกแทนที่ด้วยDimensionsประเภทหรืออาจเป็นRectangleประเภทที่มีตำแหน่ง ในทางกลับกันมันอาจสมเหตุสมผลมากกว่าที่จะจัดกลุ่มpositionและspeedเป็นMovementDataประเภทโดยเฉพาะอย่างยิ่งหากการเร่งความเร็วเข้ามาในรูปภาพ จากบริบทผมถือว่าmaxHp, attackDamage, defenseฯลฯ นอกจากนี้ยังอยู่ด้วยกันในStatsประเภท ดังนั้นลายเซ็นที่แก้ไขอาจมีลักษณะเช่นนี้:

public Enemy(String name, Dimensions dimensions, MovementData movementData, Stats stats)

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


21
ฉันจะเพิ่มด้วยว่าการมีค่าจำนวนมากอาจบ่งบอกถึงการละเมิดหลักการความรับผิดชอบเดี่ยว และการจัดกลุ่มค่าเป็นวัตถุเฉพาะเป็นขั้นตอนแรกในการแยกความรับผิดชอบเหล่านั้น
ร่าเริง

2
ฉันไม่คิดว่ารายการค่าเป็นปัญหาของ SRP ส่วนใหญ่มีไว้สำหรับผู้สร้างคลาสฐาน แต่ละคลาสในลำดับชั้นสามารถมีความรับผิดชอบเดียว Enemyเป็นเพียงคลาสที่กำหนดเป้าหมายPlayerแต่คลาสพื้นฐานทั่วไปของพวกเขาCombatantต้องการสถิติการต่อสู้
MSalters

@MSalters ไม่จำเป็นต้องระบุถึงปัญหา SRP แต่สามารถทำได้ ถ้าเขาต้องการที่จะทำตัวเลขให้ได้มากพอฟังก์ชั่นเหล่านั้นสามารถหาทางเข้าไปในคลาสศัตรูเมื่อพวกเขาควรจะเป็นฟังก์ชั่นแบบคงที่ / ฟรี (ถ้าเขาใช้Dimensions/ MovementDataเป็นที่เก็บข้อมูลเก่าแบบธรรมดา) หรือวิธีการ ประเภท / วัตถุ) ตัวอย่างเช่นถ้าเขาไม่ได้สร้างไว้แล้วชนิดที่เขาอาจจะได้จบลงด้วยการทำคณิตศาสตร์เวกเตอร์ในVector2 Enemy
Doval

24

คุณอาจต้องการที่จะดูที่รูปแบบการสร้าง จากลิงก์ (พร้อมตัวอย่างของรูปแบบและทางเลือก):

[รูปแบบ] ตัวสร้างเป็นตัวเลือกที่ดีเมื่อออกแบบคลาสที่คอนสตรัคเตอร์หรือโรงงานคงที่จะมีมากกว่าหนึ่งพารามิเตอร์โดยเฉพาะถ้าพารามิเตอร์เหล่านั้นส่วนใหญ่เป็นตัวเลือก รหัสลูกค้านั้นง่ายต่อการอ่านและเขียนด้วยผู้สร้างมากกว่ารูปแบบตัวสร้าง telescoping แบบดั้งเดิมและผู้สร้างปลอดภัยกว่า JavaBeans


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

1
ขอขอบคุณทั้งรูปแบบตัวสร้างและรูปแบบโรงงานดูเหมือนพวกเขาจะทำงานได้ดีกับสิ่งที่ฉันพยายามทำโดยรวม ฉันคิดว่าการผสมผสานระหว่าง Builder / Factory และข้อเสนอแนะของ Doval อาจเป็นสิ่งที่ฉันกำลังมองหา แก้ไข: ฉันเดาว่าฉันสามารถทำเครื่องหมายคำตอบเดียวเท่านั้น ฉันจะมอบมันให้ Doval เพราะมันตอบคำถามในหัวข้อ แต่คนอื่น ๆ ก็มีประโยชน์เท่ากันกับปัญหาเฉพาะของฉัน ขอบคุณทุกคน.
Travis

ฉันคิดว่ามันน่าสังเกตว่าถ้าภาษาของคุณรองรับ phantom types คุณสามารถเขียนรูปแบบ builder ที่บังคับให้ SetX บางฟังก์ชั่น / ทั้งหมดถูกเรียกใช้ นอกจากนี้ยังช่วยให้หนึ่งเพื่อให้แน่ใจว่าพวกเขาได้รับการเรียกเพียงครั้งเดียวเกินไป (ถ้าต้องการ)
Thomas Eding

1
@ Mark16 ดังที่กล่าวไว้ในลิงค์> รูปแบบของตัวสร้างจะจำลองชื่อพารามิเตอร์ทางเลือกตามที่พบใน Ada และ Python คุณพูดถึงว่าคุณใช้ C # ในคำถามและภาษานั้นสนับสนุนอาร์กิวเมนต์ที่มีชื่อ / เป็นทางเลือก (เช่น C # 4.0) ดังนั้นอาจเป็นตัวเลือกอื่น
Bob

5

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

รูปแบบโรงงานมักจะใช้ที่เป็นนามธรรมมากกว่าระดับที่แน่นอนมาใช้ แต่ก็ยังสามารถนำมาใช้เพื่อให้แม่แบบสำหรับการสร้างวัตถุ:

class EnemyFactory {

    // each of these methods is essentially a template for a kind of enemy

    Enemy enemyA(String name, ...) {
        return new Enemy(name, ..., presetValue, ...);
    }

    Enemy enemyB(String name, ...) {
        return new Enemy(name, ..., otherValue, ...);
    }

    Enemy enemyC(String name, ...) {
        return new EnemySubclass(name, ..., otherValue, ...);
    }

    ...
}

EnemyFactory factory = new EnemyFactory();
Enemy a = factory.enemyA("fred", ...);
Enemy b = factory.enemyB("willy", ...);

0

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

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

ขึ้นอยู่กับภาษาที่คุณเลือกใช้บางคนสามารถตั้งค่าเริ่มต้นสำหรับพารามิเตอร์อินพุตของตัวสร้างของคุณเช่น:

Enemy(float height = 42, float width = 42);

0

ตัวอย่างโค้ดที่จะเพิ่มในคำตอบของ Rory Hunter (ใน Java):

public class Enemy{
   private String name;
   private float width;
   ...

   public static class Builder{
       private Enemy instance;

       public Builder(){
           this.instance = new Enemy();
       }


       public Builder withName(String name){
           instance.name = name;
           return this;
       }

       ...

       public Enemy build(){
           return instance;
       }
   }
}

ตอนนี้คุณสามารถสร้างอินสแตนซ์ใหม่ของ Enemy ดังนี้:

Enemy myEnemy = new Enemy.Builder().withName("John").withX(x).build();

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