จะเริ่มพัฒนาแอปฯยังงัยดี ตอนที่ backend ยังใช้งานจริงไม่ได้ (Part 1)

Nutron
4 min readMar 9, 2017

--

Image by Wallpapersonthe & Clipartfest

การพัฒนาโมบายแอปฯไปพร้อมๆกับฝั่ง backend เรามักพบปัญหาความไม่พร้อมของ API ที่ต้องการใช้งาน หรือในบางครั้งที่ต้องการทดสอบการทำงานของแอปฯ แต่ไม่อยากต่อกับ API ตรงๆ เพราะการควบคุมผลลัพธ์นั้นทำได้ยาก ปัญหาเหล่านี้แน่นอนว่าเราสามารถแก้ไขได้ด้วยการทำ mock data แต่จะทำอย่างไรให้โค้ดของเรามีความยืดหยุ่น และแยกส่วนของโค้ดที่ใช้เพื่อการทดสอบกับโค้ดที่ใช้งานจริงออกจากกันได้อย่างชัดเจนนั้น อาจเป็นเรื่องที่น่าปวดหัวของนักพัฒนาหลายๆคน

ในบทความชุดนี้ เราจึงพูดถึงการนำแนวคิดของการทำ Dependency Injection และ ProductFlovors ใน Android มาช่วยในการทำ Mock Data และช่วยจัดการเกี่ยวกับการเข้าถึงแหล่งข้อมูล (Data source) ที่แตกต่างกัน ทำให้เราสามารถแยกส่วนการทำงานของโค้ดได้ชัดเจนมากขึ้น ทดสอบระบบง่ายขึ้น รวมถึงวันใดวันหนึ่งหากต้องการเปลี่ยน tools หรือ library ที่ใช้ในการเข้าถึงแหล่งข้อมูล ไม่ว่าจะเป็น การเชื่อมต่ออินเทอร์เน็ต หรือการติดต่อกับฐานข้อมูล ก็จะสามารถทำได้ง่ายโดยไม่ต้องกังวลว่าจะกระทบกับส่วนการทำงานหลักของโปรแกรม

บทความชุดนี้จะแบ่งออกเป็น 2 บทความหลักๆ โดยในบทความแรก เราจะพูดถึงการนำแนวคิดของการทำ Dependency Injection (DI) มาช่วยในการทำ mock data ส่วนในบทความต่อไป เราจะมาดูกันว่า เราจะใช้ความสามารถของ productFlavors มาช่วยให้การทำ mock data สะดวกมากขึ้นได้อย่างไง

Dependency Injection (DI)

ก่อนที่จะไปลุยโค้ดก็อยากให้เข้าใจความหมายโดยย่อของเจ้า DI กันก่อน Dependency Injection เป็นเทคนิคหนึ่งที่ช่วยลดการผูกมัดกันของโค้ด โดยแทนที่ class A จะสร้าง Object B ขึ้นมาใช้งานภายใน class เอง ก็เปลี่ยนมาเป็นการส่ง ObjectB ผ่านเข้ามาทาง constructor หรือ method parameter แทน ซึ่งเทคนิคนี้นอกจากจะช่วยลดการผูกมัดของโค้ดแล้ว ยังช่วยให้เราทดสอบการทำงานของแอปฯได้ง่ายขึ้นอีกด้วย เพื่อให้เห็นภาพที่ชัดเจนมากขึ้น เราลองมาดูโค้ดตัวอย่างกันครับ

โค้ดด้านบนคือโค้ดที่ไม่ได้ทำ Dependency Injection จะเห็นว่า GroceryStore สร้าง object ของ GroceryService ขึ้นมาเอง ทำให้เกิดการผูกมัดกันระหว่างสอง class และทำให้ยากต่อการทดสอบระบบ เพราะการ Mock data ของ GroceryService นั้นจะทำได้ยาก เนื่องจากเราไม่สามารถ mock data ของ object ได้จากภายนอกนั่นเอง

ต่อไปเราลองทำ Dependency Injection ให้กับ GroceryStore เราจะได้โค้ดหน้าตาประมาณนี้ครับ

จะเห็นว่าแทนที่เราจะให้ GroceryStore เป็นผู้สร้าง object ของ GroceryService ขึ้นมาเอง เราเปลี่ยนเป็นการส่ง object GroceryService เข้ามาทาง consturctor แทน

นอกจากการส่ง object ผ่านทาง constructor แบบโค้ดด้านบนแล้ว เรายังสามารถส่ง object เข้ามาในรูปแบบอื่นได้อีก เช่น Setter Injection หรือ Interface Injection เป็นต้น ซึ่งรายละเลียดเพิ่มเติมสามารถหาอ่านได้จาก Wiki หรือบทความอื่นๆครับ

