มีชื่อสำหรับรูปแบบ (ต่อต้าน -) ของพารามิเตอร์การส่งผ่านที่จะใช้หลายระดับลึกในสายการโทรหรือไม่


209

ฉันพยายามหาทางเลือกในการใช้ตัวแปรทั่วโลกในรหัสดั้งเดิม แต่คำถามนี้ไม่เกี่ยวกับทางเลือกทางเทคนิคผมส่วนใหญ่กังวลเกี่ยวกับคำศัพท์

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

higherlevel(newParam)->level1(newParam)->level2(newParam)->level3(newParam)

newParamก่อนหน้านี้ซึ่งเคยเป็นตัวแปรทั่วโลกในตัวอย่างของฉัน แต่มันอาจเป็นค่าฮาร์ดโค้ดที่ก่อนหน้านี้แทน ประเด็นก็คือว่าตอนนี้ค่าของ newParam จะได้รับที่higherlevel()และมีการ "เดินทาง" level3()ทุกวิธีการ

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

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


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

8
นี่เป็นสเปกตรัมที่กว้างเกินไปที่จะมีคำตอบเฉพาะ ในระดับนี้ฉันจะเรียกสิ่งนี้ว่า "การเข้ารหัส"
Machado

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

7
แม้ว่าฉันจะชื่นชมการอภิปรายว่าเป็นรูปแบบที่ดี / antipattern / แนวคิด / การแก้ปัญหาสิ่งที่ฉันอยากรู้คือถ้ามีชื่อ
ecerulm

3
ฉันเคยได้ยินมันเรียกว่าเกลียวมากที่สุด แต่ยังประปาเช่นในการลดสายลูกดิ่งตลอดสายการโทร
wchargin

คำตอบ:


202

ข้อมูลที่ตัวเองเรียกว่า"ข้อมูลจรจัด" เป็น "รหัสกลิ่น" ซึ่งระบุว่าโค้ดหนึ่งชิ้นกำลังสื่อสารกับโค้ดอีกชิ้นหนึ่งในระยะไกลผ่านตัวกลาง

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

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


73
ด้วยการค้นหา "ข้อมูลคนจรจัด" ฉันสามารถหาหนังสือ "กรอกรหัส" ในการสมัครสมาชิก Safari ของฉัน มีส่วนในหนังสือที่ชื่อว่า "เหตุผลในการใช้ข้อมูลทั่วโลก" และหนึ่งในเหตุผลคือ "การใช้ข้อมูลทั่วโลกสามารถกำจัดข้อมูลคนจรจัด" :) ฉันรู้สึกว่า "ข้อมูลคนจรจัด" จะทำให้ฉันสามารถค้นหาวรรณกรรมเพิ่มเติมเกี่ยวกับการจัดการกับกลุ่มดาวได้ ขอบคุณ!
ecerulm

9
@JimmyJames ฟังก์ชั่นเหล่านั้นทำสิ่งต่าง ๆ แน่นอน ไม่ใช่แค่กับพารามิเตอร์ใหม่ที่เฉพาะเจาะจงซึ่งก่อนหน้านี้เป็นแค่ระดับโลก
ecerulm

174
ในช่วง 20 ปีของการเขียนโปรแกรมฉันไม่เคยได้ยินคำนี้มาก่อนเลยและมันจะไม่ชัดเจนในทันทีว่ามันหมายถึงอะไร ฉันไม่ได้บ่นเกี่ยวกับคำตอบเพียงแค่บอกว่าคำนี้ไม่ได้ใช้กันอย่างแพร่หลาย / รู้จัก อาจจะเป็นเพียงฉัน
Derek Elkins

6
ข้อมูลทั่วโลกบางอย่างใช้ได้ แทนที่จะเรียกมันว่า "ข้อมูลทั่วโลก" คุณสามารถเรียกมันว่า "สภาพแวดล้อม" - เพราะนั่นคือสิ่งที่มันเป็น สภาพแวดล้อมอาจรวมถึงตัวอย่างเช่นเส้นทางของสตริงสำหรับ appdata (บน windows) หรือในโครงการปัจจุบันของฉันชุดแปรง GDI +, ปากกา, ฟอนต์และอื่น ๆ ทั้งหมดที่ใช้โดยคอมโพเนนต์ทั้งหมด
Robinson

