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


30

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

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

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

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

ผู้ใช้เป็นเพียงตัวอย่างเดียว นี่คือการฝึกฝนอย่างกว้างขวางในรหัสของเรา

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

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

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

มีการละเมิดหลักการอื่น ๆ ที่ไม่ใช่แบบแข็งเช่นหลักการตั้งโปรแกรมการป้องกันขั้นพื้นฐานหรือไม่?

ตัวสร้างของฉันควรมีลักษณะเช่นนี้:

MyConstructor(GUID userid, String username)

หรือสิ่งนี้:

MyConstructor(User theUser)

โพสต์แก้ไข:

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


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

2
รูปแบบที่สองมักจะใช้เมื่อจำนวนพารามิเตอร์ที่ส่งผ่านได้กลายเป็นเทอะทะ
Robert Harvey

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

9
คำว่า "แยกวิเคราะห์" ไม่สมเหตุสมผลในบริบทที่คุณใช้งาน คุณหมายถึง“ pass” แทนหรือเปล่า
Konrad Rudolph

5
สิ่งที่เกี่ยวกับIในSOLID? MyConstructorโดยทั่วไปบอกว่าตอนนี้ "ฉันต้องการGuidและstring" เหตุใดจึงไม่มีอินเทอร์เฟซที่ให้Guidและ a stringให้Userใช้อินเทอร์เฟซนั้นและให้MyConstructorขึ้นอยู่กับอินสแตนซ์ที่ใช้อินเทอร์เฟซนั้น และหากความต้องการของMyConstructorการเปลี่ยนแปลงเปลี่ยนอินเทอร์เฟซ - มันช่วยให้ผมอย่างมากในการคิดของอินเตอร์เฟซที่จะ "เป็นของ" เพื่อผู้บริโภคมากกว่าผู้ให้บริการ ดังนั้นคิดว่า "ในฐานะผู้บริโภคฉันต้องการบางสิ่งที่ทำสิ่งนี้และ" แทนที่จะเป็น "ในฐานะผู้ให้บริการที่ฉันสามารถทำได้และนั่น"
Corak

คำตอบ:


31

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

การส่งข้อมูลชนิดง่าย ๆ นั้นดีจนกระทั่งพวกมันหมายถึงบางอย่างที่ไม่ใช่สิ่งที่พวกเขาเป็น ลองพิจารณาตัวอย่างนี้:

public class Foo
{
    public void Bar(int userId)
    {
        // ...
    }
}

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

var user = blogPostRepository.Find(32);
var foo = new Foo();

foo.Bar(user.Id);

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

ตอนนี้ลองพิจารณาตัวอย่างที่เปลี่ยนแปลงนี้:

public class Foo
{
    public void Bar(User user)
    {
        // ...
    }
}

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

var user = blogPostRepository.Find(32);
var foo = new Foo();

foo.Bar(user);

ตอนนี้เรามีข้อผิดพลาดของคอมไพเลอร์ blogPostRepository.Findวิธีการส่งกลับBlogPostวัตถุซึ่งเราอย่างชาญฉลาดเรียกว่า "ผู้ใช้" จากนั้นเราจะส่ง "ผู้ใช้" นี้ไปยังBarเมธอดและรับข้อผิดพลาดคอมไพเลอร์ทันทีเนื่องจากเราไม่สามารถส่งผ่านBlogPostไปยังเมธอดที่ยอมรับUserไปยังวิธีการที่ยอมรับได้

ระบบ Type ของภาษานั้นได้รับการยกระดับให้เขียนโค้ดที่ถูกต้องเร็วขึ้นและระบุข้อบกพร่อง ณ เวลารวบรวมมากกว่าเวลาทำงาน

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


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

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

5
@Ian: ฉันคิดว่าการใส่รหัสในรองเท้าสเก็ตประเภทของตัวเองรอบปัญหาต้นฉบับที่นำเสนอโดย OP ซึ่งเป็นการเปลี่ยนแปลงโครงสร้างของคลาสผู้ใช้ทำให้จำเป็นต้องปรับโครงสร้างลายเซ็นหลายวิธี ผ่านวัตถุผู้ใช้ทั้งหมดแก้ปัญหานี้
เกร็ก Burghardt

