การเขียนโปรแกรมเป็นกระบวนการแก้ปัญหาทางวิทยาศาสตร์ด้วยการใช้คอมพิวเตอร์ ปัญหาหลายๆ ปัญหามีความยุ่งยากในการแก้ไข การพัฒนาโปรแกรมเพื่อแก้ไขปัญหาก็จะมีความยุ่งยาก แปรผันตามความความซับซ้อนของปัญหาที่ต้องการแก้ไข เพื่อที่จะทำให้การแก้ปัญหามีความง่ายมากขึ้น ผู้พัฒนาโปรแกรมต้องปรับกระบวนการ และวางแผนวิธีการแก้ปัญหาอย่างมีระเบียบ เพื่อควบคุมให้ความซับซ้อนของปัญหาอยู่ในระดับที่สามารถจัดการได้
ในช่วงยุคแรกๆ ของการพัฒนาการเขียนโปรแกรม แนวคิดของการคำนวณในทางวิทยาศาสตร์เป็นเพียงการทดลองทางความคิด ไม่ค่อยมีคนรู้จักการเขียนโปรแกรมในช่วงเวลานั้น ภายหลังได้มีการพัฒนาวิธีการพัฒนาการเขียนโปรแกรม เรียกว่า วิศวกรรมซอฟต์แวร์ วิธีการหนึ่งที่วิศวกรซอฟต์แวร์ชอบใช้ในการแก้ปัญหา คือการใช้กลยุทธ Top-Down Design หรือ Stepwise Refinement ซึ่งทำหน้าที่แก้ปัญหาโดยการมองปัญหาทั้งระบบ แล้วแบ่งปัญหาทั้งระบบออกเป็นส่วนๆ แล้วค่อยๆ แก้ปัญหาที่แบ่งออกมาแล้วทีละส่วน แล้วทดสอบว่าปัญหาที่แก้แล้ว แก้ได้จริงๆ ทดสอบซ้ำๆ จนมั่นใจว่าปัญหาได้ถูกแก้ไขเรียบร้อยแล้ว จึงค่อยขยับไปแก้ปัญหาอื่นๆ ต่อไป
เพื่อจัดการกับปัญหานี้โปรแกรมเมอร์เริ่มพัฒนาชุดวิธีการเขียนโปรแกรมที่เรียกรวมกันว่า software engineering การใช้ทักษะด้านวิศวกรรมซอฟต์แวร์ที่ดีนั้นไม่เพียงทำให้โปรแกรมเมอร์รายอื่นสามารถอ่านและเข้าใจโปรแกรมของเราได้ง่ายขึ้น แต่ยังช่วยให้เราสามารถเขียนโปรแกรมเหล่านั้นได้ง่ายขึ้นตั้งแต่แรก หนึ่งในความก้าวหน้าด้านระเบียบวิธีที่สำคัญที่สุดที่จะเกิดขึ้นจากวิศวกรรมซอฟต์แวร์คือกลยุทธ์ของการออกแบบจากบนลงล่างหรือการปรับแต่งแบบขั้นตอน (top-down design or stepwise refinement) ซึ่งประกอบด้วยการแก้ปัญหาโดยเริ่มจากปัญหาโดยรวม เราจะแบ่งปัญหาทั้งหมดออกเป็นชิ้น ๆ แล้วแก้ไขแต่ละชิ้นและแบ่งชิ้นส่วนเหล่านั้นออกไปอีกถ้าจำเป็น
เพื่อแสดงให้เห็นถึงตัวอย่างการแก้ปัญหาที่มีการแบ่งปัญหาใหญ่ๆ ออกเป็นปัญหาย่อยๆ แล้วทยอยแก้ปัญหา ให้ลองดูโลกของคาเรลที่อยู่ด้านล่าง
ในแต่ละคอลัมน์จะมีหอคอยของเหรียญวางซ้อนกันอยู่ โดยที่ไม่รู้ว่าแต่ละหอมีเหรียญวางซ้อนกันกี่เหรียญ และบางหอคอยก็ไม่มีเหรียญอยู่เลย หน้าที่ของคาเรลในรอบนี้คือให้ทำการปีนหอคอยแล้วเก็บเหรียญมาทั้งหมด แล้วนำไปวางที่มุมขวาสุดของชั้นล่าง หลังจากที่คาเรลทำงานเสร็จแล้ว เหรียญทั้งหมดทั้ง 25 เหรียญจะต้องถูกนำไปวางที่มุมขวาล่าง และคาเรลก็กลับไปยืนรอที่จุดเริ่มต้นเหมือนเดิม ตามรูปข้างล่าง
ในการเริ่มต้นเราอาจสมมติว่ามีจำนวน beeper เป็น 0 ในถุง เมื่อวาง beeper ลงไปเราจะสามารถใช้ function ที่ชื่อ beepers_in_bag() ตรวจสอบได้ นอกจากนี้เรายังสามารถสรุปได้ว่าความสูงของแต่ละ column ไปไม่ถึงจุดเหนือสุดของแผนที่ได้อีกด้วย
กุญแจสำคัญในการแก้ปัญหานี้คือการย่อยสลายโปรแกรมอย่างถูกวิธีในขณะที่ยังคงสามารถทดสอบตามที่คุณไป งานนี้มีความซับซ้อนมากกว่างานอื่น ๆ ที่คุณเห็นซึ่งทำให้การเลือกโปรแกรมย่อยที่เหมาะสมมีความสำคัญต่อการได้รับโซลูชั่นที่ประสบความสำเร็จ
หลักการที่ใช้ในการออกแบบโปรแกรม แต่ละคนก็จะมีวิธีการแก้ปัญหาแตกต่างกันไป จากตัวอย่างโจทย์ข้างบน คนทำตัวอย่างได้แบ่งวิธีการแก้ปัญหาออกเป็น 3 ขั้นตอน ขั้นตอนแรก คาเรลจะทำการเก็บเหรียญทั้งหมด ขั้นตอนที่สอง คาเรลจะเดินไปจุดสุดท้ายแล้ววางเหรียญทั้งหมด และขึ้นตอนสุดท้ายคือให้คาเรลเดินกลับไปยังจุดเริ่มต้น ทำให้ได้ฟังก์ชัน main ที่มีลักษณะดังนี้
def main():
collect_all_beepers()
drop_all_beepers()
return_home()
ในระดับนี้ ผู้เขียนโปรแกรมสามารถทำความเข้าใจปัญหาได้ง่าย แต่ก็ยังมีรายละเอียดปัญหายิบย่อยอีกเล็กน้อยของฟังก์ชันที่ยังไม่ได้ถูกระบุ ถึงแม้จะเป็นอย่างนั้น ถ้าผู้พัฒนาเชื่อว่า การแบ่งปัญหาออกเป็นขั้นตอนแบบนี้ จะสามารถแก้ปัญหาได้ ก็จะต้องพยายามลงมือสร้างวิธีแก้ปัญหาย่อยๆ ไปเรื่อยๆ เพื่อให้ขั้นตอนที่แบ่งไว้ ใช้ได้จริง แล้วปัญหาทั้งหมดก็จะถูกได้รับการแก้ไข
ตอนนี้ได้ออกแบบขั้นตอนหลักของวิธีการแก้ปัญหาของคาเรลทั้งหมดแล้ว ต่อมาก็จะเป็นส่วนของวิธีการแก้ปัญหาของขั้นแรก นั่นคือการเก็บเหรียญทั้งหมด ซึ่งจะทำการประกาศฟังก์ชันเป็น collect_all_beepers() ทีนี้ ปัญหาของการเก็บเหรียญทั้งหมด สามารถแบ่งปัญหาออกเป็นปัญหาย่อยๆ ได้อีก 2 ขั้นตอน คือ เก็บเหรียญทีละ 1 แถว แล้วค่อยเดินต่อไปยังแถวถัดไป ทำให้สามารถประกาศฟังก์ชัน collect_all_beepers() ออกมามีหน้าตาประมาณนี้
def collect_all_beepers() :
# temporary implementation for testing purposes
collect_one_tower()
move()
ข้อควรระวัง การลองเขียนโปรแกรมทั้งหมดในคราวเดียวโดยไม่ได้ทดสอบคำสั่งทีละส่วน เป็นสิ่งที่อันตราย ถ้าตรวจพบความผิดพลาดขึ้นมา จะทำให้หาแหล่งที่มาของข้อผิดพลาดได้ยาก จากตัวอย่างนี้ แนะนำว่าให้ทำการทดสอบฟังก์ชัน collect_one_tower() เก็บเหรียญทีละ 1 แถว ให้เรียบร้อยก่อน แล้วค่อยทดสอบฟังก์ชัน collect_all_tower() ต่อไป
ตามหลักการชี้แนะนำถ้าเรามีลูปที่ซับซ้อนให้ทดสอบเนื้อความของลูปก่อนที่คุณจะเขียนลูปทั้งหมด
เมื่อฟังก์ชัน collect_one_tower() ถูกเรียกใช้งาน คาเรลจะต้องยีนอยู่ที่ฐานของหอคอย ตรวจสอบว่ามีเหรียญอยู่ไหม ถ้ามีเหรียญก็ไปเก็บมาให้หมด ถ้าไม่มีก็ไม่ต้องทำอะไร เหตุการณ์นี้เหมาะกับการใช้คำสั่ง if ในการควบคุม คำสั่งที่ควรจะป้อนให้คาเรลจะมีหน้าตาประมาณนี้
if beepers_present():
collect_actual_tower()
ก่อนที่เราจะเพิ่มคำสั่งลงในโปรแกรม เราตวรคิดว่า จากโจทย์ข้างบนเราจะต้องคิดว่าจะเกิดอะไรขึ้นเมื่อทุกหอคอยมี beeper แต่บางหอคอยก็อาจจะไม่มี beeper เลย เมื่อให้ง่ายขึ้นในการออกแบบโปรแกรม
ทีนี้ ลองวิเคราะห์โดยละเอียดว่าฟังก์ชัน collect_one_tower() ควรจะสั่งให้คาเรลทำอะไรบ้าง
ซึ่งทำให้สามารถเขียนแบบจำลองของฟังก์ชัน collect_one_tower()
ได้ดังนี้
def collect_one_tower():
turn_left()
collect_line_of_beepers()
turn_around()
move_to_wall()
turn_left()
คำสั่ง turn_left()
ที่จุดเริ่มต้นและจุดสิ้นสุดของฟังก์ชัน
collect_one_tower()
มีความสำคัญต่อความถูกต้องของโปรแกรมนี้
เมื่อ collect_one_tower()
ถูกเรียก คาเรลโรบอทจะอยู่แถวที่ 1 หันหน้าไปทางทิศตะวันออกเสมอ
เมื่อการดำเนินการเสร็จสิ้นโปรแกรมโดยรวมจะทำงานอย่างถูกต้อง
ก็ต่อเมื่อคาเรลหันหน้าไปทางทิศตะวันออกที่มุมเดียวกันอีกครั้ง
เงื่อนไขที่ต้องเป็นจริงก่อนที่จะเรียกใช้ฟังก์ชันเรียกว่า preconditions
และเงื่อนไขที่ต้องใช้หลังจากฟังก์ชันเสร็จสิ้นเรียกว่า postconditions.
ในขณะที่เขียนฟังก์ชั่น ผู้ที่เขียนโปรแกรมจะได้พบปัญหาน้อยกว่ามาก ถ้าได้จดบันทึกสิ่งที่มีใน
pre-postconditions
และได้จัดการให้ผลลัพธ์ออกมาตรงตามเงื่อนไขเหล่านี้ หลังจากนั้นต้องตรวจสอบให้แน่ใจว่าโปรแกรมที่เขียนขึ้นมา
สามารถทำงานได้ตรงตามเงื่อนไขที่ได้ออกแบบไว้อยู่เสมอ โดยสมมติว่าเงื่อนไขเบื้องต้นนั้นถูกต้องในการเริ่มต้น
ยกตัวอย่างเช่น
คาเรลจะทำอะไรถ้าเรียกใช้ function collect_one_tower()
เมื่อคาเรลโรบอทอยู่แถวที่ 1
หันหน้าไปทางทิศตะวันออก คำสั่ง
turn_left()
จะทำให้คาเรลหันหน้าไปทางทิศเหนือซึ่งหมายความว่าคาเรลได้รับการจัดตำแหน่งอย่างเหมาะสมกับคอลัมน์ของเหรียญในหอคอยแล้ว
ต่อมาในส่วนของ function collect_line_of_beepers()
ซึ่งยังไม่ได้ถูกเขียนขึ้นมา
แต่จะอธิบายให้เข้าใจหลักการทำงานของ
function นี้ก่อน คือ เพียงแค่เก็บเหรียญ และเดินต่อไปโดยไม่ต้องเลี้ยว เมื่อสิ้นสุดการเรียกใช้ function
collect_line_of_beepers() คาเรลจะยังคงหันไปทางทิศเหนือ ต่อมา function turn_around()
จะทำให้คาเรลหันหน้าไปทางทิศใต้
เพื่อเดินกลับมายังจุดล่างสุดและทำตามเงื่อนไขต่อไป หลังจากั้นก็เดินให้เจอกำแพง แล้วเลี้ยวซ้าย
ในตอนนี้เมื่อเรารันโปรแกรมเราเสร็จสิ้น เราก็จะสามารถแก้ไขปัญหาการทำซ้ำใน 1 หอคอยได้แล้ว!! โดยใช้หลักการ
while
loop
ถึงจุดนี้ก็น่าจะแก้ปัญหาเก็บเหรียญ 1 แถวได้สำเร็จแล้ว
ขั้นตอนต่อมาคือการจัดให้หุ่นยนต์คาเรลทำการเก็บเหรียญซ้ำๆ
กับหอคอยอื่นๆ ที่เหลือ สามารถใช้เทคนิค while loop
ในการแก้ปัญหานี้ได้ ปัญหาต่อมาคือ หน้าตาของ while loop
ควรจะออกมาในลักษณะใด
ลองวิเคราะห์จากเงื่อนไขที่มี ซึ่งต้องการให้คาเรลหยุดเดินไปข้างหน้าเพื่อเก็บเหรียญทีละแถว เมื่อเจอกำแพง ดังนั้นจึงสามารถใช้คำสั่ง front_is_clear() ในการตรวจสอบได้ คาเรลจะเดินไปเก็บเหรียญทุกครั้งที่มีการเดินจากซ้ายไปขวาบนแถวที่ 1 ผู้เขียนสามารถยัดคำสั่ง collect_one_tower() ไว้ใน code block ของ while loop แล้วประกาศคำสั่งพวกนี้ไว้ในฟังก์ชัน collect_all_beepers()
อย่างไรก็ตาม ผู้เขียนโปรแกรมควรระวังในส่วนของการออกแบบการเขียนลูปให้ฟังก์ชัน collect_all_beepers() ที่อาจจะแฝงความผิดพลาดไว้ ดังตัวอย่างด้านล่างนี้
def collect_all_beepers():
# buggy loop!
while front_is_clear():
collect_one_tower()
move()
การเขียนฟังก์ชัน collect_all_beepers() ในรูปแบบข้างบนอาจจะทำให้เกิดความผิดพลาดในแบบเดียวกับที่ได้กล่าวไปแล้วในบทที่ 6 ที่อาจจะทำให้คาเรลไม่ได้ตรวจเช็คว่ามีเหรียญที่คอลัมน์สุดท้ายหรือไม่ ดังนั้นการออกแบบฟังก์ชันที่ถูกต้องก็ควรจะให้เช็คเหรียญอีกรอบหลังจากที่ทำงานใน while loop แล้ว
def collect_all_beepers():
while front_is_clear():
collect_one_tower()
move()
collect_one_tower()
หากยังจำได้ จะสังเกตได้ว่าฟังก์ชันนี้มีลักษณะโครงสร้างเหมือนฟังก์ชัน put_beeper() ในโปรแกรม PlaceBeeperLine ที่ได้อธิบายไว้ในบทที่ 6 แค่แตกต่างกันก็ตรงชื่อของฟังก์ชันที่ในโปรแกรมนี้เรียกว่า collect_one_tower() ซึ่งแก้ปัญหาข้อผิดพลาดในรูปแบบเดียวกัน
def collect_all_beepers():
while front_is_clear():
perform some
operation.
move()
perform the same operation for the
final corner.
จากตัวอย่างข้างบน ผู้เขียนโปรแกรมสามารถนำแนวคิดในการออกแบบโปรแกรมไปประยุกต์ใช้กับปัญหาอื่นๆ ได้ ไม่ว่าจะเป็นปัญหาที่ง่ายหรือยาก ผู้เขียนโปรแกรมก็สามารถแก้ปัญหาเหล่านั้นได้ด้วยการแบ่งการแก้ปัญหาออกเป็นขั้นตอน และใช้แนวคิดที่ได้มาจากแต่ละบทที่ผ่านมา
ในขั้นสุดท้ายของการเขียนโปรแกรมที่สมบูรณ์ ให้ลองดูตัวอย่างโปรแกรมด้านล่าง จะสังเกตุเห็นได้ว่ามีบางฟังก์ชันเพิ่มขึ้นมา เช่น move_to_wall() เพื่อให้คาเรลเดินไปยังกำแพงได้ และ return_home() ที่ให้คาเรลสามารถเดินกลับไปยังจุดเริ่มต้น ก็จะถือว่าสามารถแก้ไขปัญหานี้ได้เรียบร้อย