จะแบ่ง PR อย่างไรดี?

champillon
7 min readDec 9, 2018

--

ช่วงนี้ที่ office เร่งปิดงานกันครับ

.

เรากำลังจะ release Public Beta ให้คนทั่วไปใช้ได้

ซึ่งในฐานะ Cheap Technology Officer
หรือ เจ้าหน้าที่เทคโนโลยีราคาถูก แบบผม

ก็ต้องลงไปช่วยน้องๆปั่นงานเป็นธรรมดา

.

ระหว่างที่ลงไปช่วย น้องๆในทีมก็งง

ว่าทำไม พี่แชมป์ ส่ง PR บ่อยจัง

ผมก็ตอบไปขำๆว่า “ผมไม่คิดเยอะ”

.

แต่ก็ไม่ได้มีโอกาสอธิบายน้องๆเค้าซักที

ก็เลยถือเอา Blog นี้ไว้อธิบายเค้าแล้วกัน

.

คำว่าไม่คิดเยอะของผม

อยากให้ดูการแบ่ง Scope ของแต่ละ Feature

แน่นอนครับทุก Feature มีงานที่ต้องทำเท่าๆกัน

แต่ที่เราทำได้คือ Scope ของ PR ให้ต่างกัน

พอ PR Scope มันเล็กลง

เรื่องที่ต้องคำนึงในการทำก็เล็กลง
มันก็เหมือนแก้สมการนั่นแหละ

ถ้าแก้สมการ 3 ตัวแปร มันก็ใช้เวลานานกว่า 1 ตัวแปร
แล้ว 3 ตัวแปร มันก็ยากกว่าด้วย

วิธีที่เราจะทำให้มันง่าย ก็คือ fix แมร่งไป 2 ตัวแปร

เหลือแก้สมการตัวแปรเดียว มันก็จะง่ายขึ้น

ดังนั้นเราเลยชอบ Scope PR ให้เล็กๆ

จะได้คิดว่าตัวเองทำงานเสร็จได้ไว

.

แต่สิ่งที่จะช่วยให้เราแบ่ง PR ได้ง่าย

ก็คือ Code Structure ของเรานั่นเอง

.

จริงๆ ที่ office ผมใช้ชื่อเรียก Structure ของ Code ที่ต่างไปจากนี้นิดหน่อย

แต่ผมขอปรับให้มันเป็น Spring Structure แล้วกัน

เพื่อให้คนอ่านทั่วๆไป เข้าใจกันได้ง่ายขึ้น

.

สมมุติเราจะเพิ่ม Feature อะไรซักอันนึง

ถ้าเราใช้ Structure แบบ Spring

เราก็จะโดนบังคับให้เขียน code ใน Layer แบบนี้

.

ดังนั้น ถ้าเราจะเพิ่ม API ซักตัว

สมมุติเป็น insert ลง Table ใหม่

งานที่เราต้องทำ ก็จะมีประมาณนี้

ดังนั้นจะเห็นว่า งานที่เราต้องทำ มี 6 ส่วน

ซึ่งแต่ละส่วน เราสามารถแยกมันอิสระออกจากกันได้

โดยการ Mock Data Object ในแต่ละส่วน

แล้วตอน Test ก็ใช้ DI ฉีด Data Mock เข้าไปแทนเพื่อ Test

.

ดังนั้น เราสามารถกำหนดได้ว่า

เราจะทำทั้ง 6 งานทีเดียวเลยมั้ย

หรือจะ scope 6 งานเป็นส่วนย่อย

.

ส่วนใหญ่ผมชอบแบ่งเป็น 2 ส่วน

คือแบ่งส่วน Entity => Repository => Table

กับส่วน Controller => Model => Service

ซึ่งบางที ถ้า Service มันซับซ้อน

ผมก็จะแบ่งเป็น Model => Service

แล้วค่อยมาปั้น Controller เป็นส่วนสุดท้ายอีก PR

.

ที่เลือกทำแบบนี้ก็เพราะ