หลังจากที่เราทำความรู้จักกับ Dependency Injection กันไปแล้ว ก็ได้เวลานำแนวคิดของ DI มาใช้ในโปรเจค โดยเราจะมาดูกันว่า DI จะเข้ามาช่วยในการทำ Mock data และช่วยทำให้โค้ดของเรามีความยืดหยุ่นขึ้นได้อย่างไร

1. สร้างโปรเจค

ขั้นตอนนี้เชื่อว่าทำกันได้อยู่แล้ว ลุยเลยครับ! โดยในบทความนี้จะขอตั้งชื่อ package name ของแอปฯว่า com.sample.di

2.สร้าง Interface สำหรับการติดต่อกับ API

เนื่องจากเรากำลังจำลองการทำ mock data จาก API ดังนั้นในขั้นตอนนี้เราจะสร้าง Interface สำหรับเชื่อมต่อกับ API ขึ้นมา โดยเริ่มจากการสร้าง data class ที่เป็นผลลัพธ์ของการเรียก API ขึ้นมาก่อน ซึ่งในที่นี้ data class ของเราจะมีชื่อว่า GroceryItem อยู่ภายใต้ package main/java/com.sample.di/data ครับ

จากนั้นให้สร้าง interface ServiceApi ขึ้นมาดังนี้

เสร็จแล้วให้เราสร้าง concrete class ของ interface ขึ้นมา โดยให้ชื่อว่าServiceApiImpl

ถึงตรงนี้เราได้สร้าง class ที่เอาไว้ใช้ติดต่อกับ API ขึ้นมาเรียบร้อยแล้ว โดยใน class นี้เราจะเขียนการเชื่อมต่อขึ้นมาเอง หรือจะใช้ library อย่าง retrofit เข้ามาช่วยก็ได้ ซึ่งจะไม่ขอลงรายละเอียดของการเชื่อมต่อนะครับ

ต่อไปเราจะสร้าง class ที่จำลองการคืนค่าของ API เพื่อเอาไว้ทดสอบการทำงานของแอปฯ โดยเราจะสร้าง class MockServiceApiImpl ภายใต้ package เดียวกันดังนี้

จะเห็นว่าเราได้สร้าง mock class ที่ implement interface ServiceApi แบบเดียวกันกับ class ServiceApiImpl แต่จะแตกต่างกันตรงที่ MockServiceApiImpl ถูกสร้างขึ้นมาเพื่อทำหน้าที่จำลองการคืนค่าของ API ในขณะที่ ServiceApiImpl เป็น class ที่ไว้ใช้ติดต่อกับ API โดยตรง

สุดท้ายเราจะได้หน้าตาโครงสร้างของไฟล์ประมาณนี้ครับ

3. สร้าง Interface สำหรับเชื่อมต่อกับ ServiceApi

ในขั้นตอนต่อไปเราจะสร้าง class ที่ทำหน้าที่เป็นตัวกลางในการเรียกใช้งาน ServiceApi ทั้งนี้เพื่อทำให้โค้ดมีความยืดหยุ่นและง่ายต่อการปรับเปลี่ยนในอนาคต หากวันใดวันหนึ่งที่เราต้องการเปลี่ยน tool หรือ libary ที่ใช้ใน ServiceApi อย่างเช่น เปลี่ยนไปใช้ Retrofit แทนการเขียนการเชื่อมต่อเอง หรือ เปลี่ยนไปใช้ Realm แทนการใช้ SQLite ก็สามารถทำได้ง่าย โดยไม่กระทบกับส่วนการทำงานหลักของโปรแกรม

ให้เราสร้าง Interface ขึ้นมาก่อน โดยให้ชื่อว่า Repository

จากนั้นให้สร้าง RepositoryImpl ขึ้นมาครับ

ซึ่งจากโค้ดด้านบนจะเห็นว่า RepositoryImpl ได้นำแนวคิดของ DI เข้ามาช่วย โดยรับ object ของ ServiceApi เข้ามาผ่านทาง constructor ทำให้เราสามารถเลือกได้ว่าต้องการใช้ข้อมูลจาก mock หรือจาก API จริงๆ อีกทั้งยังช่วยลดการเกิดการผูกมัดกันระหว่างโค้ดได้อีกด้วย นอกจากนี้ยังมีการทำ caching data เพื่อลดความถี่ในการเข้าถึงแหล่งข้อมูล ทำให้แอปฯทำงานได้เร็วขึ้น

หน้าตาโครงสร้างของไฟล์ที่ได้จะประมาณนี้ครับ

4. สร้าง class ที่ช่วยจัดการกับ Repository

ต่อไปเราจะสร้าง class ที่ช่วยจัดการกับ object ของ Repository โดยจะคืนค่า object repository ให้กับผู้เรียกกรณีที่มี object repository อยู่แล้ว และจะสร้างขึ้นใหม่กรณีที่ยังไม่เคยถูกสร้างขึ้นมา และจะเก็บ object เอาไว้สำหรับการเรียกใช้งานครั้งต่อไป ให้เราสร้าง class ที่มีชื่อว่า Repositories โดยมีรายละเอียดดังนี้ครับ

