นวกรรมิกที่ตรวจสอบความถูกต้องของข้อโต้แย้งนั้นละเมิด SRP หรือไม่?


66

ฉันพยายามปฏิบัติตาม Single Responsibility Principle (SRP) ให้มากที่สุดเท่าที่จะเป็นไปได้และคุ้นเคยกับรูปแบบที่แน่นอน (สำหรับ SRP เกี่ยวกับวิธีการ) อาศัยการมอบหมายอย่างมาก ฉันต้องการทราบว่าวิธีการนี้เป็นวิธีที่ดีหรือหากมีปัญหารุนแรงกับมัน

ตัวอย่างเช่นในการตรวจสอบอินพุตให้กับ constructor ฉันสามารถแนะนำวิธีต่อไปนี้ ( Streamอินพุตเป็นแบบสุ่มอาจเป็นอะไรก็ได้)

private void CheckInput(Stream stream)
{
    if(stream == null)
    {
        throw new ArgumentNullException();
    }

    if(!stream.CanWrite)
    {
        throw new ArgumentException();
    }
}

วิธีนี้ (arguably) ทำมากกว่าหนึ่งอย่าง

  • ตรวจสอบอินพุต
  • โยนข้อยกเว้นที่แตกต่างกัน

เพื่อให้เป็นไปตาม SRP ฉันจึงเปลี่ยนตรรกะเป็น

private void CheckInput(Stream stream, 
                        params (Predicate<Stream> predicate, Action action)[] inputCheckers)
{
    foreach(var inputChecker in inputCheckers)
    {
        if(inputChecker.predicate(stream))
        {
            inputChecker.action();
        }
    }
}

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

bool StreamIsNull(Stream s)
{
    return s == null;
}

bool StreamIsReadonly(Stream s)
{
    return !s.CanWrite;
}

void Throw<TException>() where TException : Exception, new()
{
    throw new TException();
}

และสามารถโทรCheckInputเช่น

CheckInput(stream,
    (this.StreamIsNull, this.Throw<ArgumentNullException>),
    (this.StreamIsReadonly, this.Throw<ArgumentException>))

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


26
ฉันสามารถโต้แย้งว่าCheckInputยังคงทำหลายสิ่ง: มันซ้ำทั้งอาร์เรย์และการเรียกฟังก์ชันเพรดิเคตและเรียกใช้ฟังก์ชันการกระทำ นั่นไม่ใช่การละเมิด SRP หรือไม่?
Bart van Ingen Schenau

8
ใช่นั่นคือประเด็นที่ฉันพยายามทำ
Bart van Ingen Schenau

135
เป็นสิ่งสำคัญที่ต้องจำไว้ว่ามันเป็นหลักการความรับผิดชอบเดียว; ไม่ใช่หลักการกระทำเดียว มีความรับผิดชอบเดียว: ตรวจสอบว่าสตรีมถูกกำหนดและเขียนได้
David Arno

40
โปรดทราบว่าประเด็นทั้งหมดของหลักการซอฟต์แวร์เหล่านี้คือการทำให้โค้ดอ่านและบำรุงรักษาได้ดีขึ้น CheckInput ต้นฉบับของคุณนั้นง่ายต่อการอ่านและบำรุงรักษามากกว่ารุ่นที่คุณได้รับการปรับสภาพใหม่ ในความเป็นจริงถ้าฉันเคยเจอวิธี CheckInput สุดท้ายของคุณใน codebase ฉันจะทำให้มันหมดแล้วเขียนมันใหม่เพื่อให้เข้ากับสิ่งที่คุณมี
17 จาก 26

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

คำตอบ:


151

SRP อาจเป็นหลักการซอฟต์แวร์ที่เข้าใจผิดที่สุด

แอปพลิเคชันซอฟต์แวร์สร้างขึ้นจากโมดูลซึ่งสร้างขึ้นจากโมดูลซึ่งสร้างขึ้นจาก ...

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

SRP ไม่ได้เกี่ยวกับการกระทำของอะตอมเดี่ยว มันเกี่ยวกับการมีความรับผิดชอบเพียงอย่างเดียวแม้ว่าความรับผิดชอบนั้นจะต้องมีการกระทำหลายอย่าง ... และในที่สุดมันก็เกี่ยวกับการบำรุงรักษาและการทดสอบ :

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

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

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

