อะไรคือวิธีปฏิบัติที่จะนำ SRP ไปใช้?


11

ผู้คนใช้เทคนิคการปฏิบัติอะไรเพื่อตรวจสอบว่าชั้นเรียนมีการละเมิดหลักการความรับผิดชอบเดียวหรือไม่?

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

วิธีเดียวที่ฉันพบคือใช้ประโยค"......... ควร ......... ตัวเอง" โดยที่ช่องว่างแรกคือชื่อคลาสและต่อมาคือชื่อเมธอด (รับผิดชอบ)

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

มีวิธีเพิ่มเติมในการตรวจสอบ SRP หรือไม่

บันทึก:

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

UPDATE

คลาสรายงาน

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

ตัวอย่างจากที่นี่


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

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

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

และผู้ลงคะแนนเสียงโปรดอธิบายว่าทำไมต้องปรับปรุงโพสต์?!
Songo

คำตอบ:


7

SRP กล่าวโดยไม่มีเงื่อนไขที่ไม่แน่นอนว่าคลาสควรมีเหตุผลเดียวเท่านั้นที่จะเปลี่ยนแปลง

การแยกคลาส "รายงาน" ในคำถามมีสามวิธีดังนี้:

  • printReport
  • getReportData
  • formatReport

การเพิกเฉยต่อความซ้ำซ้อนReportที่ใช้ในทุกวิธีจะเห็นได้ง่ายว่าเหตุใดจึงละเมิด SRP

  • คำว่า "พิมพ์" หมายถึง UI บางประเภทหรือเครื่องพิมพ์จริง คลาสนี้มี UI หรือตรรกะการนำเสนอจำนวนหนึ่ง การเปลี่ยนแปลงข้อกำหนด UI จะทำให้การเปลี่ยนReportคลาสเป็นสิ่งจำเป็น

  • คำว่า "data" หมายถึงโครงสร้างข้อมูลบางชนิด แต่ไม่ได้ระบุอะไรจริงๆ (XML? JSON? CSV?) โดยไม่คำนึงว่าหาก "เนื้อหา" ของรายงานเปลี่ยนแปลงไปก็จะทำให้วิธีการนี้ มีการเชื่อมต่อกับฐานข้อมูลหรือโดเมน

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

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

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

แต่เมื่อมองย้อนกลับไปและสร้างชื่อที่เป็นรูปธรรม - เรียกมันว่าIncomeStatement(รายงานที่พบบ่อยมาก) - สถาปัตยกรรม "SRPed" ที่เหมาะสมจะมีสามประเภท:

  • IncomeStatement- โดเมนและ / หรือคลาสโมเดลที่มีและ / หรือคำนวณข้อมูลที่ปรากฏในรายงานที่จัดรูปแบบ

  • IncomeStatementPrinterIPrintable<T>ซึ่งอาจจะใช้บางอินเตอร์เฟซมาตรฐานเช่น มีวิธีการหนึ่งที่สำคัญPrint(IncomeStatement)และอาจมีวิธีการหรือคุณสมบัติอื่น ๆ สำหรับการกำหนดการตั้งค่าเฉพาะสำหรับการพิมพ์

  • IncomeStatementRendererซึ่งจัดการการเรนเดอร์หน้าจอและคล้ายกับคลาสพรินเตอร์มาก

  • คุณยังสามารถเพิ่มในที่สุดก็เรียนที่มีคุณลักษณะเฉพาะเจาะจงมากขึ้นเช่น/IncomeStatementExporterIExportable<TReport, TFormat>

สิ่งนี้ทำให้ง่ายขึ้นอย่างมากในภาษาสมัยใหม่ด้วยการแนะนำของ generics และ IoC container รหัสแอปพลิเคชันส่วนใหญ่ไม่จำเป็นต้องพึ่งพาIncomeStatementPrinterคลาสที่เฉพาะเจาะจงสามารถใช้IPrintable<T>และดำเนินการกับรายงานที่พิมพ์ได้ทุกประเภทซึ่งให้ประโยชน์ทั้งหมดที่รับรู้ของReportคลาสพื้นฐานด้วยprintวิธีการและไม่มีการละเมิด SRP ตามปกติ . การนำไปปฏิบัติจริงต้องประกาศเพียงครั้งเดียวในการลงทะเบียนคอนเทนเนอร์ IoC

