Software Trends (แนวโน้มซอฟต์แวร์)

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

19.1 Object-oriented programming and inheritance (Object-oriented programming และ inheritance)

Object-oriented programming เป็นแนวคิดใหม่ที่สำคัญที่สุดแนวคิดหนึ่งในการพัฒนาซอฟต์แวร์ในช่วง 30-40 ปีที่ผ่านมา มันนำเสนอแนวคิด เช่น class, inheritance, private method และ instance variable หากใช้อย่างรอบคอบ กลไกเหล่านี้จะช่วยให้ได้ซอฟต์แวร์ที่ออกแบบได้ดี ตัวอย่างเช่น private method และ variable สามารถใช้เพื่อรับประกันการซ่อนข้อมูล: ไม่มีโค้ดภายนอก class ใดที่เรียก private method หรือเข้าถึง private variable ได้ ดังนั้นจึงไม่เกิด external dependency

องค์ประกอบสำคัญของ object-oriented programming คือ inheritance Inheritance มีสองรูปแบบที่ส่งผลต่อความซับซ้อนของซอฟต์แวร์ต่างกัน รูปแบบแรก: interface inheritance คือการที่ parent class กำหนด signature ของ method หนึ่งตัวหรือมากกว่า แต่ไม่ implement method นั้น Subclass แต่ละตัวต้อง implement signature แต่ subclass ต่างๆ สามารถ implement method เดียวกันได้วิธีต่างๆ ตัวอย่างเช่น interface อาจกำหนด method สำหรับ I/O operation subclass หนึ่งอาจ implement operation สำหรับ disk file ส่วน subclass อีกตัวอาจ implement operation เดียวกันสำหรับ network socket

Interface inheritance ช่วยลด complexity โดยนำ interface เดียวกันมาใช้สำหรับหลายจุดประสงค์ ความรู้ที่ได้จากการแก้ปัญหาหนึ่ง (เช่นวิธีใช้ I/O interface อ่านและเขียนไฟล์ disk) สามารถนำไปใช้แก้ปัญหาอื่น (เช่นการสื่อสารผ่าน network socket) อีกมุมมองคือ depth: ยิ่ง implementation ที่ต่างกันของ interface มีจำนวนมาก interface ก็ยิ่ง deep มากขึ้น เพื่อให้ interface มี implementation หลายตัว มันต้องจับ feature สำคัญของ implementation ทั้งหมดในขณะที่หลีกเลี่ยงรายละเอียดที่แตกต่างกันระหว่างพวกมัน เรื่องนี้อยู่ที่หัวใจของ abstraction

รูปแบบที่สอง: implementation inheritance คือการที่ parent class ไม่เพียงกำหนด signature แต่ยังมี default implementation ด้วย Subclass สามารถเลือกให้ inherit implementation ของ parent หรือ override มันโดยกำหนด method ใหม่ด้วย signature เดียวกัน ถ้าไม่มี implementation inheritance method implementation เดียวกันอาจต้อง duplicate ใน subclass หลายตัว ซึ่งสร้าง dependency ระหว่าง subclass (การแก้ไขต้องถูก duplicate ในสำเนาทั้งหมด) ดังนั้น implementation inheritance ลด code ที่ต้องแก้ไขเมื่อระบบพัฒนา กล่าวอีกนัยหนึ่งคือลด change amplification problem ที่อธิบายในบท 2

อย่างไรก็ตาม implementation inheritance สร้าง dependency ระหว่าง parent class และ subclass แต่ละตัว Class instance variable ใน parent class มักถูกเข้าถึงโดยทั้ง parent และ child class สิ่งนี้ส่งผลให้เกิด information leakage ระหว่าง class ใน hierarchy และทำให้ยากต่อการแก้ไข class หนึ่งใน hierarchy โดยไม่พิจารณา class อื่น ตัวอย่างเช่น developer ที่แก้ไข parent class อาจต้องตรวจสอบ subclass ทั้งหมดเพื่อให้มั่นใจว่าไม่ทำให้เกิด bug ในทำนองเดียวกัน ถ้า subclass override method ใน parent class developer ของ subclass อาจต้องตรวจสอบ implementation ใน parent ในกรณีที่เลวร้ายที่สุด programmer ต้องมีความรู้สมบูรณ์เกี่ยวกับ class hierarchy ทั้งหมดใต้ parent class เพื่อแก้ไข class ใดๆ Class hierarchy ที่ใช้ implementation inheritance อย่างกว้างขวางมักมีความซับซ้อนสูง

ดังนั้น implementation inheritance ควรใช้อย่างรอบคอบ ก่อนใช้ implementation inheritance ให้พิจารณาว่า composition สามารถให้ประโยชน์เดียวกันได้หรือไม่ ตัวอย่างเช่น อาจเป็นไปได้ที่ใช้ helper class เล็กๆ เพื่อ implement functionality ที่ใช้ร่วมกัน แทนการ inherit method จาก parent class original class แต่ละตัวสามารถสร้างบน feature ของ helper class ได้