7
@ Robinson ไม่มาก ตัวอย่างเช่นคุณต้องการให้รหัสการเขียนภาพของคุณแตะที่% AppData% หรือคุณต้องการให้อาร์กิวเมนต์สำหรับเขียน นั่นคือความแตกต่างระหว่างสถานะโลกกับการโต้แย้ง "สภาพแวดล้อม" สามารถพึ่งพาการฉีดได้อย่างง่ายดายเพียงนำเสนอสำหรับผู้ที่รับผิดชอบในการโต้ตอบกับสภาพแวดล้อม แปรง GDI + ฯลฯ นั้นมีเหตุผลมากกว่า แต่ก็เป็นเรื่องของการจัดการทรัพยากรในสภาพแวดล้อมที่ไม่สามารถทำเพื่อคุณได้จริง ๆ แล้วการขาด APIs และ / หรือภาษา / ไลบรารี / รันไทม์ของคุณนั้นค่อนข้างมาก
Luaan

102

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

ตัวอย่างเช่นสมมติว่าฉันต้องคำนวณจำนวนวันระหว่างวันที่ปฏิทินสองวันดังนั้นฉันจึงสร้างฟังก์ชัน:

int daysBetween(Day a, Day b)

ในการทำเช่นนี้ฉันจะสร้างฟังก์ชั่นใหม่:

int daysSinceEpoch(Day day)

จากนั้นฟังก์ชั่นแรกของฉันกลายเป็นเพียงแค่:

int daysBetween(Day a, Day b)
{
    return daysSinceEpoch(b) - daysSinceEpoch(a);
}

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

สิ่งที่ฉันอยากจะแนะนำคือดูแต่ละฟังก์ชั่นและเริ่มด้วยคำถามสองสามข้อ:

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

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

หากคุณเพียงแค่มีพารามิเตอร์ที่มากเกินไปพิจารณาวิธีการที่ Object refactoring


2
ฉันไม่ได้ตั้งใจจะโต้เถียงกับ (ต่อต้าน) แต่ฉันก็ยังสงสัยว่าจะมีชื่อสำหรับ "สถานการณ์" ที่ต้องอัพเดตลายเซ็นของฟังก์ชันมากมายหรือไม่ ฉันเดาว่าเป็น "กลิ่นรหัส" มากกว่า antipattern มันบอกฉันว่ามีบางสิ่งบางอย่างที่จะแก้ไขในรหัสดั้งเดิมนี้ถ้าฉันต้องปรับปรุงฟังก์ชัน 6 ลายเซ็นเพื่อรองรับการกำจัดของโลก แต่ฉันคิดว่าพารามิเตอร์การส่งผ่านนั้นปกติแล้วและฉันขอขอบคุณที่ให้คำแนะนำเกี่ยวกับวิธีแก้ไขปัญหาพื้นฐาน
ecerulm

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

2
ฉันมักจะทำตามวิธีการนั้นด้วยและอาจจะทำในกรณีนี้ด้วย ฉันแค่อยากจะปรับปรุงคำศัพท์ / คำศัพท์เกี่ยวกับเรื่องนี้เพื่อที่ฉันจะได้ค้นคว้าเพิ่มเติมเกี่ยวกับเรื่องนี้และทำสิ่งที่ดีกว่า
ecerulm

3
@ecululm ฉันไม่คิดว่าจะมีชื่อเดียวสำหรับสิ่งนี้ มันเป็นอาการที่พบได้ทั่วไปในโรคหลายชนิดรวมถึงโรคที่ไม่ใช่โรคเช่น 'ปากแห้ง' หากคุณใส่คำอธิบายโครงสร้างของรหัสออกมามันอาจชี้ไปที่บางสิ่งที่เฉพาะเจาะจง
JimmyJames

@ecululm มันบอกคุณว่ามีบางสิ่งที่จะต้องแก้ไข - ตอนนี้มันชัดเจนมากขึ้นว่าบางสิ่งบางอย่างที่จะได้รับการแก้ไขกว่าเมื่อมันเป็นตัวแปรทั่วโลกแทน
Immibis

61