จะเห็นได้ว่าภายใน Repositories เราสามารถสร้าง object ของ repository ที่ใช้ Service API ที่แตกต่างกัน โดยมี boolean flag isMockMode (ในบรรทัดที่ 6) เป็นตัวกำหนดว่า ควรเข้าถึงแหล่งข้อมูลที่เป็น MockServiceApi หรือ ServiceApi ทำให้เราสามารถสับเปลี่ยนการเข้าถึงแหล่งข้อมูลได้ง่ายขึ้น สุดท้ายเราจะได้หน้าตาโครงสร้างของไฟล์ประมาณนี้ครับ

ถึงตรงนี้เราได้สร้าง class ต่างๆไว้พร้อมสำหรับการเรียกใช้งานแล้ว และได้นำแนวคิดของ DI เข้ามาช่วย มีการวางโครงสร้างของโค้ดเพื่อให้เอื้อต่อการปรับเปลี่ยนในอนาคต และง่ายต่อการสับเปลี่ยนในการเลือกเข้าถึงแหล่งข้อมูล ในขั้นตอนต่อไปเราจะทดสอบการเรียกใช้งานกันครับ

ทดสอบการเรียกใช้งาน

หลังจากที่เตรียมทุกอย่างเสร็จเรียบร้อยแล้ว ก็ได้เวลาลองเรียกใช้งานกันแล้วหล่ะครับ โดยจะขออธิบายจากการทำงานระหว่าง class GroceryActivity และ GroceryPresenter ดังนี้

GroceryPresenter
GroceryActivity

จากโค้ดในบรรทัดที่ 12 จะเห็นว่า class GroceryActivity ส่ง object repository ให้กับ class GroceryPresenter ผ่านทาง constructor ซึ่งเป็นการนำเอาแนวคิดของ DI มาใช้ ทำให้ class GroceryPresenter ไม่ผูกมัดกับ class Repository มากเกินไป และยังทำให้เราสามารถควบคุมการเข้าถึงแหล่งข้อมูลของ GroceryPresenter ได้ ผ่านทาง Repositories โดยที่เราสามารถเลือกได้ว่าต้องการให้ใช้ข้อมูลที่เราจำลองขึ้น หรือต้องการให้ใช้ข้อมูลจาก API จริงๆ

สรุป

เราได้เห็นการนำแนวคิดของ DI เข้ามาช่วยในการทำ mock data ให้ดูเป็นระบบมากขึ้น ลดการเกิดการผูกมัดกันระหว่างโค้ด ทำให้โค้ดมีความยืดหยุ่นมากขึ้น และง่ายต่อการปรับเปลี่ยนในอนาคต และยังทำให้เราสามารถพัฒนาแอปฯต่อไปได้ ในระหว่างที่ backend ยังไม่พร้อมใช้งาน แต่อย่างไรตาม…

โค้ดที่ได้ในตอนนี้ อาจสร้างปัญหาให้กับเราในอนาคต

ปัญหาคืออะไร แล้วจะแก้อย่างไร

การที่เราสามารถสลับไปมาระหว่าง Mock environment และ Real environment ด้วย flag ใน Repositories ได้นั้น ทำให้ง่ายต่อการลืมที่จะเปลี่ยนกลับมาเป็น Real environment ในภายหลัง เกิดเป็น Human Error และอาจทำให้เราเผลอปล่อยแอปฯที่ใช้งาน mock data ออกสู่ตลาด ซึ่งไม่เป็นผลดีต่อแอปฯของเราเป็นแน่ นอกจากนี้ โค้ดที่ใช้เพื่อการทดสอบระบบและโค้ดที่ใช้งานจริงไม่ได้ถูกแยกออกจากกันอย่างชัดเจน โดยโค้ดทั้งคู่อยู่ภายใต้โมดูล main เหมือนกัน ส่งผลให้เวลา build apk file จะมีโค้ดที่ใช้เพื่อการทดสอบปะปนไปด้วย

ในบทความหน้า (part 2) เราจะมาดูกันว่า เราจะสามารถแก้ไข้ปัญหาเหล่านี้ด้วย productFlavors ได้อย่างไร โค้ดที่ได้จะถูกแบ่งเป็นสัดส่วนได้ชัดเจนมากน้อยแค่ไหน และจะช่วยลดการเกิด human error ได้อย่างไร โปรดติดต่อในบทความหน้าครับ

สุดท้ายนี้ หากผู้อ่านคิดว่าบทความนี้มีประโยชน์ ฝากกด ❤ ด้วยนะครับ

References

--

--