ส่วน Entity => Repository => Table

ส่วนนี้ มันควรจะเป็นงาน “โง่ๆ, ง่ายๆ”

เพราะแค่เขียน SQL ให้ถูก

แล้ว Map มันเป็น Entity ให้ได้

แค่นั้นก็จบแล้ว

(หรือที่บางที เค้าเรียกว่า Object-Relation Mapping)

.

ก็แปลว่า ถ้าเราไม่ใส่ลูกเล่นใน SQL

(ซึ่งจริงๆ ก็ไม่ควรใส่ เพราะไม่งั้น Code จะผูกกับ Database)

เราก็จะทำ PR นี้ได้เร็วมากๆ

เพราะมันโง่ ง่าย และไม่ต้องคิดเยอะ

.

ต่อมาพอเราไม่ต้องพะวงกับ Entity => Table แล้ว

เราก็มาจัดการส่วนที่ซับซ้อนที่สุดคือ Service

ซึ่งบางทีต้องเอา 2–3 Tables มาผสมกัน

จึงอาจจะทำให้เราปวดหัวได้

.

เทคนิคที่ผมใช้คือ “Refactoring” ครับ

คือถ้าเราจะเริ่มมีเรื่องปวดหัว

เช่นต้องเอา Data ใน 2–3 Tables

มาทำอะไรกันซักอย่าง

เราต้อง “จัดระเบียบ” Code ของ Data 2–3 Tables นั้นก่อน

ก่อนที่เราจะทำอะไรเพิ่ม

เราก็จะได้ปวดหัวน้อยลง

.

การจัดระเบียบนี่แหละ ที่ผมชอบใช้คำพังเพยว่า

“Code Automate เรา, เรา Automate Code”

“Code หมุนรอบเรา, เราหมุนรอบ Code”

เพราะถ้าเราจัดระเบียบมันได้ดี

.

เราแทบไม่ต้องคิดเลยว่า

เราจะเขียน Service ที่ต้องยุ่งกับ Data 2–3 Tables ต่อยังไง

เพราะตอนแรก Code มันไม่เป็นระเบียบ

ทำให้เราไม่รู้ว่าจะจับตัวไหนมาใส่หรือไม่ใส่ดี

.

ก็เหมือนโต๊ะทำงานเรา ถ้าเรามาแล้วนั่งทำงานเลย

โดยที่ไม่จัดโต๊ะทำงาน เราก็จะหาของที่เอามาใช้ทำงานไม่เจอ

แต่ถ้าโต๊ะทำงานเราจัดระเบียบแล้ว จะหยิบปากกา หยิบดินสอ

มันก็หาได้ง่าย เหมือนมันรอให้เราใช้อยู่แล้ว

.

อีกอย่างคือ เท่าที่ผมรู้สึกเอง

การ Refactor Code มันใช้สมองคนละแบบกับการเขียน Code ใหม่

ดังนั้นเวลาผมเขียน Code ตันๆ คิดอะไรไม่ออกว่าจะทำยังไง

ผมก็จะไปนั่ง Refactor Code เล่นๆ

เผื่อว่า เราจัดระเบียบอะไรไปแล้ว

จะทำให้เราที่เราคิดไม่ออกมันคิดออก

ที่คิดออกเพราะ code ที่เราต้องทำงานเป็นระเบียบมากขึ้น

ทำให้เราคิด code ใหม่ออกง่ายขึ้น

.

ทีนี้ผมจะยกตัวอย่างให้เห็นภาพ

ว่า Feature ที่ผมทำ กับ Existing Code ที่ผมมี

ผมแบ่ง PR และเลือก Refactoring Code ตอนไหน

.

Feature ที่ผมทำคือ ยัด Invoice ที่เป็น Excel ลง Database

โดยจะยัดเป็น Draft ไว้ก่อน เพื่อที่จะรอ Confirm สำหรับส่ง Invoice

.