BobDalgleish ได้ตั้งข้อสังเกตแล้วว่ารูปแบบ (ต่อต้าน -) นี้เรียกว่า " ข้อมูลคนจรจัด "

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

สำหรับตัวอย่างง่ายๆพิจารณาเกมที่มีตัวละครที่ผู้เล่นสามารถปรับแต่งที่มีคุณสมบัติเหมือนplayerName, playerEyeColorและอื่น ๆ แน่นอนว่าผู้เล่นยังมีตำแหน่งทางกายภาพในแผนที่เกมและคุณสมบัติอื่น ๆ เช่นพูดระดับปัจจุบันและระดับสูงสุดและอื่น ๆ

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

playerName = "Bob"
playerEyeColor = GREEN
playerXPosition = -8
playerYPosition = 136
playerHealth = 100
playerMaxHealth = 100

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

mainGameLoop()
 -> processInputEvent()
     -> doPlayerAction()
         -> movePlayer()
             -> checkCollision()
                 -> interactWithNPC()
                     -> interactWithShopkeeper()

... และinteractWithShopkeeper()ฟังก์ชั่นนี้มีเจ้าของที่อยู่ผู้เล่นตามชื่อดังนั้นตอนนี้คุณต้องผ่านplayerNameข้อมูลการเดินผ่านทุกฟังก์ชั่นเหล่านั้นทันที และแน่นอนถ้าเจ้าของร้านคิดว่าผู้เล่นที่มีตาสีฟ้าไร้เดียงสาและจะคิดราคาที่สูงขึ้นสำหรับพวกเขาคุณจะต้องผ่านplayerEyeColorฟังก์ชั่นทั้งหมดและอื่น ๆ

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

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

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

แต่วิธีแก้ไขคือให้วัตถุเก็บการอ้างอิงถึงวัตถุอื่นใดที่มีความสัมพันธ์แบบถาวรหรือชั่วคราวด้วย ตัวอย่างเช่นวัตถุผู้เล่น (และอาจเป็นวัตถุ NPC ด้วย) อาจจะเก็บการอ้างอิงไปยังวัตถุ "เกมโลก" ซึ่งจะมีการอ้างอิงถึงระดับปัจจุบัน / แผนที่เพื่อให้วิธีการเช่นplayer.moveTo(x, y)ไม่จำเป็นต้อง จะได้รับแผนที่อย่างชัดเจนเป็นพารามิเตอร์

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

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


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

6
@JimmyJames: ประเด็นของคุณเกี่ยวกับความแตกต่างเป็นสิ่งที่ดีและฉันคิดว่าจะทำด้วยตัวเอง แต่ทิ้งมันไว้เพื่อไม่ให้คำตอบนานยิ่งขึ้น จุดที่ฉันพยายามทำ (อาจแย่) เพื่อทำสิ่งนั้นในขณะที่การไหลของข้อมูลล้วนมีความแตกต่างกันเล็กน้อยfoo.method(bar, baz)และmethod(foo, bar, baz)มีเหตุผลอื่น ๆ (รวมถึง polymorphism, encapsulation, locality ฯลฯ ) เพื่อให้ชอบอดีต
Ilmari Karonen

@IlmariKaronen: ยังเห็นได้ชัดว่ามันมีประโยชน์อย่างมากในอนาคตที่จะพิสูจน์ต้นแบบของฟังก์ชั่นจากการเปลี่ยนแปลงในอนาคต / การเพิ่ม / การลบ / การทำซ้ำ / การปรับโครงสร้างวัตถุในอนาคต (เช่น playerAge) สิ่งนี้มีค่าเพียงอย่างเดียว
smci

34

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

  • ในฐานะที่เป็นตัวแปรโกลบอลขอบเขตนั้นใหญ่เกินไปเมื่อโปรแกรมมาถึงขนาดที่แน่นอน

  • เป็นพารามิเตอร์โลคัลขอบเขตขอบเขตอาจเล็กเกินไปเมื่อนำไปสู่รายการพารามิเตอร์ซ้ำหลายรายการใน call chains

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


10
+1 สำหรับการออกแบบคลาสที่เหมาะสม ดูเหมือนว่าเป็นปัญหาคลาสสิคที่รอการแก้ไขปัญหา OO
l0b0