มาเปรียบเทียบกัน

Constructor(Stream stream) {
    CheckInput(stream);
    // ...
}

เมื่อเทียบกับ:

Constructor(Stream stream) {
    CheckInput(stream,
        (this.StreamIsNull, this.Throw<ArgumentNullException>),
        (this.StreamIsReadonly, this.Throw<ArgumentException>));
    // ...
}

ทีนี้CheckInputลดน้อยลง ... แต่ผู้โทรทำได้มากกว่า!

คุณได้เปลี่ยนรายการของข้อกำหนดจากCheckInputที่พวกเขาจะห่อหุ้มเพื่อConstructorที่พวกเขาจะมองเห็นได้

มันเป็นการเปลี่ยนแปลงที่ดีหรือไม่? มันขึ้นอยู่กับ:

  • หากCheckInputมีการเรียกเฉพาะที่นั่น: มันเป็นที่ถกเถียงกันอยู่ในมือข้างหนึ่งมันทำให้ความต้องการที่มองเห็นในทางกลับกันมัน clutters รหัส;
  • หากCheckInputมีการเรียกหลาย ๆ ครั้งด้วยข้อกำหนดเดียวกันนั่นก็เป็นการละเมิด DRY และคุณมีปัญหาการห่อหุ้ม

มันเป็นสิ่งสำคัญที่จะตระหนักว่าเป็นความรับผิดชอบที่เดียวอาจบ่งบอกถึงจำนวนมากของการทำงาน "สมอง" ของรถยนต์ที่ขับขี่ด้วยตนเองมีความรับผิดชอบเดียว:

ขับรถไปยังปลายทาง

มันเป็นความรับผิดชอบที่เดียว แต่ต้องมีการประสานงานตันของเซ็นเซอร์และนักแสดงพาจำนวนมากของการตัดสินใจและยังมีความต้องการที่ขัดแย้งกันอาจจะเป็นที่ 1 ...

... อย่างไรก็ตามมันถูกห่อหุ้มทั้งหมด ดังนั้นลูกค้าไม่สนใจ

1 ความปลอดภัยของผู้โดยสารความปลอดภัยของผู้อื่นการเคารพต่อกฎระเบียบ ...


2
ฉันคิดว่าวิธีที่คุณใช้คำว่า "encapsulation" และอนุพันธ์นั้นทำให้เกิดความสับสน นอกจากนั้นคำตอบที่ยอดเยี่ยม!
Fabio Turati

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

13
@SavaB: แน่นอน แต่หลักการยังคงเหมือนเดิม โมดูลควรมีความรับผิดชอบเดี่ยวแม้ว่าขอบเขตที่ใหญ่กว่าองค์ประกอบของมัน
Matthieu M.

3
@ user949300 เอาละ "ขับรถ" แค่ไหน จริงๆแล้ว "การขับขี่" เป็นความรับผิดชอบและ "ปลอดภัย" และ "ถูกกฎหมาย" เป็นข้อกำหนดเกี่ยวกับวิธีการเติมเต็มความรับผิดชอบนั้น และเรามักจะระบุข้อกำหนดเมื่อระบุความรับผิดชอบ
Brian McCutchon

1
"SRP อาจเป็นหลักการซอฟต์แวร์ที่เข้าใจผิดที่สุด" เห็นได้จากคำตอบนี้ :)
Michael

41