งานทั้งหมดมันจะเป็นประมาณนี้ครับ

  • เอา Excel จาก Client มาอยู่ใน Server ก่อง
  • แงะ Excel ออกมา แล้วตรวจสอบว่าข้อมูลใน Excel ตรง format หรือไม่ ถ้าตรงแล้วค่อยแปลง row ใน Excel => Data Object
  • ยัด Data Object ลง Database

.

ทีนี้พอเราเห็นงานแค่นี้ เราก็จะคิดแค่ว่า 3 PR ก็พอใช่มั้ยครับ

แต่ถ้าเราทำแค่ 3 PR ตรง PR ที่ 2 คือ แงะ Excel แล้วตรวจสอบข้อมูลก่อนทำเป็น Data Object เนี่ย

ดูแล้วจะมหาโหด ทำงานเยอะมากๆ แล้วไม่รู้ว่าเมื่อไหร่จะเสร็จ

ทีนี้เราจะแบ่งอย่างไรกันดีหล่ะ???

.

จุดสำคัญในการแบ่งงานคือ “โง่ ง่าย ทำไปเรื่อยๆ”

จุดนี้เน้นความถึก ในอารมณ์ใจเราครับ

เพราะถ้าเราแบ่งงานเป็นก้อนใหญ่ๆ เราจะรู้สึกว่างานมันน้อย

อย่างเช่น เราแบ่งงานทั้งหมดเป็นแค่ 3PR

เราก็จะรู้สึกว่า PR มันน้อย ทำแบบเดียวก็เสร็จ

แต่จริงๆ Evil in Detail ใน Task ที่ 2 ชัวร์

แล้วเราก็จะโดน “ปีศาจ รายละเอียด” เล่นงาน

.

สำหรับ Feature นี้ ถ้าจำไม่ผิด ผมซัดไปเกือบ 20 PR มั้ง

เพราะผมเน้น “โง่ๆ ง่ายๆ ทำไปเรื่อยๆ”

ดังนั้นการแบ่งงาน ต้องทำให้เรารักษาตัวเองอยู่ในสถานะ

เขียน Code แบบโง่ๆ แล้วเขียน Code ให้ง่ายๆ ซึ่งทำไปได้เรื่อยๆ เสมอ

เพราะถ้าเราทำงานยาก เราจะปวดหัว

พอเราปวดหัว เราก็จะเขียน Code ใน PR ต่อไปไม่ได้

ดังนั้น เราต้องแบ่งงานให้โง่ๆ ง่ายๆ เข้าไว้

.

“อย่าทำตัวเป็น Pogba ที่เล่นเหนือ แต่เพื่อนเล่นบอลต่อไม่ได้”

.

ทีนี้เราจะทำให้มันโง่ๆ ง่ายๆ อย่างไรหล่ะ

ผมเปิด Existing Code ที่เกี่ยวข้องให้เห็นก่อนแล้วกัน

.

นี่คือ Code เดิม ตอนต้นสัปดาห์ครับ มีแค่ Draft*

.

ตอนแรกที่ผมเริ่ม

ผมไล่ดู Code ใน DraftService กับ DraftRepo ก่อน

ว่ามันเยอะน้อยขนาดไหน

สรุปว่า ทั้ง 2 ไฟล์ ซัดไปเกือบ 200 บรรทัด หมดแล้ว

.

ผมก็เลยคิดว่า แมร่งมึนแน่ ถ้าจะไปต่อ

เพราะจะ 200 บรรทัดแล้ว แค่ Scroll ดูคงเสียเวลาแย่

อย่ากระนั้นเลย ก่อนอื่นเรา Refactor มันเป็นส่วนย่อยๆก่อน

.

ก็เลยทำ PR แรกเป็น Refactor แบบนี้ครับ

ทำให้มันกึ่งๆ CQRS ก่อนเลย

คือแยก Action (Insert, Delete, Update) ออกจาก Query (Select)

เพราะความจริงเรา Action กับ Data พวกนี้ไม่กี่ท่าหรอก

แต่เรา Query แมร่งหลายรูปแบบมากๆ