บางคนเมื่อเผชิญหน้ากับการออกแบบข้างต้นตอบสนองด้วยสิ่งที่ชอบ: "แต่ดูเหมือนว่ารหัสขั้นตอนและจุดทั้งหมดของ OOP คือการทำให้เราห่างไกลจากการแยกข้อมูลและพฤติกรรม!" ที่ผมบอกว่า: ผิด

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

ยกตัวอย่างเช่นงบกำไรขาดทุนที่แท้จริงมีรายได้รวม , ค่าใช้จ่ายรวมและกำไรสุทธิเส้น ระบบการเงินที่ได้รับการออกแบบอย่างเหมาะสมน่าจะไม่เก็บสิ่งเหล่านี้เนื่องจากไม่ใช่ข้อมูลธุรกรรม - ที่จริงแล้วมันเปลี่ยนไปตามการเพิ่มข้อมูลธุรกรรมใหม่ อย่างไรก็ตามการคำนวณบรรทัดเหล่านี้จะเหมือนกันเสมอไม่ว่าคุณจะพิมพ์แสดงหรือส่งออกรายงาน ดังนั้นคุณIncomeStatementชั้นจะมีจำนวนเงินที่ยุติธรรมของพฤติกรรมไปในรูปแบบของgetTotalRevenues(), getTotalExpenses()และgetNetIncome()วิธีการและคนอื่น ๆ อาจจะหลาย มันเป็นวัตถุสไตล์ OOP ของแท้ที่มีพฤติกรรมของตัวเองแม้ว่ามันจะดูเหมือน "ทำ" ไม่มากนักก็ตาม

แต่วิธีformatและprintพวกเขาไม่มีอะไรเกี่ยวข้องกับข้อมูลเอง ในความเป็นจริงมันไม่น่าเป็นไปได้มากที่คุณจะต้องมีวิธีการใช้งานที่หลากหลายเช่นคำแถลงรายละเอียดการจัดการและคำแถลงที่ไม่ละเอียดสำหรับผู้ถือหุ้น การแยกฟังก์ชั่นอิสระเหล่านี้ออกเป็นคลาสที่แตกต่างกันช่วยให้คุณสามารถเลือกการใช้งานที่แตกต่างกันในขณะทำงานโดยไม่ต้องใช้วิธีการเดียวที่เหมาะกับทุกprint(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)วิธี yuck!

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

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


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


+1 คำตอบที่ดีแน่นอน IncomeStatementแต่ฉันแค่สับสนเกี่ยวกับการเรียน ไม่ออกแบบที่นำเสนอของคุณหมายความว่าIncomeStatementจะมีกรณีของIncomeStatementPrinterและIncomeStatementRendererเพื่อที่ว่าเมื่อผมเรียกprint()บนIncomeStatementมันจะมอบหมายการเรียกร้องให้IncomeStatementPrinterแทน?
Songo

@Songo: ไม่อย่างแน่นอน! คุณไม่ควรมีการพึ่งพาแบบวนรอบหากคุณกำลังติดตามโซลิด เห็นได้ชัดว่าคำตอบของฉันไม่ได้ทำให้มันมากพอที่ชัดเจนว่าIncomeStatementชั้นไม่ได้มีprintวิธีการหรือformatวิธีการหรือวิธีการอื่นใดที่ไม่ได้จัดการโดยตรงกับการตรวจสอบหรือจัดการกับข้อมูลรายงานของตัวเอง นั่นคือสิ่งที่ชั้นเรียนเหล่านั้นมีไว้สำหรับ หากคุณต้องการพิมพ์หนึ่งรายการคุณต้องพึ่งพาIPrintable<IncomeStatement>ส่วนต่อประสานที่ลงทะเบียนไว้ในคอนเทนเนอร์
Aaronaught