@Ian: แม้ว่าจะซื่อสัตย์แม้ว่าการทำงานใน C # ฉันถูกล่อลวงมากที่จะห่อรหัสและการจัดเรียงในโครงสร้างเพียงเพื่อให้ชัดเจนยิ่งขึ้น
เกร็ก Burghardt

1
"ไม่มีอะไรผิดปกติที่จะส่งตัวชี้ไปรอบ ๆ " หรือการอ้างอิงเพื่อหลีกเลี่ยงปัญหาทั้งหมดเกี่ยวกับพอยน์เตอร์ที่คุณอาจพบเจอ
Yay295

17

ฉันคิดถูกไหมว่านี่เป็นการละเมิดหลักการเปิด / ปิด?

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

มีหลักการอื่นใดที่ผิดไปจากการฝึกฝนนี้หรือไม่? perahaps การพึ่งพาการพึ่งพาอาศัยกัน?

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

มีการละเมิดหลักการอื่น ๆ ที่ไม่ใช่แบบแข็งเช่นหลักการตั้งโปรแกรมการป้องกันขั้นพื้นฐานหรือไม่?

ไม่ได้วิธีการนี้เป็นวิธีการเข้ารหัสที่สมบูรณ์แบบ มันไม่ได้เป็นการละเมิดหลักการดังกล่าว

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

ในการพูดถึงความคิดเห็นของคุณ:

มีปัญหาเกี่ยวกับการแยกค่าใหม่โดยไม่จำเป็นลงไปห้าระดับของเชนและเปลี่ยนการอ้างอิงทั้งหมดเป็นวิธีทั้งห้าที่มีอยู่ทั้งหมด ...

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

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

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

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

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


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

@Jimbo ฉันได้อัปเดตคำตอบของฉันแล้วลองตอบความคิดเห็นของคุณ
David Arno

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

ไม่ใช่การผกผันของการพึ่งพาเพื่อส่งผ่านพารามิเตอร์ของผู้ใช้แทนที่จะเป็นผู้ใช้เอง
James Ellis-Jones

@ JamesEllis-Jones การคว่ำการพึ่งพาพลิกการพึ่งพาจาก "ถาม" เป็น "บอก" หากคุณส่งผ่านUserอินสแตนซ์แล้วค้นหาวัตถุนั้นเพื่อรับพารามิเตอร์จากนั้นคุณจะกลับเฉพาะการอ้างอิงเท่านั้น ยังมีบางคนถามว่าเกิดอะไรขึ้น การผกผันของการพึ่งพาที่แท้จริงคือ 100% "บอกไม่ต้องถาม" แต่มันมาพร้อมกับราคาที่ซับซ้อน
David Arno

10

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

แต่ที่อาจจะวิ่งปะทะกันของอินเตอร์เฟซแยกหลักการตั้งแต่คุณอาจจะผ่านไปทางข้อมูลมากกว่าความต้องการฟังก์ชั่นในการทำงานของตน

ดังนั้นเช่นเดียวกับสิ่งส่วนใหญ่ - มันขึ้นอยู่กับมันขึ้นอยู่กับ

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

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


+1 แต่ฉันไม่แน่ใจเกี่ยวกับวลีของคุณ "คุณสามารถส่งผ่านข้อมูลเพิ่มเติมได้" เมื่อคุณผ่าน (User theuser ผู้ใช้) คุณจะส่งข้อมูลขั้นต่ำสุดไปยังการอ้างอิงถึงวัตถุหนึ่งชิ้น จริงที่อ้างอิงสามารถใช้เพื่อรับข้อมูลเพิ่มเติม แต่หมายความว่ารหัสการโทรไม่จำเป็นต้องได้รับ เมื่อคุณผ่าน (ผู้ใช้ GUID ชื่อผู้ใช้สตริง) วิธีการที่เรียกสามารถเรียก User.find (หมายเลขผู้ใช้) เพื่อค้นหาอินเทอร์เฟซสาธารณะของวัตถุดังนั้นคุณจึงไม่ซ่อนอะไรเลย
dcorking

