Python: Refactoring expression and classes

source: http://hubpages.com/technology/How-To-Refactor-Code-In-Python-A-Beginners-Guide

       ตอนวันศุกร์ที่ผ่านมานึกครึ้มอยากจะดู PyCon ปีนี้ว่าไปถึงไหนกันแล้ว ก็เลยนั่งไล่ลิสต์ Session ที่จะนั่งดูไว้ วันนี้มีเวลาเลยนึกได้ว่าไหนๆ ก็ดูแล้วก็คิดว่าจะสรุปเป็นตอนๆ ไว้เลยว่า session นี้ได้ไรมั่ง เผื่อเอามาอ่านทีหลังด้วย

       อย่างวันนี้นั่งดู Refactoring Python: Why and how to restructure your code สาเหตุที่เลือก session นี้มาดูเป็นอันแรกเพราะว่าคุ้นชื่อคนพูดคือ Brett Slatkin คนเขียน Effective Python นั่นแหละ พอดูๆ ไปดูๆ มาอ่าวเห้ย นี่มันเรื่องในหนังสือ Item 29, 30 นี่หว่า

       Brett เริ่มโดยการเล่าก่อนว่า Refactoring เนี่ยจริงๆ แล้วมันคือ reorganizing + rewriting code ซ้ำๆ จนกว่ามันจะ ชัดเจน สำหรับคนที่มาอ่านใหม่ (ชัดเจนนี่คือ Obvious จาก Clean Code) และสิ่งที่แตกต่างระหว่าง programming กับ refactoring คือสิ่งที่เรา optimize ไปเพื่ออะไร ตอนเราเขียนโปรแกรมเราจะ optimize จนกว่ามันจะ work แต่ถ้าเรา refactor เราจะ optimize จนกว่ามันจะชัดเจน (ภาษาไทยเราน่าจะบอกว่าอ่านง่ายมากกว่า)

เวลาที่น่าจะเหมาะกับการที่ refactor ที่สุดคือ
  • ตอนก่อนจะทำอะไรที่มันยากขึ้นไปกว่านี้
  • ตอนเขียนเทสแล้วรู้สึกว่ามันยากไปละ (everything should be easy to test)
  • ตอนที่เรารู้สึกว่ามันไม่ DRY เช่น ก็อปฟังก์ชั่นมาวางไปวางมา
  • ตอนที่เราเพิ่มฟีเจอร์ใหม่ในไฟล์เดิมแล้วโค้ดมัน couple มากๆ
  • ตอนไปอ่านโค้ดคนอื่นแล้วรู้สึกว่ามันซับซ้อนไปละ
       จะ refactor ยังไงสเตปมันจะง่ายๆ ตามนี้เลยคือ หา code ที่มันแย่ๆ ก่อนเลยแล้วทำให้มันดีขึ้น หลังจากนั้นก็เขียนกับแก้เทส วนเวียนอยู่อย่างงี้ ซึ่ง Refactor จริงๆ ส่วนใหญ่จะเป็นการ rename, split, move, simplify, หรือ Redraw boundaries ใช้ได้ทั้ง variable, function, class, module

       สิ่งที่จำเป็นและขาดไม่ได้เลยคือเทส ถ้าไม่มีเทสก็ไม่ต้องคิดจะ refactor อะไรเลย เพราะเราไม่มีอะไรจะรับประกันได้เลยว่าเราไปทำอะไรพังรึเปล่า อีกอย่างนึงที่สำคัญก็คือ source control ถ้าไม่มีก็ไม่ต้องคิดจะ refactor อีกเช่นกัน เพราะถ้าเราแก้ไฟล์ไปมากๆ แต่กลับไปจุดเดิมไม่ได้สมมติมันเปลี่ยนไปมากๆ แล้วแทนที่จะสร้างปัญหาก็มีแต่จะทำให้มันแย่ลง และสิ่งสำคัญที่สุดอย่างสุดท้ายคือกล้าที่จะทำพลาด หลายคนกลัวไม่กล้า refactor เพราะกลัวจะไปทำ code ที่เขียนไว้ก่อนหน้านี้พังแล้วแก้คืนไม่ได้

Extract Variable & Extract Function

ถ้า boolean expression มันซับซ้อนเกินไป ก็แตกมันออกมาเป็น Variable ซะ นอกจากจะอ่านง่ายขึ้นแล้ว ยังดีต่อ performance ด้วยนะ

Before
After

หรือจะแยกออกมาเป็น function ก็ได้เหมือนกัน แต่ performance แย่หน่อยแนะนำให้ทำคู่กับวิธีแรกจะดีกว่า
อีกวิธีนึงคือแยก boolean expression ออกมาเป็น boolness classes วิธีนี้จะซ่อนความซับซ้อนของ boolean expression ไว้ใน class โดยใช้ method __bool__ หรือ __nonzero__ วิธีนี้ *ดีต่อเทส* มาก

