| Topic 9 | DRY—ความเลวร้ายของการทำซ้ำ |
|---|
การป้อนความรู้ที่ขัดแย้งกันสองชุดให้คอมพิวเตอร์คือวิธีที่กัปตันเจมส์ ที. เคิร์ก (James T. Kirk) ชื่นชอบใช้ในการปิดการทำงานของปัญญาประดิษฐ์จอมอาละวาด แต่น่าเสียดายที่หลักการเดียวกันนี้ก็สามารถใช้ล้มโค้ด ของคุณ ได้เช่นกัน
ในฐานะโปรแกรมเมอร์ เรารวบรวม จัดระเบียบ ดูแลรักษา และนำความรู้มาใช้ประโยชน์ เราบันทึกความรู้ไว้ในสเปก (specifications) ทำให้มันมีชีวิตขึ้นมาในโค้ดที่รันได้จริง และใช้มันเพื่อสร้างการตรวจสอบที่จำเป็นระหว่างการทดสอบ
แต่น่าเสียดายที่ความรู้ไม่เคยหยุดนิ่ง มันเปลี่ยนแปลงอยู่เสมอ—และมักจะเปลี่ยนอย่างรวดเร็วด้วย ความเข้าใจของคุณต่อ requirement อาจเปลี่ยนไปหลังการประชุมกับลูกค้า รัฐบาลเปลี่ยนกฎระเบียบ แล้ว business logic บางส่วนก็ล้าสมัยไปทันที การทดสอบอาจแสดงให้เห็นว่า อัลกอริทึมที่เลือกใช้ไม่ได้ผล ความไม่แน่นอนทั้งหมดนี้หมายความว่าเราใช้เวลาส่วนใหญ่ไปกับโหมดการบำรุงรักษา (maintenance) คอยจัดระเบียบและปรับถ้อยคำของความรู้ในระบบของเราใหม่อยู่เรื่อยๆ
คนส่วนใหญ่มักคิดว่าการบำรุงรักษาจะเริ่มขึ้นก็ต่อเมื่อแอปพลิเคชันถูกปล่อยออกไปแล้ว และหมายถึงการแก้บั๊กกับการเพิ่มฟีเจอร์เท่านั้น เราคิดว่าคนเหล่านี้เข้าใจผิด โปรแกรมเมอร์อยู่ในโหมดการบำรุงรักษาตลอดเวลาต่างหาก ความเข้าใจของเราเปลี่ยนไปทุกวัน requirement ใหม่ๆ เข้ามาและ requirement เดิมก็พัฒนาไปเรื่อยๆ ในขณะที่เรากำลังก้มหน้าก้มตาทำโปรเจกต์อยู่ บางทีสภาพแวดล้อมก็เปลี่ยนไปด้วย ไม่ว่าจะด้วยเหตุผลอะไรก็ตาม การบำรุงรักษาไม่ใช่กิจกรรมที่แยกออกมาต่างหาก แต่เป็นส่วนหนึ่งของกระบวนการพัฒนาทั้งหมดที่เกิดขึ้นเป็นประจำ
เมื่อเราทำการบำรุงรักษา เราต้องค้นหาและเปลี่ยนแปลงตัวแทนของสิ่งต่างๆ—แคปซูลความรู้ที่ฝังอยู่ในแอปพลิเคชัน ปัญหาคือมันง่ายมากที่จะทำให้ ความรู้ซ้ำซ้อนกันในสเปก กระบวนการ และโปรแกรมที่เราพัฒนาขึ้น และเมื่อเราทำแบบนั้น เราก็กำลังเปิดประตูต้อนรับฝันร้ายของการบำรุงรักษา— ฝันร้ายที่เริ่มต้นขึ้นตั้งแต่ก่อนที่แอปพลิเคชันจะถูกปล่อยออกไปเสียอีก
เรารู้สึกว่าวิธีเดียวที่จะพัฒนาซอฟต์แวร์ได้อย่างน่าเชื่อถือ และทำให้สิ่งที่เราพัฒนาเข้าใจและบำรุงรักษาได้ง่ายขึ้น คือการยึดถือสิ่งที่เราเรียกว่าหลักการ DRY:
ความรู้ทุกชิ้นต้องมีตัวแทนเพียงหนึ่งเดียวที่ชัดเจนไม่คลุมเครือและเป็นแหล่งอ้างอิงหลักภายในระบบ
ทำไมเราถึงเรียกมันว่า DRY?
| Tip 15 | DRY—อย่าทำซ้ำตัวเอง (Don't Repeat Yourself) |
|---|
ทางเลือกอื่นคือการให้สิ่งเดียวกันถูกแสดงออกในสองที่หรือมากกว่านั้น ถ้าคุณเปลี่ยนที่หนึ่ง คุณต้องจำให้ได้ว่าต้องไปเปลี่ยนที่อื่นๆ ด้วย ไม่เช่นนั้นโปรแกรมของคุณก็จะถูกความขัดแย้งกันเองเล่นงานจนคุกเข่า เหมือนกับคอมพิวเตอร์เอเลี่ยนในหนังนั่นแหละ คำถามไม่ใช่ว่า คุณจะจำได้หรือเปล่า แต่เป็นว่าคุณจะลืมเมื่อไหร่ต่างหาก
คุณจะพบว่าหลักการ DRY โผล่มาซ้ำแล้วซ้ำเล่าตลอดทั้งเล่มนี้ บ่อยครั้งในบริบทที่ไม่เกี่ยวกับการเขียนโค้ดเลยด้วยซ้ำ เรารู้สึกว่ามันเป็นหนึ่งในเครื่องมือที่สำคัญที่สุดในกล่องเครื่องมือของ Pragmatic Programmer ครับ
ในหัวข้อนี้ เราจะสรุปปัญหาของการทำซ้ำและเสนอกลยุทธ์ทั่วไปในการรับมือกับมันครับ
DRY Is More Than Code (DRY ไม่ใช่แค่เรื่องโค้ด)
ก่อนอื่นขอเคลียร์อะไรบางอย่างให้ชัดเจนก่อน ในฉบับพิมพ์ครั้งแรกของหนังสือเล่มนี้ เราอธิบายความหมายของ Don't Repeat Yourself ได้ไม่ค่อยดีนัก หลายคนเข้าใจว่ามันหมายถึงแค่เรื่องโค้ดเท่านั้น พวกเขาคิดว่า DRY แปลว่า “ห้ามก๊อปปี้แล้ววางบรรทัดซอร์สโค้ด”
นั่น ก็เป็น ส่วนหนึ่งของ DRY จริงอยู่ แต่มันเป็นแค่ส่วนเล็กๆ ที่แทบจะไม่สำคัญเท่าไหร่เลย
DRY เกี่ยวข้องกับการทำซ้ำของ ความรู้ ของ เจตนา มันคือการแสดงสิ่งเดียวกันออกมาในสองที่ที่ต่างกัน ซึ่งอาจจะเป็นคนละวิธีกันโดยสิ้นเชิงด้วยซ้ำ
นี่คือบททดสอบง่ายๆ: เมื่อโค้ดส่วนใดส่วนหนึ่งต้องเปลี่ยนแปลง คุณพบว่าตัวเองต้องไปเปลี่ยนมันในหลายที่ และในหลายรูปแบบที่ต่างกันหรือเปล่า? คุณต้องเปลี่ยนทั้งโค้ดและเอกสารประกอบ หรือทั้ง database schema และโครงสร้างที่เก็บมันไว้ หรือ…? ถ้าใช่ แสดงว่าโค้ดของคุณไม่ DRY แล้วล่ะครับ
งั้นมาดูตัวอย่างทั่วไปของการทำซ้ำกันบ้างครับ
Duplication in Code (การทำซ้ำในโค้ด)
มันอาจจะดูเป็นเรื่องเล็กน้อย แต่การทำซ้ำของโค้ดนั้นพบเห็นได้บ่อยมากจริงๆ นี่คือตัวอย่าง:
| | def print_balance(account) |
|---|---|
| | printf "Debits: %10.2f \n " , account.debits |
| | printf "Credits: %10.2f \n " , account.credits |
| | if account.fees < 0 |
| | printf "Fees: %10.2f- \n " , -account.fees |
| | else |
| | printf "Fees: %10.2f \n " , account.fees |
| | end |
| | printf " ———- \n " |
| | if account.balance < 0 |
| | printf "Balance: %10.2f- \n " , -account.balance |
| | else |
| | printf "Balance: %10.2f \n " , account.balance |
| | end |
| | end |
ตอนนี้ขอให้มองข้ามประเด็นที่ว่าเรากำลังทำผิดพลาดแบบมือใหม่ด้วยการเก็บค่าเงินไว้ในตัวแปร float ไปก่อน แล้วลองดูว่าคุณเจอการทำซ้ำใน โค้ดนี้ไหม (เราเห็นอย่างน้อยสามจุด แต่คุณอาจจะเห็นมากกว่านั้นก็ได้)
คุณเจออะไรบ้าง? นี่คือรายการของเรา
อย่างแรก เห็นได้ชัดว่ามีการก๊อปปี้แล้ววางโค้ดที่จัดการกับตัวเลขติดลบซ้ำกัน เราสามารถแก้ไขได้ด้วยการเพิ่มฟังก์ชันใหม่เข้าไป:
| | def format_amount(value) |
|---|---|
| | result = sprintf( "%10.2f" , value.abs) |
| | if value < 0 |
| | result + "-" |
| | else |
| | result + " " |
| | end |
| | end |
| | |
| | def print_balance(account) |
| | printf "Debits: %10.2f \n " , account.debits |
| | printf "Credits: %10.2f \n " , account.credits |
| | printf "Fees: %s \n " , format_amount(account.fees) |
| | printf " ———- \n " |
| | printf "Balance: %s \n " , format_amount(account.balance) |
| | end |
การทำซ้ำอีกอย่างคือความกว้างของฟิลด์ (field width) ที่ถูกใช้ซ้ำในทุกการเรียก printf เรา สามารถ แก้ไขได้ด้วยการสร้างค่าคงที่แล้วส่งมันเข้าไปในแต่ละการเรียก แต่ทำไมไม่ใช้ฟังก์ชันที่มีอยู่แล้วไปเลยล่ะ?
| | def format_amount(value) |
|---|---|
| | result = sprintf( "%10.2f" , value.abs) |
| | if value < 0 |
| | result + "-" |
| | else |
| | result + " " |
| | end |
| | end |
| | |
| | def print_balance(account) |
| | printf "Debits: %s \n " , format_amount(account.debits) |
| | printf "Credits: %s \n " , format_amount(account.credits) |
| | printf "Fees: %s \n " , format_amount(account.fees) |
| | printf " ———- \n " |
| | printf "Balance: %s \n " , format_amount(account.balance) |
| | end |
ยังมีอีกไหม? ลองคิดดูว่าถ้าลูกค้าขอให้เพิ่มช่องว่างระหว่างป้ายกำกับ (label) กับตัวเลขล่ะ เราจะต้องไปแก้ถึงห้าบรรทัด มาลบการทำซ้ำนี้กันดีกว่า:
| | def format_amount(value) |
|---|---|
| | result = sprintf( "%10.2f" , value.abs) |
| | if value < 0 |
| | result + "-" |
| | else |
| | result + " " |
| | end |
| | end |
| | |
| | def print_line(label, value) |
| | printf "%-9s%s \n " , label, value |
| | end |
| | |
| | def report_line(label, amount) |
| | print_line(label + ":" , format_amount(amount)) |
| | end |
| | |
| | def print_balance(account) |
| | report_line( "Debits" , account.debits) |
| | report_line( "Credits" , account.credits) |
| | report_line( "Fees" , account.fees) |
| | print_line( "" , "———-" ) |
| | report_line( "Balance" , account.balance) |
| | end |
ถ้าเราต้องเปลี่ยนรูปแบบการแสดงจำนวนเงิน เราก็แค่เปลี่ยน format_amount ถ้าเราอยากเปลี่ยนรูปแบบของป้ายกำกับ เราก็แค่เปลี่ยน report_line .
แต่ก็ยังมีการละเมิด DRY แบบแฝงอยู่ดี: จำนวนขีด (hyphen) ในเส้นคั่นนั้นสัมพันธ์กับความกว้างของฟิลด์จำนวนเงิน แต่มันก็ไม่ได้ตรงกันเป๊ะ เพราะตอนนี้มันสั้นกว่าอยู่หนึ่งตัวอักษร ทำให้เครื่องหมายลบท้ายตัวเลขยื่นเลยคอลัมน์ออกไป นี่คือเจตนาของลูกค้า ซึ่งเป็นคนละเจตนากับการจัดรูปแบบจำนวนเงินจริงๆ
Not All Code Duplication Is Knowledge Duplication (ไม่ใช่ทุกการทำซ้ำของโค้ดจะเป็นการทำซ้ำของความรู้)
สมมติว่าในแอปสั่งซื้อไวน์ออนไลน์ของคุณ คุณต้องเก็บและตรวจสอบความถูกต้อง (validate) อายุของผู้ใช้ พร้อมกับจำนวนที่พวกเขาสั่งซื้อ ตามที่เจ้าของเว็บไซต์กำหนดไว้ ทั้งสองค่านี้ต้องเป็นตัวเลขและมากกว่าศูนย์ คุณจึงเขียนโค้ด validation ขึ้นมา:
| | def validate_age(value): |
|---|---|
| | validate_type(value, :integer) |
| | validate_min_integer(value, 0) |
| | |
| | def validate_quantity(value): |
| | validate_type(value, :integer) |
| | validate_min_integer(value, 0) |
ระหว่างการทำ code review เจ้าคนรู้ไปหมดประจำทีมก็ตีกลับโค้ดนี้ทันที โดยอ้างว่ามันละเมิด DRY เพราะเนื้อหาในทั้งสองฟังก์ชันเหมือนกันเป๊ะ
แต่เขาคิดผิด โค้ดเหมือนกันก็จริง แต่ความรู้ที่มันแสดงออกมานั้นต่างกัน ฟังก์ชันทั้งสองตรวจสอบสิ่งที่แยกจากกันโดยสิ้นเชิง เพียงแต่บังเอิญมีกฎเดียวกันเท่านั้นเอง นี่คือเรื่องบังเอิญ ไม่ใช่การทำซ้ำครับ
Duplication in Documentation (การทำซ้ำในเอกสารประกอบ)
ไม่รู้ว่าตำนานที่ว่าคุณควรเขียนคอมเมนต์กำกับทุกฟังก์ชันเกิดขึ้นมาได้ยังไง คนที่เชื่อในความบ้าคลั่งนี้มักจะสร้างอะไรแบบนี้ขึ้นมา:
| | # Calculate the fees for this account. |
|---|---|
| | # |
| | # * Each returned check costs $20 |
| | # * If the account is in overdraft for more than 3 days, |
| | # charge $10 for each day |
| | # * If the average account balance is greater that $2,000 |
| | # reduce the fees by 50% |
| | |
| | def fees(a) |
| | f = 0 |
| | if a.returned_check_count > 0 |
| | f += 20 * a.returned_check_count |
| | end |
| | if a.overdraft_days > 3 |
| | f += 10*a.overdraft_days |
| | end |
| | if a.average_balance > 2_000 |
| | f /= 2 |
| | end |
| | f |
| | end |
เจตนาของฟังก์ชันนี้ถูกบอกไว้สองครั้ง ครั้งหนึ่งในคอมเมนต์ และอีกครั้งในโค้ด เมื่อลูกค้าเปลี่ยนค่าธรรมเนียม เราก็ต้องไปอัปเดตทั้งสองที่ เมื่อเวลาผ่านไป เราแทบจะรับประกันได้เลยว่าคอมเมนต์กับโค้ดจะไม่ตรงกันอีกต่อไป
ลองถามตัวเองดูว่าคอมเมนต์นี้เพิ่มอะไรให้กับโค้ดบ้าง ในมุมมองของเรา มันแค่ชดเชยการตั้งชื่อและการจัดวางที่ไม่ดีเท่านั้นเอง ลองแบบนี้ดูไหมครับ:
| | def calculate_account_fees(account) |
|---|---|
| | fees = 20 * account.returned_check_count |
| | fees += 10 * account.overdraft_days if account.overdraft_days > 3 |
| | fees /= 2 if account.average_balance > 2_000 |
| | fees |
| | end |
ชื่อฟังก์ชันบอกอยู่แล้วว่ามันทำอะไร และถ้าใครต้องการรายละเอียดเพิ่มเติม ก็ดูได้จากซอร์สโค้ดโดยตรง นี่แหละคือ DRY!
DRY Violations in Data (การละเมิด DRY ในข้อมูล)
โครงสร้างข้อมูลของเราคือตัวแทนของความรู้ และมันก็สามารถละเมิดหลักการ DRY ได้เช่นกัน ลองมาดูคลาสที่ใช้แทนเส้นตรงเส้นหนึ่งกัน:
| | class Line { |
|---|---|
| | Point start; |
| | Point end; |
| | double length; |
| | }; |
เมื่อมองแวบแรก คลาสนี้อาจดูสมเหตุสมผลดี เพราะเส้นตรงหนึ่งเส้นก็ต้องมีจุดเริ่มต้นและจุดสิ้นสุด และย่อมมีความยาวเสมอ (ถึงแม้จะเป็นศูนย์ก็ตาม) แต่ที่นี่เรามีการทำซ้ำอยู่ ความยาวถูกกำหนดโดยจุดเริ่มต้นและจุดสิ้นสุด เปลี่ยนจุดใดจุดหนึ่งไป ความยาวก็เปลี่ยนตาม ทางที่ดีกว่าคือทำให้ความยาวเป็นฟิลด์ที่คำนวณขึ้นมา:
| | class Line { |
|---|---|
| | Point start; |
| | Point end; |
| | double length() { return start.distanceTo(end); } |
| | }; |
ในขั้นตอนหลังๆ ของการพัฒนา คุณอาจเลือกละเมิดหลักการ DRY ด้วยเหตุผลด้านประสิทธิภาพ ซึ่งมักเกิดขึ้นเมื่อคุณต้อง cache ข้อมูลไว้ เพื่อหลีกเลี่ยงการทำ operation ที่มีต้นทุนสูงซ้ำๆ เคล็ดลับคือต้องจำกัดผลกระทบให้อยู่ในวงแคบ การละเมิดนี้จะไม่ถูกเปิดเผยออกไปสู่โลกภายนอก มีเพียง method ภายในคลาสเท่านั้นที่ต้องคอยดูแลให้ทุกอย่างถูกต้องตรงกัน:
| | class Line { |
|---|---|
| | private double length; |
| | private Point start; |
| | private Point end; |
| | |
| | public Line(Point start, Point end) { |
| | this .start = start; |
| | this .end = end; |
| | calculateLength(); |
| | } |
| | |
| | // public |
| | void setStart(Point p) { this .start = p; calculateLength(); } |
| | void setEnd(Point p) { this .end = p; calculateLength(); } |
| | |
| | Point getStart() { return start; } |
| | Point getEnd() { return end; } |
| | |
| | double getLength() { return length; } |
| | |
| | private void calculateLength() { |
| | this .length = start.distanceTo(end); |
| | } |
| | }; |
ตัวอย่างนี้ยังแสดงให้เห็นถึงประเด็นสำคัญอีกอย่างหนึ่งด้วย: เมื่อใดก็ตามที่โมดูลเปิดเผยโครงสร้างข้อมูลออกมา คุณกำลังผูก (coupling) โค้ดทั้งหมดที่ใช้โครงสร้างนั้นเข้ากับการ implement ของโมดูลนั้นด้วย หากเป็นไปได้ ให้ใช้ accessor function ในการอ่านและเขียนแอตทริบิวต์ ของอ็อบเจ็กต์เสมอ มันจะทำให้การเพิ่มฟังก์ชันการทำงานในอนาคตทำได้ง่ายขึ้น
การใช้ accessor function แบบนี้เชื่อมโยงกับ Uniform Access principle ของ Meyer ซึ่งอธิบายไว้ใน Object-Oriented Software Construction [Mey97] ซึ่งระบุว่า
บริการทั้งหมดที่โมดูลหนึ่งนำเสนอควรเข้าถึงได้ผ่านสัญกรณ์ (notation) แบบเดียวกัน ซึ่งไม่เปิดเผยว่าบริการเหล่านั้นถูก implement ผ่านการเก็บข้อมูล (storage) หรือผ่านการคำนวณ (computation)
Representational Duplication (การทำซ้ำเชิงการแสดงผล)
โค้ดของคุณเชื่อมต่อกับโลกภายนอกอยู่เสมอ ไม่ว่าจะเป็นไลบรารีอื่นผ่าน API บริการอื่นผ่านการเรียกระยะไกล (remote call) หรือข้อมูลจากแหล่งภายนอกต่างๆ และแทบทุกครั้งที่คุณทำแบบนั้น คุณก็นำการละเมิด DRY บางรูปแบบเข้ามาด้วย เพราะโค้ดของคุณต้องมีความรู้ ที่ปรากฏอยู่ใน สิ่ง ภายนอกนั้นด้วยเช่นกัน มันต้องรู้จัก API หรือ schema หรือความหมายของ error code หรืออะไรก็ตาม การทำซ้ำตรงนี้คือทั้งสองฝ่าย (โค้ดของคุณกับสิ่งภายนอกนั้น) ต่างต้องมีความรู้เกี่ยวกับการแสดงผล (representation) ของอินเทอร์เฟซระหว่างกัน เปลี่ยนฝั่งใดฝั่งหนึ่ง อีกฝั่งก็พังทันที
การทำซ้ำแบบนี้เป็นสิ่งที่หลีกเลี่ยงไม่ได้ แต่สามารถบรรเทาลงได้ นี่คือกลยุทธ์บางอย่างครับ
Duplication Across Internal APIs (การทำซ้ำข้าม Internal API)
สำหรับ internal API ให้มองหาเครื่องมือที่ให้คุณระบุ API ในรูปแบบที่เป็นกลาง (neutral format) เครื่องมือเหล่านี้มักจะสร้าง เอกสารประกอบ, mock API, functional test และ API client ให้โดยอัตโนมัติ โดยตัวหลังนี้อาจสร้างได้หลายภาษาด้วย ในสถานการณ์ที่ดีที่สุด เครื่องมือควรเก็บ API ทั้งหมดของคุณไว้ใน repository กลาง เพื่อให้แชร์ข้ามทีมกันได้
Duplication Across External APIs (การทำซ้ำข้าม External API)
ทุกวันนี้คุณจะพบมากขึ้นเรื่อยๆ ว่า public API ถูกจัดทำเอกสารอย่างเป็นทางการด้วยเครื่องมืออย่าง OpenAPI [15] สิ่งนี้ทำให้คุณสามารถนำเข้า API spec เข้าไปในเครื่องมือ API ของคุณเอง และเชื่อมต่อกับบริการนั้นได้อย่างน่าเชื่อถือมากขึ้น
ถ้าคุณหาสเปกแบบนี้ไม่เจอ ลองพิจารณาสร้างขึ้นมาเองแล้วเผยแพร่ออกไปดูสิครับ ไม่เพียงแต่คนอื่นจะได้ใช้ประโยชน์จากมัน คุณเองก็อาจได้รับความช่วยเหลือในการดูแลรักษามันด้วยซ้ำ
Duplication with Data Sources (การทำซ้ำกับแหล่งข้อมูล)
แหล่งข้อมูลหลายแห่งให้คุณตรวจสอบ (introspect) data schema ของมันได้ ซึ่งสามารถใช้กำจัดการทำซ้ำระหว่างแหล่งข้อมูลนั้นกับโค้ดของคุณได้มาก แทนที่จะเขียนโค้ดสำหรับเก็บข้อมูลนี้ด้วยมือ คุณสามารถสร้าง container ขึ้นมาจาก schema ได้โดยตรง persistence framework หลายตัว จะจัดการงานหนักส่วนนี้ให้คุณเอง
มีอีกทางเลือกหนึ่งที่เรามักจะชอบใช้มากกว่า แทนที่จะเขียนโค้ดที่แทนข้อมูลภายนอกด้วยโครงสร้างตายตัว (เช่น instance ของ struct หรือ class) ก็แค่ยัดมันลงใน key/value data structure ไปเลย (ภาษาของคุณอาจเรียกมันว่า map, hash, dictionary หรือแม้แต่ object)
ถ้าใช้วิธีนี้เพียวๆ ก็มีความเสี่ยงอยู่ เพราะคุณจะสูญเสียความมั่นใจไปมากว่าข้อมูลที่คุณกำลังทำงานด้วยนั้นคืออะไรกันแน่ ดังนั้นเราจึงแนะนำให้ เพิ่มชั้นที่สองเข้าไปในโซลูชันนี้ นั่นคือชุด validation แบบ table-driven ง่ายๆ ที่คอยตรวจสอบว่า map ที่คุณสร้างขึ้นมีข้อมูลที่คุณต้องการ อย่างน้อยที่สุด และอยู่ในรูปแบบที่คุณต้องการ เครื่องมือจัดทำเอกสาร API ของคุณอาจสร้างสิ่งนี้ให้ได้เลย
Interdeveloper Duplication (การทำซ้ำระหว่างนักพัฒนา)
การทำซ้ำประเภทที่ตรวจจับและจัดการได้ยากที่สุดบางทีก็เกิดขึ้นระหว่างนักพัฒนาต่างคนกันในโปรเจกต์เดียวกัน ฟังก์ชันการทำงานทั้งชุด อาจถูกทำซ้ำโดยไม่ได้ตั้งใจ และการทำซ้ำนั้นอาจไม่ถูกตรวจพบไปนานหลายปี จนนำไปสู่ปัญหาในการบำรุงรักษา เราเคยได้ยินเรื่องราวจากปากคน ที่เกี่ยวข้องโดยตรงเกี่ยวกับรัฐหนึ่งในสหรัฐฯ ที่ระบบคอมพิวเตอร์ของรัฐบาลถูกตรวจสอบเพื่อรองรับ Y2K การตรวจสอบพบโปรแกรมมากกว่า 10,000 โปรแกรม ที่แต่ละโปรแกรมมีโค้ดตรวจสอบเลขประกันสังคม (Social Security Number) คนละเวอร์ชันกัน
ในระดับภาพรวม ให้จัดการปัญหานี้ด้วยการสร้างทีมที่แข็งแกร่งและแน่นแฟ้น มีการสื่อสารที่ดีต่อกัน
แต่ในระดับโมดูล ปัญหานี้แอบแฝงและร้ายกว่านั้น ฟังก์ชันการทำงานหรือข้อมูลที่จำเป็นต้องใช้บ่อยๆ แต่ไม่ได้อยู่ในขอบเขตความรับผิดชอบที่ชัดเจน อาจถูก implement ขึ้นมาซ้ำแล้วซ้ำเล่าหลายครั้ง
เรารู้สึกว่าวิธีที่ดีที่สุดในการรับมือกับเรื่องนี้คือการส่งเสริมให้นักพัฒนาสื่อสารกันอย่างสม่ำเสมอและกระตือรือร้น
อาจจะจัด daily scrum standup meeting ก็ได้ หรือตั้งฟอรัม (เช่น Slack channel) ไว้พูดคุยปัญหาที่พบร่วมกัน วิธีนี้ให้ช่องทางการสื่อสาร ที่ไม่รบกวนใคร—แม้จะอยู่คนละสถานที่กันก็ตาม—พร้อมทั้งยังเก็บประวัติของทุกอย่างที่พูดคุยกันไว้อย่างถาวรด้วย
แต่งตั้งสมาชิกในทีมคนหนึ่งให้เป็นบรรณารักษ์ของโปรเจกต์ (project librarian) ซึ่งมีหน้าที่ช่วยอำนวยความสะดวกในการแลกเปลี่ยนความรู้ จัดที่ส่วนกลางไว้ใน source tree สำหรับเก็บ utility routine และสคริปต์ต่างๆ และควรตั้งใจอ่านซอร์สโค้ดและเอกสารของคนอื่นด้วย ไม่ว่าจะแบบไม่เป็นทางการหรือระหว่างทำ code review คุณไม่ได้กำลังแอบสอดส่อง—คุณกำลังเรียนรู้จากพวกเขาต่างหาก และอย่าลืมว่ามันเป็นเรื่อง ที่ต้องเผื่อแผ่กันทั้งสองทาง—อย่าไปหงุดหงิดที่คนอื่นมาไล่ดู (คุ้ยเขี่ย?) โค้ด ของคุณ บ้างเหมือนกัน
| Tip 16 | ทำให้การใช้ซ้ำเป็นเรื่องง่าย (Make It Easy to Reuse) |
|---|
สิ่งที่คุณพยายามทำคือสร้างสภาพแวดล้อมที่การค้นหาและใช้ซ้ำของที่มีอยู่แล้วนั้นง่ายกว่าการเขียนขึ้นมาใหม่เอง ถ้ามันไม่ง่าย คนก็จะไม่ทำ และถ้าคุณล้มเหลวในการใช้ซ้ำ คุณก็เสี่ยงที่จะทำให้ความรู้ถูกทำซ้ำครับ
Related Sections Include (หัวข้อที่เกี่ยวข้อง)
- Topic 8, The Essence of Good Design (แก่นแท้ของการออกแบบที่ดี)
- Topic 28, Decoupling (การลดการยึดติดกัน)
- Topic 32, Configuration (การกำหนดค่า)
- Topic 38, Programming by Coincidence (การเขียนโปรแกรมแบบเดาสุ่ม)
- Topic 40, Refactoring (การรีแฟกเตอร์)