ไหนจะ export เป็น CSV, ไหนจะ Query by Status, ไหนจะ Filter, ไหนจะ Search

.

ดังนั้น พอเราแยก Code Query ออกจาก Action

ก็จะทำให้ Code เราสะอาดขึ้นเยอะ

พอมันสะอาด เราก็จะรู้ว่า Feature นี้

เราจะยุ่งแค่ DraftAction*

เพราะเราแค่ยัด Excel ลง Table Draft ให้ได้

ที่เหลือจะ Query มาใช้อย่างไร ก็ให้มันวิ่งตาม Flow เดิมของมัน

.

ดังนั้น DraftQuery* ที่เราแยกออกมา

เราจะไม่ไปแตะมันอีกเลยหลัง Refact แล้ว

.

เห็นมั้ยว่า PR แรก เราก็ทำอะไร โง่ๆ ง่ายๆ

ซึ่งก็คือ เราแยก method ที่เป็น Action ออกจาก method ที่เป็น Query

.

ต่อไปเราจะเริ่ม PR ที่ 2 กัน

เช่นเคย โง่ๆ ง่ายๆ อีกเหมือนเดิม

PR ที่ 2 นี่เราทำแค่ save Excel จาก Client ไปอยู่บน Server ให้ได้ก็พอ

ถ้าเอาขึ้นมาได้ถือว่า จบงาน PR ที่2

.

ดังนั้น งานเรามีแค่นี้ครับ

PR นี้โชคดีมากครับ

จุดยากของมันคือ Save File ลง Storage

(ซึ่ง Office ผมใช้ Google Storage)

.

แต่ผมโชคดีที่น้องเค้าเขียน GoogleStorageUtility ไว้แล้ว

ไว้ Save file กับ Get file จาก Storage

เพราะมี Feature อื่นๆ ที่เราต้องใช้ Google Storage อยู่แล้ว

ดังนั้น ผมไม่ต้องทำส่วนยากๆตรงนั้น (สบ๊ายยยย)

เราก็เลยทำแค่ CRUD โง่ๆ ง่ายๆ อีกแล้วครับ

.

Controller ที่ Get file MultiPart น้องก็มีตัวอย่างให้ลอก Code

ที่ผมต้องทำก็แค่ นิยาม Stack ใหม่ของ Code

จากเดิมเรามี DraftAction* กับ DraftQuery*

ซึ่งได้จากที่เราทำ refactor PR ในครั้งที่แล้ว

.

ตอนนี้เราก็เลยเพิ่ม Stack DraftImport* เข้าไปอีกอัน

เพราะ DraftImport จะถือว่ายังไม่เป็น Draft

แต่มันทำหน้าที่แปลง Excel => Draft

ที่เพิ่ม Stack ไม่ใส่ใน DraftAction* เพราะป้องกันความสับสน

ของมาเป็นชิ้น กับของมาเป็นชุด วิธีจัดการมันต่างกัน

.

หลัง PR นี่จบไปได้อย่างรวดเร็ว

เพราะไม่มีงานยากๆ มาเป็น stopper ใส่ผมเลย

(งานยากๆอย่าง GoogleStorage

หรือ parseMultiPart Data ของ HTTP

น้องทำไว้หมดแล้ว)

.

ต่อไปเราก็ทำ PR ที่ 3

Save meta Data ของ DraftImport ลง Database

PR นี้ก็หมูครับ CRUD ง่ายๆ

คิดว่าเสร็จ within and Hour or 2 Hour (ถ้าพิมพ์ช้า)

.

ต่อไปก็ Pr ที่ 4 ครับ

อันนี้เราจะแงะ Excel => Draft

จุดนี้ งานไม่ง่ายครับ

เพราะ Code แงะ Excel แมร่งไม่มีเลยซักอัน

ต้อง Google กันด่วนๆ

PR นี้ผมเผื่อเวลาไว้เยอะหน่อย

คือใช้วันหยุด 5 ธค. นี่แหละ