ฉันเห็นประเด็นของคุณแล้ว อย่างไรก็ตามการพึ่งพาแบบวนรอบอยู่ที่ไหนถ้าฉันฉีดPrinterอินสแตนซ์ในIncomeStatementชั้นเรียน วิธีที่ผมคิดว่ามันคือเมื่อผมเรียกมันจะมอบหมายให้มันIncomeStatement.print() IncomeStatementPrinter.print(this, format)มีวิธีใดที่ผิดปกตินี้ ... คำถามอื่นคุณกล่าวถึงว่าIncomeStatementควรมีข้อมูลที่ปรากฏในรายงานที่จัดรูปแบบแล้วหากฉันต้องการให้อ่านจากฐานข้อมูลหรือจากไฟล์ XML ฉันควรแยกวิธีที่โหลดข้อมูลออกมาหรือไม่ ในคลาสที่แยกต่างหากและมอบหมายการโทรเข้าIncomeStatementหรือไม่
Songo

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

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

2

วิธีที่ฉันตรวจสอบ SRP คือการตรวจสอบทุกวิธี (ความรับผิดชอบ) ของชั้นเรียนและถามคำถามต่อไปนี้:

"ฉันจะต้องเปลี่ยนวิธีการใช้งานฟังก์ชั่นนี้หรือไม่?"

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


1

นี่คือคำพูดจากกฎที่ 8 ของObject Calisthenics :

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

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


2
มุมมองนี้เป็นแบบง่ายๆอย่างสิ้นหวัง แม้แต่ไอน์สไตน์ที่โด่งดัง แต่สมการง่าย ๆ นั้นต้องการตัวแปรสองตัว
Robert Harvey

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

1
ฉันสงสัยว่าไม่แน่นอน VS รัฐไม่เปลี่ยนรูปนอกจากนี้ยังมีการพิจารณาที่สำคัญ
JK

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

@Dunk ฉันไม่เห็นด้วยกับคุณ แต่การสนทนานั้นเป็นหัวข้อสำหรับคำถามทั้งหมด
MattDavey

1

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

แก้ไข: ยังสังเกตเห็นว่าชั้นไม่เปลี่ยนรูป ดังนั้นเมื่อมันถูกสร้างขึ้นคุณจะไม่สามารถเปลี่ยนแปลงอะไรได้เลย คุณสามารถเพิ่ม setFormatter () และ setPrinter () และไม่เจอปัญหามากเกินไป กุญแจ IMHO คือการไม่เปลี่ยนแปลงข้อมูลดิบหลังการเริ่มต้น

public class Report
{
    private ReportData data;
    private ReportDataDao dao;
    private ReportFormatter formatter;
    private ReportPrinter printer;


    /*
     *  Parameterized constructor for depndency injection, 
     *  there are better ways but this is explicit.
     */
    public Report(ReportDataDao dao, 
        ReportFormatter formatter, ReportPrinter printer)
    {
        super();
        this.dao = dao;
        this.formatter = formatter;
        this.printer = printer;
    }

    /*
     * Delegates to the injected printer.
     */
    public void printReport()
    {
        printer.print(formatReport());
    }


    /*
     * Lazy loading of data, delegates to the dao 
     * for the meat of the call.
     */
    public ReportData getReportData()
    {
        if (reportData == null)
        {
            reportData = dao.loadData();
        }
        return reportData;
    }

    /*
     * Delegate to the formatter for formatting 
     * (notice a pattern here).
     */
    public ReportData formatReport()
    {
        formatter.format(getReportData());
    }
}

ขอบคุณสำหรับการใช้งาน ฉันมี 2 สิ่งในบรรทัดที่if (reportData == null)ฉันคิดว่าคุณหมายถึงdataแทน ประการที่สองฉันหวังว่าจะทราบว่าคุณมาถึงการดำเนินการนี้อย่างไร เช่นเดียวกับสาเหตุที่คุณตัดสินใจมอบหมายการโทรทั้งหมดไปยังวัตถุอื่นแทน อีกสิ่งหนึ่งที่ฉันสงสัยอยู่เสมอว่ามันเป็นความรับผิดชอบของรายงานที่จะพิมพ์เองหรือไม่! ทำไมคุณไม่สร้างprinterคลาสแยกต่างหากที่รับค่าreportในคอนสตรัคเตอร์?
Songo

