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

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

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

มีแนวทางหลักสองแนวทางในการต่อสู้กับความซับซ้อน ซึ่งทั้งสองแนวทางจะถูก กล่าวถึงในหนังสือเล่มนี้ แนวทางแรกคือการกำจัดความซับซ้อนโดยการทำให้โค้ดเรียบง่ายขึ้นและชัดเจนขึ้น ตัวอย่างเช่น ความซับซ้อนสามารถลดลงได้โดยการกำจัด special cases หรือการใช้ identifiers อย่างสม่ำเสมอ

แนวทางที่สองในการจัดการกับความซับซ้อนคือการ encapsulate มัน เพื่อให้โปรแกรมเมอร์สามารถทำงานบนระบบได้โดยไม่ต้องเผชิญกับความซับซ้อนทั้งหมดในคราวเดียว แนวทางนี้เรียกว่า modular design ในการออกแบบแบบ modular ระบบซอฟต์แวร์จะถูกแบ่งออกเป็น modules เช่น classes ในภาษาที่เป็น object-oriented โดย modules ถูกออกแบบให้ค่อนข้างเป็นอิสระจากกัน เพื่อให้โปรแกรมเมอร์สามารถทำงานบน module หนึ่งได้โดยไม่ต้องเข้าใจรายละเอียดของ modules อื่นๆ

เนื่องจากซอฟต์แวร์ปรับเปลี่ยนได้ง่าย การออกแบบซอฟต์แวร์จึงเป็นกระบวนการที่ต่อเนื่องตลอดวงจรชีวิตของระบบซอฟต์แวร์ ซึ่งทำให้การออกแบบซอฟต์แวร์แตกต่างจากการออกแบบระบบทางกายภาพอย่างอาคาร เรือ หรือสะพาน อย่างไรก็ตาม การออกแบบซอฟต์แวร์ไม่ได้ถูกมองเช่นนี้เสมอไป ตลอดประวัติศาสตร์การเขียนโปรแกรมส่วนใหญ่ การออกแบบถูกรวมไว้ที่จุดเริ่มต้นของโปรเจกต์ เหมือนกับในสาขาวิศวกรรมอื่นๆ รูปแบบที่สุดโต่งของแนวทางนี้เรียกว่า waterfall model ซึ่งโปรเจกต์ถูกแบ่งออกเป็นเฟสต่างๆ เช่น requirements definition, design, coding, testing และ maintenance ใน waterfall model แต่ละเฟสจะเสร็จสมบูรณ์ก่อนที่เฟสถัดไปจะเริ่มต้น ในหลายกรณีคนที่แตกต่างกันรับผิดชอบแต่ละเฟส ระบบทั้งหมดถูกออกแบบในครั้งเดียวในช่วง design phase การออกแบบจะถูกกำหนดไว้เมื่อสิ้นสุดเฟสนี้ และบทบาทของเฟสถัดไปคือการขยายรายละเอียดและนำการออกแบบนั้นไปใช้งาน

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

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

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

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

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

หนังสือเล่มนี้มีเป้าหมายหลักสองประการ ประการแรกคือการอธิบายธรรมชาติของความซับซ้อนของซอฟต์แวร์: "ความซับซ้อน" หมายถึงอะไร ทำไมมันถึงสำคัญ และคุณจะรู้ได้อย่างไรว่าเมื่อใดที่โปรแกรมมีความซับซ้อนที่ไม่จำเป็น เป้าหมายที่สองซึ่งท้าทายกว่าคือการนำเสนอเทคนิคที่คุณสามารถใช้ในระหว่างกระบวนการพัฒนาซอฟต์แวร์เพื่อลดความซับซ้อนให้น้อยที่สุด น่าเสียดายที่ไม่มีสูตรสำเร็จรูปที่รับประกันการออกแบบซอฟต์แวร์ที่ยอดเยี่ยมได้ แต่ฉันจะนำเสนอแนวคิดระดับสูงหลายอย่างที่ใกล้เคียงกับแนวคิดเชิงปรัชญา เช่น "classes should be deep" หรือ "define errors out of existence" แนวคิดเหล่านี้อาจไม่สามารถระบุการออกแบบที่ดีที่สุดได้ในทันที แต่คุณสามารถใช้มันเพื่อเปรียบเทียบทางเลือกในการออกแบบและนำทางการสำรวจพื้นที่การออกแบบของคุณ

1.1    How to use this book (วิธีการใช้หนังสือเล่มนี้)

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

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

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

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

เกือบทั้งหมดของตัวอย่างในหนังสือเล่มนี้เป็น Java หรือ C++ และการอภิปรายส่วนใหญ่อยู่ในแง่ของการออกแบบ classes ในภาษาที่เป็น object-oriented อย่างไรก็ตาม แนวคิดเหล่านี้ใช้ได้กับโดเมนอื่นๆ เช่นกัน เกือบทั้งหมดของแนวคิดที่เกี่ยวข้องกับ methods สามารถนำไปใช้กับ functions ในภาษาที่ไม่มีคุณสมบัติแบบ object-oriented เช่น C ได้ แนวคิดการออกแบบยังใช้ได้กับ modules อื่นๆ ที่ไม่ใช่ classes เช่น subsystems หรือ network services

เมื่อมีพื้นฐานเหล่านี้แล้ว มาดูรายละเอียดเพิ่มเติมเกี่ยวกับสิ่งที่ทำให้เกิดความซับซ้อน และวิธีทำให้ระบบซอฟต์แวร์เรียบง่ายขึ้น