5
@dcorking " เมื่อคุณผ่าน (ผู้ใช้ theuser) คุณจะผ่านข้อมูลขั้นต่ำมากการอ้างอิงไปยังวัตถุหนึ่งรายการ " คุณส่งข้อมูลสูงสุดที่เกี่ยวข้องกับวัตถุนั้น: วัตถุทั้งหมด msgstr " วิธีการที่เรียกสามารถเรียกใช้ User.find (หมายเลขผู้ใช้) ... " ในระบบที่ออกแบบมาอย่างดีนั้นจะเป็นไปไม่ได้เนื่องจากวิธีการที่เป็นปัญหาจะไม่สามารถเข้าถึงUser.find()ได้ ในความเป็นจริงมีไม่ควรแม้แต่จะ ค้นหาผู้ใช้ไม่ควรเป็นความรับผิดชอบของUser.find User
David Arno

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

@DavidArno อาจเป็นคำตอบที่ชัดเจนสำหรับ OP ใครเป็นผู้รับผิดชอบในการค้นหาผู้ใช้ มีชื่อสำหรับหลักการออกแบบของการแยกตัวค้นหา / โรงงานออกจากชั้นเรียนหรือไม่?
dcorking

1
@ dcorking ฉันจะบอกว่านั่นเป็นนัยหนึ่งของหลักการความรับผิดชอบเดียว การรู้ตำแหน่งที่ผู้ใช้ถูกจัดเก็บและวิธีการเรียกคืนด้วย ID เป็นความรับผิดชอบแยกต่างหากUser- คลาสไม่ควรมี อาจมีUserRepositoryหรือคล้ายกันที่เกี่ยวข้องกับสิ่งต่าง ๆ
Hulk

3

การออกแบบนี้เป็นไปตามรูปแบบพารามิเตอร์วัตถุ มันแก้ปัญหาที่เกิดขึ้นจากการมีพารามิเตอร์จำนวนมากในลายเซ็นวิธีการ

ฉันคิดถูกไหมว่านี่เป็นการละเมิดหลักการเปิด / ปิด?

ไม่การใช้รูปแบบนี้เปิดใช้หลักการเปิด / ปิด (OCP) สำหรับคลาสอนุพันธ์Userสามารถให้เป็นพารามิเตอร์ที่ทำให้เกิดพฤติกรรมที่แตกต่างในชั้นเรียนการบริโภค

มีหลักการอื่นใดที่ผิดไปจากการฝึกฝนนี้หรือไม่?

มันสามารถเกิดขึ้นได้ ให้ฉันอธิบายบนพื้นฐานของหลักการที่เป็นของแข็ง

หลักการความรับผิดชอบเดี่ยว (SRP) สามารถละเมิดถ้ามันมีการออกแบบที่คุณได้อธิบายว่า:

คลาสนี้แสดงข้อมูลทั้งหมดเกี่ยวกับผู้ใช้ ID ชื่อระดับการเข้าถึงแต่ละโมดูลเขตเวลาเป็นต้น

ปัญหาคือมีข้อมูลทั้งหมด ถ้าUserคลาสมีคุณสมบัติมากมายมันจะกลายเป็นData Transfer Objectขนาดใหญ่ซึ่งถ่ายโอนข้อมูลที่ไม่เกี่ยวข้องจากมุมมองของคลาสที่ใช้ไป ตัวอย่าง: จากมุมมองของคลาสบริโภคUserAuthenticationคุณสมบัติUser.IdและUser.Nameเกี่ยวข้อง แต่ไม่ใช่User.Timezoneแต่ไม่