ถ้าไม่มีทางออกสำหรับ implementation inheritance ให้พยายามแยก state ที่ manage โดย parent class จาก state ที่ manage โดย subclass วิธีหนึ่งคือให้ instance variable บางตัว manage อย่างเต็มที่โดย method ใน parent class โดย subclass ใช้มันในลักษณะ read-only หรือผ่าน method อื่นๆ ใน parent class วิธีนี้ใช้แนวคิดของ information hiding ภายใน class hierarchy เพื่อลด dependency

แม้ว่า mechanism ของ object-oriented programming สามารถช่วยในการ implement clean design แต่ไม่ได้รับประกันว่าจะได้ good design โดยตัวของมันเอง ตัวอย่างเช่น ถ้าเป็น class ที่ shallow หรือมี complex interface หรืออนุญาต external access ไปที่ internal state ของมัน พวกมันยังคงส่งผลให้เกิด high complexity

19.2 Agile development (Agile development)

Agile development เป็นแนวทางในการพัฒนาซอฟต์แวร์ที่เกิดขึ้นในช่วงปลาย 1990 จากแนวคิดต่างๆ เกี่ยวกับวิธีทำให้การพัฒนาเบา ยืดหยุ่น และ incremental และได้รับการ formally define ในการประชุมของ practitioner ในปี 2001 Agile development เป็นส่วนใหญ่เกี่ยวกับ process ของการพัฒนาซอฟต์แวร์ (organizing team, managing schedule, role ของ unit testing, interaction กับลูกค้า) ซึ่งแตกต่างจากการออกแบบซอฟต์แวร์ แต่มันสัมพันธ์กับหลักการออกแบบบางส่วนในหนังสือเล่มนี้

องค์ประกอบสำคัญหนึ่งของ agile development คือแนวคิดว่า development ควรเป็น incremental และ iterative ในแนวทาง agile software system ถูกพัฒนาในชุดของ iteration โดยแต่ละ iteration เพิ่มและประเมิน feature ใหม่ไม่กี่ตัว iteration แต่ละครั้งรวม design, test และ customer input โดยทั่วไป นี่คล้ายแนวทาง incremental ที่สนับสนุนที่นี่ ดังที่กล่าวในบท 1 ไม่สามารถมองเห็นระบบที่ซับซ้อนได้ดีพอเพื่อ determine best design ที่จุดเริ่มต้นของ project วิธีที่ดีที่สุดในการได้ good design คือพัฒนาระบบแบบ incremental โดยแต่ละ increment เพิ่ม abstraction ไม่กี่ตัวและ refactor abstraction ที่มีอยู่แล้วตามประสบการณ์ นี่คล้ายแนวทาง agile development

ความเสี่ยงหนึ่งของ agile development คือมันสามารถนำไปสู่ tactical programming Agile development มีแนวโน้มที่จะ focus developer ไปที่ feature ไม่ใช่ abstraction และให้กำลังใจให้ developer ชะลอการตัดสินใจออกแบบเพื่อให้ได้ working software โดยเร็วที่สุด ตัวอย่างเช่น agile practitioner บางคนแย้งว่าไม่ควร implement general-purpose mechanism ทันที ให้ implement minimal special-purpose mechanism ในตอนแรก แล้ว refactor เป็นบางสิ่งที่ generic มากขึ้นต่อมาเมื่อรู้ว่าจำเป็น แม้ว่าข้อโต้แย้งเหล่านี้จะสมเหตุสมผลในระดับหนึ่ง พวกมันก็โต้แย้งต่อ investment approach และให้กำลังใจให้ใช้ tactical style ของ programming มากขึ้น นี่สามารถส่งผลให้เกิด rapid accumulation ของความซับซ้อน

การพัฒนา incremental โดยทั่วไปเป็นความคิดที่ดี แต่ increment ของการพัฒนาควรเป็น abstraction ไม่ใช่ feature ไม่เป็นไรที่จะชะลอการคิดเกี่ยวกับ abstraction เฉพาะจนกว่า feature จะต้องใช้มัน เมื่อต้องใช้ abstraction ให้ลงทุนเวลาเพื่อออกแบบมันอย่าง clean ตามคำแนะนำในบท 6 และทำให้มัน general-purpose ได้บ้าง

19.3 Unit tests (Unit test)

