การย้ายจากสถาปัตยกรรมที่ทำเอง (Custom Architecture)
ไกด์นี้อธิบายแนวทางที่อาจเป็นประโยชน์เมื่อย้ายจาก Custom self-made architecture มาเป็น Feature-Sliced Design
นี่คือโครงสร้างโฟลเดอร์ของ Custom architecture ทั่วไป เราจะใช้เป็นตัวอย่างในไกด์นี้ คลิกที่ลูกศรสีฟ้าเพื่อเปิดโฟลเดอร์
📁 src
📁 actions
- 📁 product
- 📁 order
- 📁 api
- 📁 components
- 📁 containers
- 📁 constants
- 📁 i18n
- 📁 modules
- 📁 helpers
📁 routes
- 📁 products.jsx
- 📄 products.[id].jsx
- 📁 utils
- 📁 reducers
- 📁 selectors
- 📁 styles
- 📄 App.jsx
- 📄 index.js
ก่อนที่คุณจะเริ่ม
คำถามสำคัญที่สุดที่ต้องถามทีมของคุณเมื่อพิจารณาจะเปลี่ยนมาใช้ Feature-Sliced Design คือ — คุณต้องการมันจริงๆ หรือเปล่า? เรารัก Feature-Sliced Design แต่แม้แต่เราก็ยอมรับว่าบางโปรเจกต์ก็อยู่ได้สบายๆ โดยไม่ต้องมีมัน
นี่คือเหตุผลบางข้อที่ควรพิจารณาเปลี่ยน:
- สมาชิกทีมใหม่บ่นว่ามันยากที่จะเรียนรู้จนทำงานได้จริง (Productive Level)
- การแก้ไขโค้ดส่วนหนึ่งมักทำให้ส่วนอื่นที่ไม่เกี่ยวข้องกันพัง บ่อยๆ
- การเพิ่มฟังก์ชันใหม่ทำได้ยากเนื่องจากจำนวนสิ่งที่ต้องคำนึงถึงมีมากเกินไป
หลีกเลี่ยงการเปลี่ยนไปใช้ FSD โดยขัดความต้องการของเพื่อนร่วมทีม แม้ว่าคุณจะเป็น Lead ก็ตาม ก่อนอื่น โน้มน้าวเพื่อนร่วมทีมของคุณว่าประโยชน์ที่ได้คุ้มค่ากับต้นทุนการย้ายและต้นทุนการเรียนรู้ Architecture ใหม่แทนอันเดิมที่ใช้อยู่
และจำไว้ว่าการเปลี่ยนแปลงทางสถาปัตยกรรมใดๆ ไม่ได้เห็นผลทันทีในสายตาผู้บริหาร ตรวจสอบให้แน่ใจว่าพวกเขาเห็นด้วยกับการเปลี่ยนก่อนเริ่ม และอธิบายให้พวกเขาฟังว่ามันจะเป็นประโยชน์ต่อโปรเจกต์อย่างไร
ถ้าคุณต้องการตัวช่วยในการโน้มน้าว Project manager ว่า FSD มีประโยชน์ ลองพิจารณาประเด็นเหล่านี้:
- การย้ายไป FSD สามารถทำแบบค่อยเป็นค่อยไปได้ ดังนั้นมันจะไม่หยุดการพัฒนาฟีเจอร์ใหม่
- Architecture ที่ดีสามารถลดเวลาที่ Developer ใหม่ต้องใช้ในการเริ่มทำงานจริงได้อย่างมาก
- FSD เป็น Architecture ที่มีเอกสารรองรับ ดังนั้นทีมไม่ต้องเสียเวลาเขียนดูแลเอกสารของตัวเองตลอดเวลา
ถ้าคุณตัดสินใจที่จะเริ่มย้ายแล้ว สิ่งแรกที่คุณควรทำคือตั้ง Alias สำหรับ 📁 src มันจะมีประโยชน์ในภายหลังในการอ้างถึง Top-level folders เราจะใช้ @ เป็น Alias สำหรับ ./src ในส่วนที่เหลือของไกด์นี้
Step 1. แบ่งโค้ดตาม Pages
Custom architectures ส่วนใหญ่มีการแบ่งตามหน้าอยู่แล้ว ไม่ว่า Logic จะเล็กหรือใหญ่ ถ้าคุณมี 📁 pages อยู่แล้ว คุณข้ามขั้นตอนนี้ได้เลย
ถ้าคุณมีแค่ 📁 routes ให้สร้าง 📁 pages และพยายามย้าย Component code จาก 📁 routes มาให้มากที่สุดเท่าที่จะทำได้ ในอุดมคติ คุณควรจะมี Route ที่เล็กและ Page ที่ใหญ่ ขณะที่คุณย้ายโค้ด ให้สร้างโฟลเดอร์สำหรับแต่ละหน้าและเพิ่มไฟล์ Index:
ตอนนี้ ไม่เป็นไรถ้า Pages ของคุณอ้างอิงถึงกันและกัน คุณค่อยจัดการทีหลัง แต่ตอนนี้ให้โฟกัสที่การสร้างการแบ่งแยกตามหน้าที่ชัดเจน
Route file:
export { ProductPage as default } from "@/pages/product"
Page index file:
export { ProductPage } from "./ProductPage.jsx"
Page component file:
export function ProductPage(props) {
return <div />;
}
Step 2. แยกทุกอย่างอื่นออกจาก Pages
สร้างโฟลเดอร์ 📁 src/shared และย้ายทุกอย่างที่ไม่ได้ Import จาก 📁 pages หรือ 📁 routes ไปที่นั่น สร้างโฟลเดอร์ 📁 src/app และย้ายทุกอย่างที่ Import pages หรือ Routes ไปที่นั่น รวมถึง Routes เองด้วย
จำไว้ว่า Shared layer ไม่มี Slices ดังนั้นไม่เป็นไรถ้า Segments จะ Import กันเอง
คุณควรจะได้โครงสร้างไฟล์แบบนี้:
📁 src
📁 app
📁 routes
- 📄 products.jsx
- 📄 products.[id].jsx
- 📄 App.jsx
- 📄 index.js
📁 pages
📁 product
📁 ui
- 📄 ProductPage.jsx
- 📄 index.js
- 📁 catalog
📁 shared
- 📁 actions
- 📁 api
- 📁 components
- 📁 containers
- 📁 constants
- 📁 i18n
- 📁 modules
- 📁 helpers
- 📁 utils
- 📁 reducers
- 📁 selectors
- 📁 styles
Step 3. จัดการ Cross-imports ระหว่าง Pages
ค้นหาทุกจุดที่หน้าหนึ่งกำลัง Import จากอีกหน้าหนึ่ง และทำหนึ่งในสองสิ่งนี้:
- Copy-paste โค้ดที่ถูก Import ไปยังหน้าที่เรียกใช้เพื่อลบ Dependency
- ย้ายโค้ดไปไว้ใน Segment ที่เหมาะสมใน Shared:
- ถ้าเป็นส่วนของ UI kit ให้ย้ายไป
📁 shared/ui - ถ้าเป็น Configuration constant ให้ย้ายไป
📁 shared/config - ถ้าเป็น Backend interaction ให้ย้ายไป
📁 shared/api
- ถ้าเป็นส่วนของ UI kit ให้ย้ายไป
การ Copy-paste ไม่ใช่เรื่องผิดทางสถาปัตยกรรม อันที่จริง บางครั้งมันถูกต้องกว่าที่จะ Duplicate แทนที่จะ Abstract เป็น Reusable module ใหม่ เหตุผลคือบางครั้งส่วนที่แชร์กันของ Pages เริ่มที่จะแตกต่างกัน และคุณคงไม่อยากให้ Dependencies มาขัดขวางในกรณีเหล่านี้
อย่างไรก็ตาม หลักการ DRY ("Don't Repeat Yourself") ก็ยังสมเหตุสมผล ดังนั้นต้องแน่ใจว่าคุณไม่ได้ Copy-pasting business logic ไม่งั้นคุณจะต้องจำไปแก้ Bugs ในหลายที่พร้อมกัน
Step 4. รื้อ Shared Layer
คุณอาจจะมีของเยอะมากใน Shared layer ในขั้นตอนนี้ และโดยทั่วไปคุณควรหลีกเลี่ยงสิ่งนั้น เหตุผลคือ Shared layer อาจเป็น Dependency ของ Layer อื่นๆ ใน Codebase ของคุณ ดังนั้นการเปลี่ยนแปลงโค้ดตรงนั้นย่อมเสี่ยงต่อผลกระทบที่ไม่ได้ตั้งใจมากกว่า
ค้นหา Objects ทั้งหมดที่ใช้แค่ในหน้าเดียวและย้ายไปที่ Slice ของหน้านั้น และใช่ นั่นรวมถึง Actions, Reducers, และ Selectors ด้วย ไม่มีประโยชน์ที่จะรวม Actions ทั้งหมดเข้าด้วยกัน แต่มีประโยชน์ที่จะวาง Actions ที่เกี่ยวข้องไว้ใกล้กับที่ที่มันถูกใช้
คุณควรจะได้โครงสร้างไฟล์แบบนี้:
📁 src
- 📁 app (เหมือนเดิม)
📁 pages
📁 product
- 📁 actions
- 📁 reducers
- 📁 selectors
📁 ui
- 📄 Component.jsx
- 📄 Container.jsx
- 📄 ProductPage.jsx
- 📄 index.js
- 📁 catalog
📁 shared (เฉพาะของที่ Reuse)
- 📁 actions
- 📁 api
- 📁 components
- 📁 containers
- 📁 constants
- 📁 i18n
- 📁 modules
- 📁 helpers
- 📁 utils
- 📁 reducers
- 📁 selectors
- 📁 styles
Step 5. จัดระเบียบโค้ดตาม Technical purpose
ใน FSD การแบ่งตาม Technical purpose ทำด้วย Segments มีบางอันที่พบบ่อย:
ui— ทุกอย่างที่เกี่ยวกับการแสดงผล UI: UI components, Date formatters, Styles, ฯลฯapi— Backend interactions: Request functions, Data types, Mappers, ฯลฯmodel— Data model: Schemas, Interfaces, Stores, และ Business logiclib— Library code ที่ Modules อื่นใน Slice นี้ต้องการconfig— Configuration files และ Feature flags
คุณสามารถสร้าง Segments ของคุณเองได้ด้วยถ้าต้องการ แต่อย่าสร้าง Segments ที่รวมโค้ดตามสิ่งที่มันเป็น เช่น components, actions, types, utils แต่ให้รวมโค้ดตามสิ่งที่มันทำ
จัดระเบียบ Pages ของคุณใหม่เพื่อแยกโค้ดตาม Segments คุณน่าจะมี ui segment อยู่แล้ว ตอนนี้ได้เวลาสร้าง Segments อื่นๆ เช่น model สำหรับ Actions, Reducers, และ Selectors หรือ api สำหรับ Thunks และ Mutations
และจัดระเบียบ Shared layer เพื่อลบโฟลเดอร์เหล่านี้:
📁 components,📁 containers— ส่วนใหญ่ควรกลายเป็น📁 shared/ui📁 helpers,📁 utils— ถ้ามี Reused helpers เหลืออยู่ ให้รวมกลุ่มตามฟังก์ชัน เช่น Dates หรือ Type conversions และย้ายกลุ่มพวกนี้ไป📁 shared/lib📁 constants— เช่นกัน รวมกลุ่มตามฟังก์ชันและย้ายไป📁 shared/config
ขั้นตอนทางเลือก (Optional steps)
Step 6. สร้าง Entities/Features จาก Redux slices ที่ใช้ในหลายหน้า
โดยปกติ Redux slices ที่ถูก Reuse จะอธิบายสิ่งที่เกี่ยวข้องกับ Business เช่น Products หรือ Users ดังนั้นพวกนี้สามารถย้ายไป Entities layer, หนึ่ง Entity ต่อหนึ่งโฟลเดอร์ ถ้า Redux slice เกี่ยวข้องกับ Action ที่ Users ต้องการทำในแอป เช่น Comments ก็สามารถย้ายไป Features layer
Entities และ Features ควรเป็นอิสระต่อกัน ถ้า Business domain ของคุณมีการเชื่อมต่อกันโดยธรรมชาติระหว่าง Entities ให้อ้างอิงถึง Guide เกี่ยวกับ Business entities สำหรับคำแนะนำในการจัดระเบียบการเชื่อมต่อเหล่านี้
API functions ที่เกี่ยวกับ Slices เหล่านี้สามารถอยู่ที่ 📁 shared/api ได้
Step 7. Refactor Modules ของคุณ
โฟลเดอร์ 📁 modules มักใช้สำหรับ Business logic ดังนั้นมันจึงค่อนข้างคล้ายกับ Features layer ของ FSD โดยธรรมชาติ บาง Modules อาจอธิบาย UI ก้อนใหญ่ๆ เช่น App header ในกรณีนั้น คุณควรย้ายพวกมันไปที่ Widgets layer
Step 8. สร้างรากฐาน UI ที่สะอาดใน shared/ui
📁 shared/ui ในอุดมคติควรประกอบด้วยชุดของ UI elements ที่ไม่มี Business logic ฝังอยู่ และควรจะ Reusable สูง
Refactor UI components ที่เคยอยู่ใน 📁 components และ 📁 containers เพื่อแยก Business logic ออกมา ย้าย Business logic นั้นไปยัง Layers ที่สูงกว่า ถ้ามันไม่ได้ถูกใช้ในหลายที่เกินไป คุณอาจพิจารณา Copy-paste ก็ได้