ซัดมันแทบทั้งช่วงเย็นหลังกินข้าวกะคุณพ่อเสร็จแล้ว

.

ดังนั้นงานที่จะเกิดขึ้นใน PR นี้มีดังนี้ครับ

PR นี้คือ ผมโยนงานงานแงะ Excel

ไปเข้าคิวการทำงานของ DraftExtractUtility (ซึ่งเราต้องเขียนขึ้นมาใหม่)

โดย Framework ที่ผมใช้

เป็น Play Framework และ Akka Actor (ตาม Stack ของ Scala)

ซึ่งทำให้เราทำ message queue ได้ง่าย

.

พอมีไฟล์ใหม่มา ไฟล์ใหม่ จะถูกส่งไปใน Queue ของ Akka Actor

เมื่อ Akka Actor ว่าง ก็จะหยิบงานใน Queue มาทำ

ซึ่งพวกนี้เป็นเรื่องทั่วๆไป ของคนที่เขียน Scala ซึ่งใช้ Reactive Mindset

หรือแปลง่ายๆว่า code แมร่งเขียนไม่ยาก แค่นั้นแหละ

.

แล้วผมค่อยใช้เวลาส่วนนึง (แทบ 2–3 ชั่วโมง)

นั่งแงะ Excel ออกมาเป็น DraftImportRecord

.

ทีนี้ก็จะมีคนถามว่าทำไมผมไม่แปลงเป็น Draft เลย

ทำไมสร้าง Stack ใหม่ขึ้นมาอีกคือ DraftImportRecord_

ก็เพราะว่า DraftImportRecord มันต้องโดน Validate ก่อน

ซึ่งไม่เหมือน Draft ที่เวลาเรารับ Draft จาก Front-end

ตัว Front-end ของผม Validate Data ให้แล้ว

.

แต่พอเรารับเป็น Excel มา Back-end แมร่งซวย

ต้องมานั่ง Validate เอง

ผมเลยแยก Stack ไว้อีกอัน

เพราะแมร่ง Data คนละตัวกัน ทำงานกันคนละหน้าที่

.

พอ PR นี้จบ

หลังจากที่หัวร้อนกับ Excel ไปซักพัก

เราก็มาเริ่ม PR ต่อไปกัน

.

PR ที่ 5 ครับ

งานนี้คือ save DraftImportRecord ลง draft_import_records

งานนี้ โง่ๆ ง่ายๆ อีกแล้วครับ CRUD ธรรมดา

ผมทำเสร็จภายใน 1 ชม. หรือถ้าแอบอู้เล่น facebook ก็ 2 ชม. ก็เสร็จ

.

แล้วเราก็มา PR ที่ 6 ครับ

PR นี้เราจะเริ่ม Validate Data Format ที่ได้จาก Excel

เช่น field ราคา ใส่ตัวหนังสือมารึป่าวอะไรแบบนี้

มันก็ไม่ยากครับ Data Format Validation ธรรมดา

เสร็จภายใน 1–2 ชม. อยู่แล้ว

.

แล้วเราก็มาถึง PR ที่ 7 กัน

สิ่งที่ต้องทำคือ Save DraftImportRecord ที่ error ไว้ใน draft_import_invalid_records

เพราะเราต้องแจ้ง User ที่ upload excel

มาว่า row ที่ไม่ผ่านพัง column ไหน และพังเพราะอะไร

(ซึ่งถ้าได้ยิน requirement นี้แล้วก็จะร้องเหี้ยกัน

เพราะต้อง validate ทุก field แล้วเช็คว่าผิดเพราะอะไรด้วย)

.

ดีนะครับ ที่ผมทำ DraftValidateUtility ใน PR ที่ 6 ไปแล้ว

ดังนั้นอะไรที่มัน Invalid เราก็แค่ save Invalid ลง Database แค่นั้น

ดังนั้น PR นี้ผมทำงานประมาณนี้ครับ