การอ้างอิงลุงบ๊อบเกี่ยวกับ SRP ( https://8thlight.com/blog/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html ):

หลักการความรับผิดชอบเดียว (SRP) ระบุว่าแต่ละโมดูลซอฟต์แวร์ควรมีเหตุผลเดียวเท่านั้นที่จะเปลี่ยน

... หลักการนี้เป็นเรื่องของคน

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

... นี่คือเหตุผลที่เราไม่ใส่ SQL ใน JSP นี่คือเหตุผลที่เราไม่ได้สร้าง HTML ในโมดูลที่คำนวณผลลัพธ์ นี่คือเหตุผลที่กฎทางธุรกิจไม่ควรรู้สคีมาฐานข้อมูล นี่คือเหตุผลที่เราแยกข้อกังวลออก

เขาอธิบายว่าโมดูลซอฟต์แวร์จะต้องจัดการกับความกังวลเฉพาะ ดังนั้นตอบคำถามของคุณ:

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

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


4
คำตอบที่ดีที่สุด เพื่ออธิบายรายละเอียดเกี่ยวกับ OP ถ้าการตรวจสอบยามมาจากสองผู้มีส่วนได้เสียที่แตกต่างกัน / หน่วยงานแล้วcheckInputs()ควรจะแยกพูดเข้าและcheckMarketingInputs() checkRegulatoryInputs()มิฉะนั้นจะเป็นการดีที่จะรวมพวกเขาทั้งหมดไว้ในวิธีการเดียว
user949300

36

ไม่การเปลี่ยนแปลงนี้ไม่ได้รับแจ้งจาก SRP

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

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

การวางประเภทบนเมธอดไม่ใช่การละเมิดหลักการความรับผิดชอบเดี่ยว ไม่ใช่วิธีการบังคับใช้เงื่อนไขเบื้องต้นหรือยืนยัน Postconditions


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

23

ไม่ใช่ความรับผิดชอบทั้งหมดที่ถูกสร้างขึ้นเท่ากัน

ป้อนคำอธิบายรูปภาพที่นี่

ป้อนคำอธิบายรูปภาพที่นี่

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

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

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

ดังนั้นเมื่อคุณเสนอ

CheckInput(Stream stream);

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


21

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

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

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

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


14

บทบาท CheckInput

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

การเพิ่มรูปแบบนี้สำหรับภาคแสดงอาจมีประโยชน์หากคุณไม่ต้องการตรรกะในการตรวจสอบอินพุตที่จะใส่ในชั้นเรียนของคุณ แต่รูปแบบนี้ดูเหมือนจะค่อนข้างชัดเจนสำหรับสิ่งที่คุณพยายามจะทำ มันอาจจะตรงไปตรงกว่าที่จะเพียงแค่อินเตอร์เฟซIStreamValidatorด้วยวิธีการเดียวisValid(Stream)ถ้านั่นคือสิ่งที่คุณต้องการที่จะได้รับ การใช้คลาสใด ๆIStreamValidatorสามารถใช้เพรดิเคตเช่นStreamIsNullหรือStreamIsReadonlyหากพวกเขาต้องการ แต่กลับไปที่จุดศูนย์กลางมันเป็นการเปลี่ยนแปลงที่ไร้สาระที่จะสร้างผลประโยชน์ในการรักษาหลักการความรับผิดชอบเดี่ยว

ตรวจสติ

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

ข้อสรุป

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


4

หลักการกึกก้องไม่ระบุชิ้นส่วนของรหัสควร "ทำสิ่งเดียวเท่านั้น"

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

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

ตัวอย่างจริงของการละเมิด SRP จะเป็นรหัสดังนี้:

var message = "Your package will arrive before " + DateTime.Now.AddDays(14);

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


คลาสที่แตกต่างกันสำหรับแทบทุกความต้องการฟังดูเหมือนฝันร้ายที่ไม่บริสุทธิ์
whatsisname

@whatsisname: บางที SRP อาจไม่เหมาะกับคุณ ไม่มีหลักการออกแบบสำหรับทุกประเภทและทุกขนาดของโครงการ (แต่โปรดทราบว่าเรากำลังพูดถึงข้อกำหนดอิสระเท่านั้น(เช่นสามารถเปลี่ยนแปลงได้อย่างอิสระ) ไม่ใช่แค่ข้อกำหนดใด ๆ หลังจากนั้นมันก็แค่ขึ้นอยู่กับว่าพวกมันถูกระบุอย่างละเอียดขนาดไหน)
JacquesB

ฉันคิดว่ามันมากกว่า SRP ต้องการองค์ประกอบของการตัดสินสถานการณ์ซึ่งเป็นการยากที่จะอธิบายในวลีลวงเดียว
whatsisname

@whatsisname: ฉันเห็นด้วยอย่างยิ่ง
JacquesB

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

3

ฉันชอบประเด็นนี้จากคำตอบของ @ EricLippert :

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

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

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

มันเป็นไปได้จริง ๆ ที่จะเรียงลำดับการทำเช่นนี้ใน C # เราสามารถจับตัวอักษรnullในเวลารวบรวมแล้วจับที่ไม่ใช่ตัวอักษรnullในเวลาทำงาน นั่นไม่ดีเท่าการตรวจสอบแบบคอมไพล์แบบเต็มเวลา แต่เป็นการปรับปรุงที่เข้มงวดเกินกว่าที่จะไม่รวบรวมเวลา

ดังนั้นคุณรู้ว่า C # มีNullable<T>อะไรบ้าง? ลองย้อนกลับไปและสร้างNonNullable<T>:

public struct NonNullable<T> where T : class
{
    public T Value { get; private set; }
    public NonNullable(T value)
    {
        if (value == null) { throw new NullArgumentException(); }
        this.Value = value;
    }
    //  Ease-of-use:
    public static implicit operator T(NonNullable<T> value) { return value.Value; }
    public static implicit operator NonNullable<T>(T value) { return new NonNullable<T>(value); }

    //  Hack-ish overloads that prevent null-literals from being implicitly converted into NonNullable<T>'s.
    public static implicit operator NonNullable<T>(Tuple<T> value) { return new NonNullable<T>(value.Item1); }
    public static implicit operator NonNullable<T>(Tuple<T, T> value) { return new NonNullable<T>(value.Item1); }
}

ตอนนี้แทนที่จะเขียน

public void Foo(Stream stream)
{
  if (stream == null) { throw new NullArgumentException(); }

  // ...method code...
}

, แค่เขียน:

public void Foo(NonNullable<Stream> stream)
{
  // ...method code...
}

จากนั้นมีสามกรณีใช้งาน:

  1. ผู้ใช้โทรFoo()ด้วยค่าที่ไม่เป็นโมฆะStream:

    Stream stream = new Stream();
    Foo(stream);
    

    NonNullable<>นี้เป็นที่ต้องการกรณีการใช้งานและจะทำงานร่วมกับหรือโดยไม่ต้อง

  2. ผู้ใช้โทรFoo()ด้วยค่า null Stream:

    Stream stream = null;
    Foo(stream);
    

    นี่เป็นข้อผิดพลาดในการโทร ที่นี่NonNullable<>ช่วยแจ้งผู้ใช้ว่าไม่ควรทำสิ่งนี้ แต่ไม่ได้หยุดพวกเขา NullArgumentExceptionทั้งสองวิธีนี้จะส่งผลในเวลาทำงาน

  3. ผู้ใช้โทรFoo()ด้วยnull:

    Foo(null);

    nullจะไม่แปลงเป็น a โดยปริยายNonNullable<>ดังนั้นผู้ใช้จะได้รับข้อผิดพลาดใน IDE ก่อนเวลาทำงาน นี่คือการมอบหมายการตรวจสอบโมฆะให้กับระบบประเภทเช่นเดียวกับ SRP จะแนะนำ

คุณสามารถขยายวิธีนี้เพื่อยืนยันสิ่งอื่น ๆ เกี่ยวกับข้อโต้แย้งของคุณได้เช่นกัน ตัวอย่างเช่นเนื่องจากคุณต้องการกระแสข้อมูลที่เขียนได้คุณสามารถกำหนดการstruct WriteableStream<T> where T:Streamตรวจสอบทั้งnullและstream.CanWriteในตัวสร้าง นี่จะเป็นการตรวจสอบชนิดรันไทม์ แต่:

  1. มันตกแต่งประเภทด้วยWriteableStreamqualifier ส่งสัญญาณความต้องการที่จะโทร

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

  3. มันเป็นไปตาม SRP ที่ดีขึ้นโดยการผลักดันหน้าที่ตรวจสอบประเภทไปยังระบบประเภท (ตามที่ขยายโดยผู้ตกแต่งทั่วไป)


3

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

นี่คือสิ่งStreamที่ดำเนินการหากผ่านการตรวจสอบแล้ว:

class Stream
{
    public void someAction()
    {
        if(!stream.canWrite)
        {
            throw new ArgumentException();
        }

        System.out.println("My action");
    }
}

แต่ตอนนี้เรากำลังละเมิด SRP! "คลาสควรมีเหตุผลเดียวเท่านั้นที่จะเปลี่ยน" เรามีการผสมผสานระหว่าง 1) การตรวจสอบและ 2) ตรรกะจริง เรามีสองเหตุผลที่อาจต้องเปลี่ยน

