SRP กล่าวโดยไม่มีเงื่อนไขที่ไม่แน่นอนว่าคลาสควรมีเหตุผลเดียวเท่านั้นที่จะเปลี่ยนแปลง
การแยกคลาส "รายงาน" ในคำถามมีสามวิธีดังนี้:
printReport
getReportData
formatReport
การเพิกเฉยต่อความซ้ำซ้อนReport
ที่ใช้ในทุกวิธีจะเห็นได้ง่ายว่าเหตุใดจึงละเมิด SRP
คำว่า "พิมพ์" หมายถึง UI บางประเภทหรือเครื่องพิมพ์จริง คลาสนี้มี UI หรือตรรกะการนำเสนอจำนวนหนึ่ง การเปลี่ยนแปลงข้อกำหนด UI จะทำให้การเปลี่ยนReport
คลาสเป็นสิ่งจำเป็น
คำว่า "data" หมายถึงโครงสร้างข้อมูลบางชนิด แต่ไม่ได้ระบุอะไรจริงๆ (XML? JSON? CSV?) โดยไม่คำนึงว่าหาก "เนื้อหา" ของรายงานเปลี่ยนแปลงไปก็จะทำให้วิธีการนี้ มีการเชื่อมต่อกับฐานข้อมูลหรือโดเมน
formatReport
เป็นเพียงชื่อที่น่ากลัวสำหรับวิธีการทั่วไป แต่ฉันถือว่าโดยมองไปที่ว่ามันอีกครั้งมีสิ่งที่จะทำอย่างไรกับ UI และอาจจะเป็นลักษณะที่แตกต่างกันของ UI printReport
กว่า ดังนั้นอีกเหตุผลที่ไม่เกี่ยวข้องกับการเปลี่ยนแปลง
ดังนั้นคลาสนี้อาจเป็นคู่กับฐานข้อมูลอุปกรณ์หน้าจอ / เครื่องพิมพ์และตรรกะการจัดรูปแบบภายในบางอย่างสำหรับบันทึกหรือเอาต์พุตไฟล์หรืออะไรก็ตาม ด้วยการมีฟังก์ชั่นทั้งสามในชั้นเดียวคุณจะเพิ่มจำนวนการพึ่งพาและเพิ่มความน่าจะเป็นที่การพึ่งพาหรือการเปลี่ยนแปลงข้อกำหนดใด ๆ จะทำให้ชั้นนี้แตก (หรืออย่างอื่นที่ขึ้นอยู่กับมัน)
ส่วนหนึ่งของปัญหาที่นี่คือคุณได้เลือกตัวอย่างที่มีหนามเป็นพิเศษ คุณอาจจะไม่ได้เรียนที่เรียกว่าReport
แม้ว่าจะเพียง แต่สิ่งหนึ่งเพราะ ... สิ่งที่รายงาน? สัตว์ "แตกต่าง" ไม่ใช่ทั้งหมดหรือไม่ขึ้นอยู่กับข้อมูลที่แตกต่างกันและข้อกำหนดที่แตกต่างกันใช่ไหม และไม่ได้เป็นรายงานสิ่งที่มีอยู่แล้วได้รับการจัดรูปแบบทั้งสำหรับหน้าจอหรือสำหรับการพิมพ์?
แต่เมื่อมองย้อนกลับไปและสร้างชื่อที่เป็นรูปธรรม - เรียกมันว่าIncomeStatement
(รายงานที่พบบ่อยมาก) - สถาปัตยกรรม "SRPed" ที่เหมาะสมจะมีสามประเภท:
IncomeStatement
- โดเมนและ / หรือคลาสโมเดลที่มีและ / หรือคำนวณข้อมูลที่ปรากฏในรายงานที่จัดรูปแบบ
IncomeStatementPrinter
IPrintable<T>
ซึ่งอาจจะใช้บางอินเตอร์เฟซมาตรฐานเช่น มีวิธีการหนึ่งที่สำคัญPrint(IncomeStatement)
และอาจมีวิธีการหรือคุณสมบัติอื่น ๆ สำหรับการกำหนดการตั้งค่าเฉพาะสำหรับการพิมพ์
IncomeStatementRenderer
ซึ่งจัดการการเรนเดอร์หน้าจอและคล้ายกับคลาสพรินเตอร์มาก
คุณยังสามารถเพิ่มในที่สุดก็เรียนที่มีคุณลักษณะเฉพาะเจาะจงมากขึ้นเช่น/IncomeStatementExporter
IExportable<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 ควรมีตัวแปรสถานะหนึ่งหรือสองตัวเท่านั้น เสื้อคลุมบาง ๆ นั้นแทบจะไม่ถูกคาดหวังว่าจะทำอะไรที่มีประโยชน์อย่างแท้จริง ดังนั้นอย่าไปลงน้ำด้วยสิ่งนี้