21

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

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

Sandwich make_sandwich() {
    PeanutButter pb = get_peanut_butter();
    Jelly j = get_jelly();
    return pb + j;
}
extern PhysicalRefrigerator g_refrigerator;
PeanutButter get_peanut_butter() {
    return g_refrigerator.get("peanut butter");
}
Jelly get_jelly() {
    return g_refrigerator.get("jelly");
}

แต่จะเป็นการดีกว่าถ้าจะใช้การฉีดแบบพึ่งพาและเขียนแบบนี้แทน:

Sandwich make_sandwich(Refrigerator& r) {
    PeanutButter pb = get_peanut_butter(r);
    Jelly j = get_jelly(r);
    return pb + j;
}
PeanutButter get_peanut_butter(Refrigerator& r) {
    return r.get("peanut butter");
}
Jelly get_jelly(Refrigerator& r) {
    return r.get("jelly");
}

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

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

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


10
นี่เป็นรูปแบบการต่อต้านที่ชัดเจนมาก ไม่มีการเรียกให้ตู้เย็นผ่านไป ทีนี้การผ่าน IngredientSource ทั่วไปอาจใช้ได้ แต่ถ้าคุณได้ขนมปังจาก breadbin, ปลาทูน่าจาก larder, ชีสจากตู้เย็น ... โดยการฉีดขึ้นกับแหล่งที่มาของส่วนผสมในการดำเนินการที่ไม่เกี่ยวข้องกับการขึ้นรูปเหล่านั้น ส่วนผสมในแซนวิชคุณได้ละเมิดความกังวลและแยกกลิ่นรหัส
Dewi Morgan

8
@DewiMorgan: เห็นได้ชัดว่าคุณสามารถ refactor ต่อไปกล่าวสรุปRefrigeratorเป็นIngredientSourceหรือแม้กระทั่งการพูดคุยความคิดของ "แซนวิช" ในtemplate<typename... Fillings> StackedElementConstruction<Fillings...> make_sandwich(ElementSource&); เรียกว่า "การเขียนโปรแกรมทั่วไป" และมันก็ทรงพลังพอสมควร แต่แน่นอนว่ามันเป็นวิธีที่เกินความจริงมากกว่า OP ที่ต้องการเข้ามาในตอนนี้ อย่าลังเลที่จะเปิดคำถามใหม่เกี่ยวกับระดับที่เหมาะสมของนามธรรมสำหรับโปรแกรมแซนด์วิช ;)
Quuxplusone

11
หากไม่มีข้อผิดพลาดผู้ใช้ที่ไม่มีสิทธิ์ควรไม่สามารถเข้าถึงmake_sandwich()ได้
dotancohen


19
ข้อผิดพลาดที่ร้ายแรงที่สุดในรหัสของคุณคือคุณเก็บเนยถั่วไว้ในตู้เย็น
Malvolio

15

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


3
หากlevel3()ต้องการnewParamนั่นคือการมีเพศสัมพันธ์อย่างแน่นอน แต่อย่างใดส่วนต่างของรหัสต้องสื่อสารกับแต่ละอื่น ๆ ฉันไม่จำเป็นต้องเรียกพารามิเตอร์ฟังก์ชั่นการเชื่อมต่อที่ไม่ดีถ้าฟังก์ชันนั้นใช้ประโยชน์จากพารามิเตอร์ ฉันคิดว่าแง่มุมที่เป็นปัญหาของโซ่คือการมีเพศสัมพันธ์เพิ่มเติมที่แนะนำให้ใช้level1()และlevel2()ไม่มีประโยชน์อะไรnewParamนอกจากจะผ่านมันไป คำตอบที่ดี +1 สำหรับการมีเพศสัมพันธ์
null

6
@null หากพวกเขาจริงๆไม่ได้มีการใช้งานสำหรับมันที่พวกเขาสามารถทำขึ้นค่าแทนที่จะได้รับจากการโทรของพวกเขา
Random832

3

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

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

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   FilterAndReportStuff(stuffs, desiredName);
}

public void FilterAndReportStuff(IEnumerable<Stuff> stuffs, string desiredName) {
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   ReportStuff(stuffs.Filter(filter));
}