เราสามารถแก้ปัญหานี้ด้วยการตกแต่งการตรวจสอบ อันดับแรกเราต้องแปลงStreamให้เป็นส่วนต่อประสานและนำไปใช้ในระดับที่เป็นรูปธรรม

interface Stream
{
    void someAction();
}

class DefaultStream implements Stream
{
    @Override
    public void someAction()
    {
        System.out.println("My action");
    }
}

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

class WritableStream implements Stream
{
    private final Stream stream;

    public WritableStream(final Stream stream)
    {
        this.stream = stream;
    }

    @Override
    public void someAction()
    {
        if(!stream.canWrite)
        {
            throw new ArgumentException();
        }
        stream.someAction();
    }
}

ตอนนี้เราสามารถเขียนสิ่งเหล่านี้ในแบบที่เราชอบ:

final Stream myStream = new WritableStream(
    new DefaultStream()
);

ต้องการการตรวจสอบเพิ่มเติมหรือไม่ เพิ่มมัณฑนากรอื่น


1

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

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

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


0

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

ให้ดูตัวอย่างที่ไม่ดีในรหัสหลอก

class Math
    private int a;
    private int b;
    def constructor(int x, int y) 
        if(x != null)
          a = x
        else if(x < 0)
          a = abs(x)
        else if (x == -1)
          throw "Some Silly Error"
        else
          a = 0
        end
        if(y != null)
           b = y
        else if(y < 0)
           b = abs(y)
        else if(y == -1)
           throw "Some Silly Error"
        else
         b = 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

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