โง่ๆ ง่ายๆ เสร็จใน 2 ชม. อยู่แล้วครับ

.

ไป PR ที่ 8 กันดีกว่า

อันนี้คือ เรามี DraftImportRecord แล้ว

และเราต้องรอ User confirm ไฟล์ที่ Import เข้ามา

เพื่อ save DraftImportRecord => Draft

.

ดังนั้น PR นี้ทำแค่ QueryByDraftImportId then Save to Draft

หรือ CRUD ง่ายๆ อีกแล้วครับ

PR นี้ก็จบไปในเวลา 1–2 ชม. อีกแล้ว

.

ไป PR ที่ 9 กันดีกว่า

ทีนี้เราก็เหลือแค่นำรายการที่ Invalid ไปโชว์

CRUD ธรรมดา 1–2 ชม. จบ

.

ต่อไปก็ PR ที่ 10 ทำ Update DraftImportStatus

ซึ่ง Status ผมจะแบ่งเป็นประมาณนี้ครับ

NEW => ไฟล์เข้ามาใหม่

FAIL => ถ้าไม่ใช่ไฟล์ Excel ก็ให้มันพังไป

VALIDATING => กำลังใช้ DraftExtractUtility แงะไฟล์อยู่

VALIDATED => แงะไฟล์เสร็จแล้ว ลงใน DraftImportRecord แล้ว และมี Error ไฟล์ลงใน DraftImportInvalidRecord แล้ว

EXECUTING => กำลัง query จาก DraftImportRecord ไป save ใน Draft

EXECUTED => งานทุกอย่างเสร็จหมดแล้ว

.

งานนี้ผมก็แค่ทำ CRUD ง่ายๆ เข้าไปครับ

PR นี้ก็โง่ๆ ง่ายๆ CRUD อีกแล้วครับ

จบภายในชั่วโมง เพราะ code เดิมมีหมดแล้ว

แค่แทรกเข้าไปในโครงสร้างหลักเฉยๆ

.

PR 11 เราก็ทำเรื่องง่ายๆกันต่อไปคือ

Query DraftImportStatus มาให้หน้าบ้าน

เพราะเวลาที่เป็น VALIDATING กับ EXECUTING หน้าบ้านจะได้ทำ modal หมุนๆรอ

เราก็แค่เพิ่ม Function เข้าไปใน Stack เดิมๆของเรา

เสร็จภายใน 1 ชม. สบายๆ

.

PR 12 คือเค้าอยาก Preview ดู Record ที่ Validated แล้ว

ก่อนจะเลือก Import เข้า Draft

เราก็ทำ CRUD ง่ายๆ เช่นเคย

นี่ก็จบไปภายใน 1–2 ชม. เหมือนเดิม

.

จะเห็นได้ว่าจากทั้งหมด 12 PR นั้น เป็น CRUD โง่ ง่ายๆ 8 PR

ซึ่ง 8 PR นี่ทำเสร็จภายใน 1–2 ชม. แน่ๆ หรือใช้เวลาทั้งหมด 16 ชม.

(2 วัน ถ้าทำงานวันละ 8 ชม.)

.

ส่วนงานที่มัน ยากก็จะเหลือแค่ 4 PR แต่ 3 ใน 4 นั้น

Code เดิมมีตัวอย่างให้ลอกอยู่แล้ว

ดังนั้น เราทำงานที่ยากจริงๆแค่งานเดียว คือ แงะ Excel

.

เห็นไหมครับ จาก 3 งานใหญ่ๆ ที่เป็น Evil in Detail

พอเราแตกเป็น 12 งานย่อยๆ ล้วนมีของโง่ๆ ง่ายๆให้เราทำเสีย 11 งาน

เหลือของยากๆ จริงๆแค่งานเดียว

ผมเลยคิดเยอะแค่งานเดียว ส่วนอีก 11 งานนี่ไม่คิดอะไรเยอะเลย

ทำๆให้มันจบๆไปแค่นั้นแหละ

.

สวัสดีครับ

.

--

--