Extract Class & Move Fields

  ถ้า class เริ่มจะมี variable และความซับซ้อนเพิ่มมากขึ้นนั่นแหละเป็นสัญญาณว่าควรจะ refactor class ได้ละ วิธีนึงที่ใช้กันคือ Redraw bondaries มีสามขั้นตอนคือ

  • สร้าง interface ใหม่ที่ดีกว่าเดิม สามารถใช้งานกับ code เก่าๆ ได้ backwards compatibility และใส่ warnings ไว้ในจุดที่ใช้กับ code เก่า
  • ย้าย code ที่ใช้  interface เก่าไปใช้ interface ใหม่และอย่าลืมรัน test ทุกครั้งเพื่อเช็คว่ามี code ตรงไหนพังรึเปล่า ถ้าพังก็ไปแก้ซะ
  • ลบ code ของ interface เก่าให้หมด

  นิดนึงเรื่อง warnings คือมันเป็น standard library ตัวนึงที่เราจะเห็นมันบ่อยสุดก็ตอนรันเทส มีประโยชน์ในการ mark จุดที่เราทิ้ง backward compatibility ไว้ใน interface ใหม่โดยจะแจ้งเตือนเป็น. stdout message ถ้าเผลอไปแตะโดน code warnings เข้าแต่เราสามารถเปลี่ยน warnings ให้กลายเป็น exceptions ขึ้นมาได้ในกรณีต้องการไล่ลบ code ที่แปะ warnings พวกนี้ไว้โดยการรัน python -W error <code>.py

Before Step 1: แยก Animal Class ออกจาก Pet Class (ส่วนใหญ่จะแยกออกมาแล้วใช้ Attribute เดียวกัน)

Step 2: Class Pet รับพารามิเตอร์ใหม่ชื่อ animal แล้ว assign default value ให้เป็น None เหตุผลคือเราต้องการจะรักษา compatibility กับโค้ดในจุดอื่นๆ ที่ใช้ class Pet อยู่ นอกจากนั้นแล้ว field ที่เราย้ายไปไว้ใน Animal class เราจะรับใน **kwargs ของ Pet class แทนแล้ว assign เข้า Animal class แต่ก็แปะ warning ไว้เตือนความจำด้วยว่าเราใช้ Pet class จาก interface เก่าอยู่นะ

Step 3
: ย้าย field จาก Pet class ไป Animal class โดยใช้ decorator @property แทนการเข้าถึง field ตรงๆ แล้วใส่ warning ไว้ใน property method เพื่อเช็คได้ว่าใครยังใช้วิธีนี้เข้าถึง field นี้อยู่
Step 4: สร้าง helper method ไว้ใน class เราจะได้ไม่ต้องใส่ค่าลงไปใน field ของ class เองตรงๆ รวมถึงทำให้ใช้งาน class ได้ง่ายขึ้นด้วย

Move Field gotchas

ย้าย Field จาก Class เก่าไป class ใหม่ผ่าน constructor ของ class ใหม่แล้ว assign None ให้มัน
 ที่ class เก่าสร้าง parameter ใหม่ (**kwargs)แล้วเช็คจำนวน input ของ parameter นั้น

  • ถ้า parameter ใหม่รับ 3 parameter แปลว่า class นี้อาจจะกำลังถูกเรียกผ่าน interface เก่าอยู่ สิ่งที่เราต้องทำคือ set parameter พวกนี้เข้ากับ field แล้วใส่ warning ไว้, ถ้าเรารับมา 4 parameter แสดงว่าเป็นการเรียกใช้ แบบใหม่ละ

ใช้ @property decorator สำหรับ field เก่าเพื่อรักษา backward compatibility เมื่อเราเปลี่ยนชื่อ field ใหม่ซึ่งจะก่อให้เกิดปัญหาถัดไป

@property มีปัญหาตรงที่ใช้สำหรับอ่านค่าได้อย่างเดียว ถ้าเราต้องการจะรักษาความสามารถในการเขียนค่าลงใน field ของ class เราต้องใช้ decorator อีกตัวคือ @<property_name>.setter วิธีนี้จะทำให้ property ใหม่เราสามารถที่จะเขียนค่าลงใน class ได้ (อย่าลืมใส่ warning ไว้ด้วยเพราะเป็นวิธีใช้ class แบบเก่าอยู่)

ถ้าเราย้าย field ไป class ใหม่หมดแล้วควรจะทิ้ง @property ไว้ซักพักหนึ่งแต่แทนที่ข้างใน method ด้วย AttributeError exception แทน

และนี่คือหน้าตาของ class Pet กับ Animal หลัง Refactor เสร็จแล้วครับ

Reference

- bslatkin/pycon2016
- Defining “boolness” of a class in python - Stack Overflow
- Refactoring Python: Why and how to restructure your code — PyCon 2016 - Google สไลด์

Comments