สมัยก่อน developer แทบไม่เขียน test เลย หากเขียน test มักจะถูกเขียนโดย QA team ที่แยกต่างหาก อย่างไรก็ตาม หลักเกณฑ์หนึ่งของ agile development คือว่า testing ควร integrate อย่างใกล้ชิดกับ development และ programmer ควรเขียน test สำหรับโค้ดของพวกเขาเอง วิธีปฏิบัตินี้ได้กลายเป็น widespread แล้ว Test มักจะแบ่งออกเป็นสองประเภท: unit test และ system test Unit test เป็นประเภทที่มักจะถูกเขียนโดย developer มากที่สุด พวกมันมีขนาดเล็กและ focused: test โดยทั่วไปจะ validate section เล็กๆ ของโค้ดใน single method Unit test สามารถถูก run แบบแยกอิสระ โดยไม่ต้อง set up production environment สำหรับระบบ Unit test มักจะถูก run ร่วมกับ test coverage tool เพื่อให้มั่นใจว่าทุกบรรทัดของโค้ดในแอปพลิเคชันถูก test เมื่อใดก็ตามที่ developer เขียน new code หรือแก้ไข existing code พวกเขาต้องรับผิดชอบต่อการ update unit test เพื่อ maintain proper test coverage

ประเภทที่สองของ test ประกอบด้วย system test (บางครั้งเรียกว่า integration test) ซึ่ง ensure ว่า part ต่างๆ ของแอปพลิเคชันทั้งหมดทำงานร่วมกันอย่างถูกต้อง พวกมักจะเกี่ยวข้องกับการ run แอปพลิเคชันทั้งหมดใน production environment System test มีความเป็นไปได้มากขึ้นที่จะถูกเขียนโดย QA หรือ testing team ที่แยกต่างหาก

Test โดยเฉพาะ unit test มีบทบาทสำคัญในการออกแบบซอฟต์แวร์เพราะพวกมัน facilitate refactoring โดยไม่มี test suite มันเป็นอันตรายที่จะทำ major structural change ไปที่ระบบ ไม่มีวิธีง่ายๆ ในการหา bug ดังนั้นมีความเป็นไปได้ว่า bug จะไป undetected จนกว่า new code จะถูก deployed ที่ซึ่ง bug มีค่าใจใหญ่ในการหาและแก้ไข ด้วยเหตุนี้ developer หลีกเลี่ยง refactoring ในระบบโดยไม่มี good test suite พวกเขาพยายาม minimize จำนวนของ code change สำหรับ feature ใหม่แต่ละตัวหรือ bug fix ซึ่งหมายความว่า complexity สะสมและ design mistake ไม่ได้รับการแก้ไข

ด้วย good set ของ test developer สามารถมี confidence มากขึ้นเมื่อ refactoring เพราะ test suite จะหา bug ส่วนใหญ่ที่ introduce ขึ้นมา นี่ให้กำลังใจให้ developer ทำ structural improvement ไปที่ระบบ ซึ่งส่งผลให้ได้ better design Unit test นั้นมีค่าโดยเฉพาะ: พวกเขาให้ degree มากขึ้นของ code coverage กว่า system test ดังนั้นพวกเขามีความเป็นไปได้มากขึ้นที่จะ uncover bug

ตัวอย่างเช่น ในระหว่าง development ของ Tcl scripting language เราตัดสินใจ improve performance โดยการ replace Tcl interpreter ด้วย byte-code compiler นี่คือ change ใหญ่มากที่ affect เกือบทั้งหมด part ของ Tcl engine โดยโชคดี Tcl มี excellent unit test suite ซึ่งเราได้ run บน new byte-code engine Test ที่มีอยู่แล้วนั้น effective มากในการ uncover bug ใน new engine เพราะว่า single bug เท่านั้นที่ turn up หลัง alpha release ของ byte-code compiler

19.4 Test-driven development (Test-driven development)

Test-driven development เป็นแนวทางในการพัฒนาซอฟต์แวร์ที่ programmer เขียน unit test ก่อนที่จะเขียน code เมื่อ create class ใหม่ developer เขียน unit test สำหรับ class ก่อน ตาม expected behavior ของมัน Test ไม่ pass ทั้งหมด เพราะไม่มี code สำหรับ class Developer จากนั้น work ผ่าน test แต่ละครั้ง เขียน code พอเพียงเพื่อให้ test นั้น pass เมื่อ test ทั้งหมด pass class ก็เสร็จแล้ว

แม้ว่าผมเป็นผู้สนับสนุนที่แข็งแกร่งของ unit testing แต่ผมไม่ชอบ test-driven development ปัญหากับ test-driven development คือมันสำหรับ focus attention ไปที่การ get specific feature ทำงาน แทนที่จะหา best design นี่คือ tactical programming บริสุทธิ์ ด้วย disadvantage ทั้งหมด Test-driven development เป็น too incremental: ที่ moment ใดก็ตาม มันเป็นสิ่งที่น่าดึงดูด just hack in next feature เพื่อ make next test pass ไม่มี obvious time ให้ทำ design ดังนั้นมันเป็นเรื่องง่ายที่จะ end up ด้วย mess

