ISP (Interface Segregation Principle) คือหลักการ SOLID ข้อที่สี่ ซึ่งระบุว่า: ไคลเอนต์ไม่ควรพึ่งพาเมธอดที่พวกเขาไม่ได้ใช้งาน หลักการนี้ถูกกำหนดโดย Robert Martin ในบริบทของการออกแบบอินเทอร์เฟซสำหรับระบบเชิงวัตถุ ตามที่อธิบายไว้ในหนังสือ Clean Architecture (2017), หลักการแยกอินเทอร์เฟซ กำหนดให้สร้างอินเทอร์เฟซที่เชี่ยวชาญเฉพาะแคบ ๆ แทนที่จะเป็นอินเทอร์เฟซสากลเดียว ซึ่งช่วยลดการเชื่อมโยงและทำให้การเปลี่ยนแปลงง่ายขึ้น
ประเด็นสำคัญ
ISP (Interface Segregation Principle) คือหลักการแยกอินเทอร์เฟซที่ห้ามสร้างอินเทอร์เฟซ “อ้วน” ที่มีเมธอดซึ่งไคลเอนต์ทั้งหมดไม่ได้ใช้งาน แทนที่จะมีอินเทอร์เฟซเดียวที่มีเมธอดมากมาย จะมีการออกแบบอินเทอร์เฟซเล็ก ๆ หลายอัน แต่ละอันสำหรับกลุ่มไคลเอนต์ของตัวเอง
หลักการนี้ถูกนำเสนอโดย Robert Martin เพื่อเป็นวิธีแก้ปัญหาของ “การปนเปื้อนอินเทอร์เฟซ” เมื่อคลาสถูกบังคับให้ใช้งานเมธอดที่มันไม่ต้องการเพียงเพราะมีการประกาศในอินเทอร์เฟซร่วม ในภาษาที่มีการกำหนดชนิดแบบคงที่ สิ่งนี้นำไปสู่การใช้งานที่ว่างเปล่าหรือการขว้างข้อยกเว้น ซึ่งเป็นสัญญาณโดยตรงของการละเมิด ISP
ISP และ SRP เสริมซึ่งกันและกัน: SRP เกี่ยวกับความรับผิดชอบของคลาส ISP เกี่ยวกับสัญญาของอินเทอร์เฟซ SRP กล่าวว่า “หนึ่งคลาส — เหตุผลหนึ่งในการเปลี่ยนแปลง” ISP กล่าวว่า “หนึ่งอินเทอร์เฟซ — หนึ่งสถานการณ์ไคลเอนต์” เมื่อรวมกันแล้วจะสร้างสถาปัตยกรรมแบบโมดูลาร์ที่แต่ละองค์ประกอบของระบบมีขอบเขตที่ชัดเจน
Fat Interface — อินเทอร์เฟซที่มีเมธอดมากกว่าที่ไคลเอนต์เฉพาะต้องการ ตัวอย่างเช่น อินเทอร์เฟซ Worker ที่มีเมธอด work, eat, sleep หุ่นยนต์ทำงานไม่ควรใช้งาน eat และ sleep แต่ถูกบังคับให้ทำ วิธีแก้ไขคือแบ่งเป็น Workable, Eatable, Sleepable ไคลเอนต์แต่ละรายได้รับสิ่งที่ต้องการอย่างพอดี
ในการพัฒนาแอปพลิเคชันมือถือ อินเทอร์เฟซอ้วนพบได้ในโปรโตคอลผู้แทนและ DataSource โปรโตคอลหนึ่งสามารถมีเมธอดสำหรับสองสถานการณ์ที่แตกต่างกัน (การแก้ไข + การแสดงผล) แม้ว่าหน้าจอเฉพาะจะใช้เพียงหนึ่งในนั้น
การใช้งาน ISP เริ่มต้นด้วยการวิเคราะห์ไคลเอนต์ของแต่ละอินเทอร์เฟซ หากไคลเอนต์สองรายใช้ชุดเมธอดที่แตกต่างกันของอินเทอร์เฟซเดียว — ควรแบ่งอินเทอร์เฟซนั้น อินเทอร์เฟซใหม่แต่ละอันจะจัดกลุ่มเมธอดที่ถูกเรียกใช้ร่วมกันภายในสถานการณ์เดียว
กลไกการแบ่ง: อินเทอร์เฟซเดิมถูกแบ่งออกเป็นอินเทอร์เฟซแคบ ๆ หลายอัน แต่ละอันสืบทอดส่วนร่วม (ถ้ามี) ไคลเอนต์เปลี่ยนไปพึ่งพาอินเทอร์เฟซแคบที่ต้องการแทนที่จะเป็นอินเทอร์เฟซทั่วไป คลาสที่ใช้งานอินเทอร์เฟซเดิมตอนนี้จะใช้งานเฉพาะอินเทอร์เฟซแคบที่พวกเขาต้องการจริง ๆ
คำชี้แจงสำคัญ: ระดับการแบ่ง ถูกกำหนดโดยจำนวนไคลเอนต์และสถานการณ์ของพวกเขา ISP ไม่ต้องการการแบ่งสูงสุด (ไมโครอินเทอร์เฟซที่มีเมธอดเดียวแต่ละอัน) ซึ่งจะนำไปสู่ความซับซ้อนที่มากเกินไป เป้าหมายคือกำจัดการพึ่งพาของไคลเอนต์ต่อเมธอดที่ไม่จำเป็น ไม่ใช่การลดขนาดของแต่ละอินเทอร์เฟซ
สัญญาณหลัก ของการละเมิด ISP ได้แก่: คลาสที่ใช้งานอินเทอร์เฟซที่มีเมธอดว่างเปล่า (การใช้งานจำลอง), การขว้าง UnsupportedOperationException ในการใช้งาน, พารามิเตอร์หรือชนิดส่งคืนจำนวนมากที่ไคลเอนต์บางส่วนไม่ได้ใช้, และการเปลี่ยนแปลงอินเทอร์เฟซบ่อยครั้งที่ส่งผลกระทบต่อเพียงบางไคลเอนต์
ในการพัฒนา Android ตัวอย่างทั่วไปของการละเมิด ISP คืออินเทอร์เฟซ OnItemClickListener ซึ่งรวมเมธอดสำหรับคลิก คลิกค้าง และปัด หากหน้าจอเฉพาะใช้เพียงคลิก — เมธอดที่เหลือจะว่างเปล่า วิธีแก้ไขคือแบ่งเป็น OnItemClickListener, OnItemLongClickListener, OnItemSwipeListener
ในการพัฒนา iOS การละเมิด ISP ปรากฏใน ผู้แทน UIKit: โปรโตคอลหนึ่งมีเมธอดสำหรับสถานะต่าง ๆ ของคอมโพเนนต์ UITableViewDelegate รวมเมธอดสำหรับการแสดงผล การเลือก การแก้ไข และการปัด นักพัฒนามักจะใช้งานโปรโตคอลทั้งหมดด้วยเมธอดว่างเปล่ามากมาย การแบ่งเป็นหลายโปรโตคอลตามกลุ่มความรับผิดชอบช่วยแก้ปัญหา
ปัญหา ไม่ใช่แค่เรื่องความสวยงามของโค้ด เมื่ออินเทอร์เฟซเปลี่ยนแปลง (เพิ่มเมธอดใหม่) คลาสที่ใช้งานทั้งหมดต้องได้รับการอัปเดต — แม้แต่คลาสที่ไม่ต้องการเมธอดใหม่ ในการพัฒนาแอปพลิเคชันมือถือที่มีหน้าจอมากมาย สิ่งนี้นำไปสู่การเปลี่ยนแปลงแบบลูกโซ่ ISP แยกไคลเอนต์แต่ละรายจากการเปลี่ยนแปลงที่ไม่เกี่ยวข้องกับมัน
การละเมิด ISP โดยนัย เกิดขึ้นผ่านพารามิเตอร์การกำหนดค่า หากเมธอดรับวัตถุที่มีฟิลด์จำนวนมาก และไคลเอนต์ใช้เพียง 2-3 ฟิลด์ — นี่คือสัญญาณให้แบ่ง ทางเลือก: เมธอดเฉพาะทางหลายอันพร้อมชุดพารามิเตอร์ขั้นต่ำ
ในการพัฒนา Android ISP ถูกละเมิดเมื่อใช้ SharedPreferencesManager เดียวสำหรับอ่านและเขียนการตั้งค่าแอปพลิเคชันทั้งหมด Fragment ที่ต้องการเพียงอ่านธีมจะได้รับการพึ่งพาตัวจัดการส่วนกลางที่มีเมธอดมากมายสำหรับชนิดข้อมูลต่าง ๆ การแบ่งเป็น ThemePreferenceProvider, AuthPreferenceProvider, FeatureFlagProvider — การประยุกต์ใช้ ISP ในระดับบริการกำหนดค่า ผู้ให้บริการแต่ละราย มีเมธอดที่ไคลเอนต์ต้องการพอดี
พิจารณา ตัวอย่าง Android ที่มีอินเทอร์เฟซสำหรับทำงานกับข้อมูล การละเมิด ISP — อินเทอร์เฟซเดียวสำหรับการดำเนินการ CRUD ทั้งหมด แม้ว่าไคลเอนต์บางรายไม่ต้องการการดำเนินการทั้งหมด
// การละเมิด ISP: อินเทอร์เฟซอ้วน
interface UserRepository {
fun getAll(): List<User>
fun getById(id: Int): User
fun save(user: User)
fun delete(id: Int)
}
// หลังจากใช้ ISP: อินเทอร์เฟซแคบ
interface UserReader {
fun getAll(): List<User>
fun getById(id: Int): User
}
interface UserWriter {
fun save(user: User)
fun delete(id: Int)
}
// ReadOnlyViewModel ไม่ได้พึ่งพาเมธอดการเขียน
class ReadOnlyViewModel(
private val reader: UserReader
)
ตัวอย่าง iOS ที่มีการแยกโปรโตคอลสำหรับทำงานกับสื่อ:
// การละเมิด ISP: หนึ่งโปรโตคอลสำหรับงานสื่อทั้งหมด
protocol MediaService {
func play(url: URL)
func pause()
func stop()
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// หลังจาก ISP: การแยกเป็นโปรโตคอลตามความรับผิดชอบ
protocol MediaPlayer {
func play(url: URL)
func pause()
func stop()
}
protocol MediaTransfer {
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// PlayerViewModel ไม่ได้พึ่งพาเมธอดการดาวน์โหลด
class PlayerViewModel {
private let player: MediaPlayer
}
ข้อสรุปเชิงปฏิบัติ: ISP ปกป้องไคลเอนต์จากการเปลี่ยนแปลงในส่วนที่ไม่เกี่ยวข้องของอินเทอร์เฟซ การแบ่ง UserRepository เป็น UserReader และ UserWriter หมายความว่าการเปลี่ยนแปลงใน save จะไม่ส่งผลกระทบต่อ ReadOnlyViewModel และในทางกลับกัน ไคลเอนต์แต่ละรายถูกแยก จากฟังก์ชันการทำงานที่ไม่ได้ใช้ และไม่ต้องการการเปลี่ยนแปลงเมื่อส่วนอื่น ๆ ของระบบถูกแก้ไข
ISP และ SRP — คู่ที่เป็นธรรมชาติ SRP กำหนดว่าคลาสควรมีเหตุผลในการเปลี่ยนแปลงเพียงหนึ่งเดียว ISP ใช้ตรรกะเดียวกันกับอินเทอร์เฟซ: อินเทอร์เฟซควรให้บริการหนึ่งสถานการณ์ไคลเอนต์ คลาสสามารถใช้งานอินเทอร์เฟซแคบหลายอัน (แต่ละอันสอดคล้องกับความรับผิดชอบหนึ่ง) ซึ่งสะอาดกว่าอินเทอร์เฟซอ้วนเดียวที่มีความรับผิดชอบหลายอย่าง
ISP และ OCP ก็เกี่ยวข้องกัน: อินเทอร์เฟซแคบขยายได้ง่ายกว่า การเพิ่มเมธอดใหม่ในอินเทอร์เฟซแคบส่งผลกระทบต่อไคลเอนต์ของมันเท่านั้น การเพิ่มเมธอดในอินเทอร์เฟซอ้วนส่งผลกระทบต่อไคลเอนต์ทั้งหมด — อาจละเมิด OCP หากไคลเอนต์ถูกบังคับให้เปลี่ยนการใช้งานของพวกเขา
ISP และ DIP ทำงานร่วมกัน: DIP ต้องการการพึ่งพานามธรรม ISP ทำให้นามธรรมเหล่านี้แคบและโฟกัส การพึ่งพาอินเทอร์เฟซกว้างยังคงเป็นการพึ่งพานามธรรม แต่นามธรรม “ไม่ดี” จากมุมมองของ ISP หลักการสี่ประการ (SRP, OCP, ISP, DIP) สร้าง “พีระมิดโมดูลาร์”: SRP และ ISP กำหนดขอบเขต OCP และ DIP กำหนดวิธีการขยายและการเชื่อมโยง
สถาปัตยกรรมคอมโพเนนต์ ในโปรเจกต์มือถือ (โมดูล, ฟีเจอร์, เลเยอร์) ได้รับประโยชน์จาก ISP ในระดับ API สาธารณะ แต่ละโมดูลส่งออกอินเทอร์เฟซแคบสำหรับผู้บริโภคของมัน แทนที่จะเป็นส่วนหน้าส่วนกลางเดียว สิ่งนี้ช่วยให้เปลี่ยนแปลงการใช้งานภายในของโมดูลโดยไม่ส่งผลกระทบต่อผู้บริโภคที่ใช้เพียงส่วนหนึ่งของฟังก์ชันการทำงานของมัน
ในโปรเจกต์ Android ที่มี Clean Architecture ISP ถูกประยุกต์ใช้กับ UseCases: แต่ละ UseCase เป็นอินเทอร์เฟซแยกต่างหากที่มีเมธอด invoke หรือ execute เดียว ไคลเอนต์ (ViewModel) พึ่งพาเฉพาะ UseCase ที่มันต้องการ แทนที่จะเป็น repository ทั้งหมด สิ่งนี้ทำให้การพึ่งพาโปร่งใสและทดสอบได้
คำถามที่พบบ่อย
ใช่ การแบ่งมากเกินไป เป็นไปได้ ISP ไม่ต้องการหนึ่งอินเทอร์เฟซต่อเมธอด เกณฑ์คือ: มีไคลเอนต์ที่ต้องการเพียงบางส่วนของเมธอดของอินเทอร์เฟซหรือไม่? หากไคลเอนต์ทั้งหมดใช้เมธอดทั้งหมด — ไม่จำเป็นต้องแบ่งอินเทอร์เฟซ ระดับการแบ่งที่เหมาะสมถูกกำหนดโดยสถานการณ์การใช้งานจริง
ISP ในระดับพารามิเตอร์ หมายถึง: ฟังก์ชันไม่ควรรับวัตถุที่มีฟิลด์จำนวนมากหากใช้เพียงบางส่วน แทนที่จะทำอย่างนั้น ควรส่งเฉพาะข้อมูลที่จำเป็นหรือใช้อินเทอร์เฟซเฉพาะทาง (ตัวอย่างเช่น อินเทอร์เฟซ Renderable แทน User ที่สมบูรณ์)
LSP เกี่ยวกับการสืบทอดที่ถูกต้องและความเข้ากันได้ของชนิดย่อยเชิงพฤติกรรม ISP เกี่ยวกับการออกแบบอินเทอร์เฟซ: ไคลเอนต์ไม่ควรพึ่งพาเมธอดที่ไม่ได้ใช้ LSP ตอบคำถาม “สามารถใช้คลาสย่อยแทนคลาสฐานได้หรือไม่?” ISP ตอบคำถาม “ไคลเอนต์ต้องการอินเทอร์เฟซทั้งหมดหรือไม่?”
อินเทอร์เฟซแคบ ช่วยให้การสร้างออบเจกต์จำลองง่ายขึ้น: การทดสอบสร้าง mock ที่มีเมธอดหนึ่งหรือสองเมธอด ไม่ใช่เมธอดมากมาย ยิ่งอินเทอร์เฟซมีเมธอดน้อยเท่าไร การจำลองพฤติกรรมของมันก็ง่ายขึ้นเท่านั้น สิ่งนี้ช่วยลดภาระทางปัญญาของผู้พัฒนาทดสอบและลดโอกาสเกิดข้อผิดพลาดในตรรกะของ mock
หากอินเทอร์เฟซ เสถียรและไคลเอนต์ทั้งหมดใช้เมธอดทั้งหมด — การแบ่งก็ซ้ำซ้อน ตัวอย่างทั่วไป: โปรโตคอล UIKit ที่ออกแบบโดย Apple การแบ่งพวกมันมีความเสี่ยงเพราะ UIKit คาดหวังการใช้งานผู้แทนอย่างสมบูรณ์ ในกรณีเช่นนี้ การละเมิด ISP เป็นสิ่งที่สมเหตุสมผลเนื่องจากความเสถียรของ API
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