public void ReportStuff(IEnumerable<Stuff> stuffs) {
   stuffs.Report();
}

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

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

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   var filteredStuffs = stuffs.Filter(filter)
   filteredStuffs.Report();
}

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

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

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

ฉันเพิ่งลองเพิ่มการทดสอบหน่วยในชั้นเรียน (ที่ฉันไม่ได้เขียน) ที่ใช้การอ้างอิง 17 รายการซึ่งทั้งหมดนี้ต้องล้อเลียน! ฉันยังไม่ได้รับมันทั้งหมด แต่ฉันแบ่งชั้นเรียนออกเป็นสามชั้นแต่ละจัดการกับคำนามที่แยกจากกันมันเกี่ยวข้องและได้รับรายการอ้างอิงถึง 12 สำหรับที่แย่ที่สุดและประมาณ 8 สำหรับ ดีที่สุด.

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


2

คุณไม่ได้ละเมิดกฎหมายของ Demeter อย่างแท้จริง แต่ปัญหาของคุณคล้ายกับปัญหานั้นในบางวิธี เนื่องจากประเด็นคำถามของคุณคือการค้นหาแหล่งข้อมูลฉันแนะนำให้คุณอ่านเกี่ยวกับ Law of Demeter และดูว่าคำแนะนำนั้นมีผลกับสถานการณ์ของคุณมากแค่ไหน


1
รายละเอียดอ่อนแอเล็กน้อยซึ่งอาจอธิบาย downvotes แต่ด้วยจิตวิญญาณคำตอบนี้เป็นจุดที่แน่นอน: OP ควรอ่านกฎหมายของ Demeter - นี่คือคำที่เกี่ยวข้อง
Konrad Rudolph

4
FWIW ฉันไม่คิดว่ากฎแห่ง Demeter (หรือที่รู้จักว่า "สิทธิพิเศษน้อยที่สุด") มีความเกี่ยวข้องเลย กรณีของ OP เป็นหน้าที่ของเขาที่จะไม่สามารถทำงานได้ถ้ามันไม่มีข้อมูลคนจรจัด (เพราะผู้ชายคนถัดไปที่ call stack ต้องการมันเพราะผู้ชายคนถัดไปต้องการมันและอื่น ๆ ) สิทธิ์น้อยที่สุด / กฏหมายของ Demeter เกี่ยวข้องเฉพาะเมื่อพารามิเตอร์นั้นไม่ได้ใช้จริงและในกรณีนั้นการแก้ไขชัดเจน: ลบพารามิเตอร์ที่ไม่ได้ใช้!
Quuxplusone

2
สถานการณ์ของคำถามนี้ไม่มีอะไรเกี่ยวข้องกับกฎหมายของ Demeter ... มีความคล้ายคลึงกันเล็กน้อยเกี่ยวกับสายของวิธีการโทร แต่อย่างอื่นมันแตกต่างกันมาก
Eric King

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

1
ปัญหาดังกล่าวคล้ายกับการละเมิด LoD เนื่องจากการปรับโครงสร้างตามปกติที่แนะนำเพื่อจัดการกับการละเมิด LoD คือการแนะนำข้อมูลคนจรจัด IMHO นี่เป็นจุดเริ่มต้นที่ดีสำหรับการลดการแต่งงาน แต่ไม่เพียงพอ
Jørgen Fogh

1

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

แต่วิธีการที่ระบุไว้โดย OP มีประโยชน์มากถ้ามีใครทำฟังก์ชั่นการเขียนโปรแกรม


0

ไม่มีรูปแบบการต่อต้านที่นี่เลยเพราะผู้โทรไม่ทราบเกี่ยวกับทุกระดับด้านล่างและไม่สนใจ

ใครบางคนกำลังเรียกระดับที่สูงขึ้น (params) และคาดว่าระดับที่สูงขึ้นจะทำงานของมันได้ สิ่งที่ระดับสูงกว่าทำกับ params นั้นไม่ใช่ธุรกิจของผู้โทร ระดับที่สูงขึ้นจัดการกับปัญหาในวิธีที่ดีที่สุดเท่าที่จะทำได้ในกรณีนี้โดยการส่งผ่าน params ไปที่ระดับ 1 (params) ไม่เป็นไร

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

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