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 ควรมีตัวแปรสถานะหนึ่งหรือสองตัวเท่านั้น เสื้อคลุมบาง ๆ นั้นแทบจะไม่ถูกคาดหวังว่าจะทำอะไรที่มีประโยชน์อย่างแท้จริง ดังนั้นอย่าไปลงน้ำด้วยสิ่งนี้