ใช่ reportData = data ขอโทษด้วย การมอบหมายช่วยให้สามารถควบคุมการขึ้นต่อกันของข้อมูลได้อย่างละเอียด ที่รันไทม์คุณสามารถให้การใช้งานทางเลือกสำหรับแต่ละองค์ประกอบ ตอนนี้คุณสามารถมี HtmlPrinter, PdfPrinter, JsonPrinter, ... ฯลฯ ได้อีกด้วยนอกจากนี้ยังมีประโยชน์สำหรับการทดสอบเนื่องจากคุณสามารถทดสอบส่วนประกอบที่ได้รับมอบหมายของคุณในการแยกและรวมอยู่ในวัตถุข้างต้น แน่นอนคุณสามารถคว่ำความสัมพันธ์ระหว่างเครื่องพิมพ์และรายงานได้ฉันแค่ต้องการแสดงให้เห็นว่ามันเป็นไปได้ที่จะแก้ปัญหาด้วยอินเทอร์เฟซสำหรับชั้นเรียนที่ให้ไว้ มันเป็นนิสัยจากการทำงานกับระบบมรดก :)
Heath Lilley

hmmmm ... ดังนั้นถ้าคุณกำลังสร้างระบบตั้งแต่เริ่มต้นคุณจะใช้ตัวเลือกอะไร? Printerระดับที่จะรายงานหรือReportระดับที่ใช้เครื่องพิมพ์หรือไม่ ฉันพบปัญหาที่คล้ายกันก่อนที่จะต้องแยกวิเคราะห์รายงานและฉันโต้เถียงกับ TL ของฉันหากเราควรสร้างโปรแกรมแยกวิเคราะห์ที่ใช้รายงานหรือรายงานควรมีเครื่องมือแยกวิเคราะห์ภายในและparse()มีการมอบหมายให้เรียก
Songo

ฉันจะทำทั้งสองอย่าง ... printer.print (รายงาน) เพื่อเริ่มและ report.print () หากจำเป็นในภายหลัง สิ่งที่ยอดเยี่ยมเกี่ยวกับวิธีปฏิบัติของ printer.print (รายงาน) คือสามารถนำมาใช้ซ้ำได้สูง มันแยกความรับผิดชอบและช่วยให้คุณมีวิธีการอำนวยความสะดวกที่คุณต้องการ บางทีคุณอาจไม่ต้องการให้วัตถุอื่น ๆ ในระบบของคุณต้องรู้เกี่ยวกับ ReportPrinter ดังนั้นโดยการใช้เมธอด print () ในคลาสคุณจะได้รับระดับของ abstaction ที่ทำให้ตรรกะการพิมพ์รายงานของคุณจากโลกภายนอก นี่ยังมีการเปลี่ยนแปลงที่แคบและใช้งานง่าย
Heath Lilley

0

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

class Report {
  void format() {
     text = text.trim();
  }

  void print() {
     new Printer().write(text);
  }
}

วิธีการนั้นง่ายมากมันไม่สมเหตุสมผลที่จะมีReportFormatterหรือReportPrinterคลาส ปัญหาที่เห็นได้ชัดเพียงอย่างเดียวในอินเทอร์เฟซก็คือgetReportDataเพราะมันละเมิดการถามไม่บอกเกี่ยวกับวัตถุที่ไม่ใช่ค่า

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

class Report {
  void format(ReportFormatter formatter) {
     text = formatter.format(text);
  }

  void print(ReportPrinter printer) {
     printer.write(text);
  }
}

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

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

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


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

Updated SRP ขึ้นอยู่กับบริบทหากคุณโพสต์ทั้งคลาส (ในคำถามที่แยกต่างหาก) จะอธิบายได้ง่ายกว่า
Garrett Hall

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

ฉันแค่บอกว่า SRP ขึ้นอยู่กับโค้ดที่คุณไม่ควรใช้อย่างไร้เหตุผล
Garrett Hall

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

0

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

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

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

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public Type1 method3() {
        //use var2 and var3
    }
}

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

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;
    private TypeA varA;
    private TypeB varB;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public TypeA methodA() {
        //use varA and varB
    }

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