ดังที่กล่าวในส่วน 19.2 unit ของ development ควรเป็น abstraction ไม่ใช่ feature เมื่อ discover ความต้องการ abstraction อย่า create มันในชิ้นส่วนเมื่อเวลาผ่านไป design มันทั้งหมดพร้อมกัน (หรืออย่างน้อย enough เพื่อให้ได้ reasonably comprehensive set ของ core function) นี่มี likelihood มากขึ้นที่จะ produce clean design ที่ piece fit together ได้ดี

ที่เดียวที่มันสมควร write test ก่อนคือเมื่อ fix bug ก่อน fix bug ให้เขียน unit test ที่ fail เพราะ bug จากนั้น fix bug และ make sure ว่า unit test ตอนนี้ pass นี่คือวิธี best ในการให้ sure ว่าจริง ๆ ได้ fix bug ถ้า fix bug ก่อน write test มี possibility ว่า new unit test ไม่ได้ actually trigger bug ซึ่ง case นั้น มันจะไม่บอกคุณว่า จริง ๆ ได้ fix problem

19.5 Design patterns (Design pattern)

Design pattern เป็น commonly used approach สำหรับการแก้ปัญหา particular kind เช่น iterator หรือ observer แนวคิดของ design pattern ได้รับความนิยมโดยหนังสือ Design Patterns: Elements of Reusable Object-Oriented Software โดย Gamma, Helm, Johnson และ Vlissides และ design pattern ตอนนี้ถูก widely used ใน object-oriented software development

Design pattern แสดง alternative ไปที่ design: แทนที่จะ design mechanism ใหม่ from scratch just apply well-known design pattern ส่วนใหญ่แล้ว นี่คือ good: design pattern arose เพราะ solve common problem และ เพราะ generally agreed ให้ clean solution ถ้า design pattern ทำงาน well ในสถานการณ์ particular มันจะ probably be hard ให้ come up กับ approach ต่างๆ ที่ดีกว่า

ความเสี่ยง greatest กับ design pattern คือ over-application ไม่ problem ทุกตัวสามารถแก้ได้อย่าง clean ด้วย existing design pattern ไม่พยายาม force problem เข้าไป design pattern เมื่อ custom approach จะ cleaner using design pattern ไม่ได้ automatically improve software system แต่มันทำได้ก็ต่อเมื่อ design pattern ที่เข้าฟิต เช่นเดียวกับแนวคิดอื่นๆ ในซอฟต์แวร์ design แนวคิดว่า design pattern ดี ไม่ได้ necessarily หมายความว่า more design pattern เป็น better

19.6 Getters and setters (Getter และ Setter)

ใน Java programming community getter และ setter method เป็น popular design pattern Getter และ setter ถูก associate กับ instance variable สำหรับ class พวกเขามี name เช่น getFoo และ setFoo โดยที่ Foo คือชื่อของ variable Getter method return current value ของ variable และ setter method modify value

Getter และ setter ไม่ strictly necessary เพราะ instance variable สามารถทำให้ public ได้ argument สำหรับ getter และ setter คือว่า allow additional function ให้ performed ในขณะที่ get และ set เช่น updating related value เมื่อ variable change notifying listener ของ change หรือ enforce constraint บน value แม้ว่า feature นี้ไม่จำเป็นในตอนแรก พวกมันสามารถ added later โดยไม่ change interface

แม้ว่า อาจจะสมควร use getter และ setter ถ้าต้อง expose instance variable แต่มันดีกว่า ไม่ expose instance variable ในตอนแรก exposed instance variable หมายความว่า part ของ class implementation ที่ visible externally ซึ่ง violate แนวคิดของ information hiding และ increase complexity ของ interface ของ class Getter และ setter เป็น shallow method (typically only single line) ดังนั้นพวกเขา add clutter ไปที่ interface ของ class โดยไม่ให้ functionality มากมาย ดีกว่าที่จะ avoid getter และ setter (หรือ any exposure ของ implementation data) มากเท่าที่เป็นไปได้

หนึ่งในความเสี่ยงของการ establish design pattern คือ developer assume pattern ดี และ try ใช้มัน มากเท่าที่เป็นไปได้ นี่มี led ไปยัง overusage ของ getter และ setter ใน Java

19.7 Conclusion (บทสรุป)

เมื่อใดก็ตามที่ encounter proposal สำหรับ new software development paradigm challenge มันจากมุมมองของความซับซ้อน: ข้อเสนอนั้นจริง ๆ help ให้ minimize ความซับซ้อนใน large software system หรือไม่? หลาย proposal ฟังดู good บน surface แต่ถ้า look more deeply จะเห็นว่า some ของพวกเขา make ความซับซ้อน worse ไม่ใช่ better