นี่คือ SRP ที่ถูกต้องเพราะตัวสร้างกำลังทำสิ่งเดียวเท่านั้น มันกำลังเตรียมวัตถุทางคณิตศาสตร์ อย่างไรก็ตามมันไม่สามารถบำรุงรักษาได้มาก มันละเมิด DRY

ดังนั้นขอผ่านอีกครั้ง

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        cleanX(x)
        cleanY(y)
    end
    def cleanX(int x)
        if(x != null)
          a = x
        else if(x < 0)
          a = abs(x)
        else if (x == -1)
          throw "Some Silly Error"
        else
          a = 0
        end
   end
   def cleanY(int y)
        if(y != null)
           b = y
        else if(y < 0)
           b = abs(y)
        else if(y == -1)
           throw "Some Silly Error"
        else
         b = 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

ในรอบนี้เราดีขึ้นเล็กน้อยเกี่ยวกับ DRY แต่เรายังมีวิธีที่จะไปกับ DRY SRP ในทางกลับกันดูเหมือนจะปิดไปเล็กน้อย ตอนนี้เรามีสองฟังก์ชั่นที่มีงานเหมือนกัน ทั้ง cleanX และ cleanY ฆ่าเชื้ออินพุต

ให้อีกครั้ง

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        a = clean(x)
        b = clean(y)
    end
    def clean(int i)
        if(i != null)
          return i
        else if(i < 0)
          return abs(i)
        else if (i == -1)
          throw "Some Silly Error"
        else
          return 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

ในที่สุดตอนนี้ก็ดีกว่า DRY และ SRP ก็ดูเหมือนจะตกลงกัน เรามีที่เดียวที่ทำหน้าที่ "ฆ่าเชื้อโรค"

ในทางทฤษฎีแล้วรหัสสามารถบำรุงรักษาได้ดีกว่าและดีกว่า แต่เมื่อเราไปแก้ไขข้อผิดพลาดและทำให้รหัสแน่นขึ้นเราต้องทำในที่เดียวเท่านั้น

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        a = clean(x)
        b = clean(y)
    end
    def clean(int i)
        if(i == null)
          return 0
        else if (i == -1)
          throw "Some Silly Error"
        else
          return abs(i)
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

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


-3

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

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

หากคุณต้องการบังคับใช้สัญญาดังกล่าวจริงๆลองใช้Debug.Assertหรือสิ่งที่คล้ายกันเพื่อลดความยุ่งเหยิง:

public AClassThatDefinitelyNeedsAWritableStream(Stream stream)
{
   Assert.That(stream.CanWrite, "Put crucial information here, and not inane bloat.");

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