อินเตอร์เฟสแยกหลักการ (ISP) นอกจากนี้ยังมีการละเมิดด้วยเหตุผลคล้ายกัน แต่เพิ่มมุมมองอื่น ตัวอย่าง: สมมติว่าคลาสการบริโภคUserManagementต้องการให้คุณสมบัติUser.Nameถูกแยกเป็นUser.LastNameและUser.FirstNameคลาสUserAuthenticationต้องถูกแก้ไขด้วยเช่นกัน

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

class User : IUserAuthenticationInfo, IUserLocationInfo { ... }

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


1
เมื่อผู้ใช้มีความซับซ้อนจะมีการระเบิดอินเทอร์เฟซย่อยที่เป็นไปได้ combinatorial เช่นถ้าผู้ใช้มีเพียง 3 คุณสมบัติมี 7 ชุดที่เป็นไปได้ ข้อเสนอของคุณฟังดูดี แต่ใช้การไม่ได้
user949300

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

2

เมื่อเผชิญหน้ากับปัญหานี้ในรหัสของฉันเองฉันได้ข้อสรุปว่าคลาส / วัตถุจำลองพื้นฐานเป็นคำตอบ

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

กฏของฉันง่ายๆสำหรับที่เก็บคือ:

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

  • โดยที่เมธอดใช้พารามิเตอร์มากกว่า 2 ตัวพารามิเตอร์ควรถูกจัดกลุ่มเข้าด้วยกันเป็นวัตถุจำลอง

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


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

และใช่มันเป็นเรื่องปกติที่จะมี 2 รุ่นที่แตกต่างพร้อมคุณสมบัติที่เหมือนกันซึ่งให้บริการชั้น / วัตถุประสงค์ที่แตกต่างกัน (เช่น ViewModels vs POCOs)


2

เรามาตรวจสอบแต่ละด้านของ SOLID กัน:

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

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

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

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

การแยกUserขึ้นจริง ๆ แล้วจะน่าเกลียดถ้า subobjects เหลื่อมกันคนจะสับสนว่าจะเลือกอันไหนถ้าฟิลด์ที่ต้องการทั้งหมดมาจากการทับซ้อน หากคุณแยกลำดับขั้น (เช่นคุณมีUserMarketSegmentสิ่งใดบ้างUserLocation) ผู้คนจะไม่แน่ใจว่าฟังก์ชันที่พวกเขาเขียนอยู่ในระดับใด: มันจัดการกับข้อมูลผู้ใช้ในLocation ระดับหรือที่MarketSegmentระดับหรือไม่? มันไม่ได้ช่วยอะไรเลยอย่างแน่นอนว่าสิ่งนี้สามารถเปลี่ยนแปลงได้ตลอดเวลานั่นคือคุณกลับมาที่การเปลี่ยนลายเซ็นของฟังก์ชั่น

กล่าวอีกนัยหนึ่ง: ยกเว้นว่าคุณรู้จักโดเมนของคุณจริงๆและมีความคิดที่ชัดเจนว่าโมดูลใดที่เกี่ยวข้องกับแง่มุมใด ๆ ของUserมันไม่คุ้มที่จะปรับปรุงโครงสร้างของโปรแกรม


1

นี่เป็นคำถามที่น่าสนใจจริงๆ มันขึ้นอยู่กับ

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

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

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


1

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

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

interface IIdentifieable
{
    Guid ID { get; }
}

หรือ

interface INameable
{
    string Name { get; }
}

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


1

นี่คือสิ่งที่ฉันพบเป็นครั้งคราว:

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

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

ยิ่งแย่ไปกว่านั้นเมื่อคุณมีตัวอย่างUserคุณจะต้องรู้ว่ามันมาจากที่ใดเพื่อที่คุณจะได้ทราบว่ามีทรัพย์สินใดบ้าง คุณไม่ต้องการที่จะรู้ว่า

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

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

หากเป็นไปได้ให้ตั้งค่าตัวอย่างที่เหมาะสมสำหรับนักพัฒนาคนต่อไปโดยส่งเฉพาะสิ่งที่คุณต้องผ่าน

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