Single Responsibility Principle คืออะไร

The S from SOLID Principles

Travis P
Black Lens
Published in
3 min readDec 15, 2017

--

SOLID เป็นตัวย่อครับ ตัวย่อของหลักการด้านการเขียนโค้ด ให้ออกมาอ่านง่าย เมนเทนง่าย และยืดหยุ่น หลักทั้ง 5 ประกอบด้วย

  1. Single responsibility principle
  2. Open/close principle
  3. Liskov substitution principle
  4. Interface segregation principle
  5. Dependency inversion principle

ตลอดซีรีส์ SOLID นี้ เรามาทำความรู้จักแต่ละตัวกันครับ ถึงแม้โค้ดตัวอย่างจะเป็น Kotlin และมี Android แทรกเล็กน้อย แต่หลักทั้ง 5 นี้สามารถใช้ได้กับการเขียนโค้ดทุกอย่างเลย ไม่ได้เจาะจงอย่างใดอย่างหนึ่ง

Single Responsibility Principle

1 class ทำหน้าที่ 1 อย่าง

อันนี้น่าจะเคยได้ยินกันทุกคน สิ่งแรกที่ผมสงสัยคือ หน้าที่ 1 อย่างนี่ใช้อะไรตัดสิน
ซึ่งเค้าก็มักอธิบายกันว่า

ควรมีเพียง 1 เหตุผลเท่านั้นที่ class จะถูกแก้ไข

โอเค ชัดเจนขึ้น ต้องคิดเยอะขึ้น ต้องคิดให้รอบคอบด้วย ไม่งั้นเขียนเสร็จแล้วมาเจอเพิ่มทีหลังต้องแก้กันอีก เพราะสุดท้ายแล้วจุดประสงค์ของหลักการนี้คือ

ถ้าต้องแก้โค้ด ควรแก้ให้น้อยที่สุด

ตัวอย่างง่ายๆ

class ReportGenerator {
fun generateReport(): String {
// generate report and format as html string
}
}

สมมติว่าเรามีคลาสนึงที่เอาไว้ผลิต report อะไรสักอย่างออกมาเป็น html คลาสนี้ไม่ประพฤติตัวตาม Single Resposibility Principle (ต่อไปนี้ขอเรียกว่า SRP) เพราะมี 2 เหตุผลที่จะถูกแก้คือ 1. เนื้อหาเปลี่ยน 2. เปลี่ยนการ format จาก html เป็น xml (หรืออื่นๆ) เพราะฉะนั้น เรามาทำให้มันเป็น SRP กันครับ

class ReportGenerator {  val formatter: ReportFormatter = ReportFormatter()  fun generateReport(): String {
val report = // generate Report
return formatter.format(report)
}
}class ReportFormatter {
fun formatReport(report: Report): String {
return // format Report as html string
}
}

โอเค ทีนี้ถ้าเราจะแก้เนื้อหาก็แก้ใน ReportGenerator ถ้าจะแก้ format ก็แก้ใน ReportFormatter แทน

แต่ชีวิตจริงไม่ได้ง่ายขนาดนั้นใช่มั้ยครับ ตัวอย่างง่ายๆเราก็เข้าใจ แต่พอพยายามมาใช้ในชีวิตจริงนี่ไปต่อกันไม่ถูกเลย งั้นมาดูตัวอย่างที่ใกล้เคียงชีวิตจริงมากขึ้นดีกว่า

ตัวอย่างใกล้เคียงชีวิตจริง

class AuthService {  val sharedPreferences: SharedPreferences = //...  fun login(em: String, pw: String) {
val token = callApi("https://blp.com/login?e=${em}&p=${pw}")
sharedPreferences.edit().putString("KEY_TOKEN", token).apply()
}
fun logout() {
sharedPreference.edit().remove("KEY_TOKEN").apply()
}
fun isLogin(): Boolean {
return sharedPreference.getString("KEY_TOKEN", null) != null
}
fun callApi(url: String): String //...}

สมมติผมเขียน AuthService ขึ้นมาคลาสนึง ผมตั้งใจให้มันมีหน้าที่ 1 อย่างคือ จัดการ user authentication ระหว่าง app กับ backend ถ้าไม่ได้คิดอะไรมาก ก็ดูตรงกับหลักการ SRP ดีแล้ว แต่ถ้ามาไล่นับว่ามีกี่เหตุผลที่จะแก้คลาสดูล่ะ

  1. เปลี่ยน login endpoint
  2. เปลี่ยนวิธีเก็บ token เช่น จาก shared preference เป็น SQLite
  3. เปลี่ยน library ยิง http เช่น จาก Volley เป็น Retrofit

อย่างนี้ไม่ใช่ SRP แน่นอน มีตั้ง 3 เหตุผล เราต้องแตกเป็นคลาสย่อยๆซะแล้ว

class HttpService {
fun get(url: String): String //...
fun post(url: String, body: Any? = null): String //...
//...
}

เริ่มจากแยกส่วน networking ออกมา ทีนี้ถ้าจะเปลี่ยน http library ก็เปลี่ยนคลาสนี้คลาสเดียว

class TokenStorageService {  val KEY_TOKEN = "com.blp.KEY_TOKEN"
val sharedPreferences = //...
fun putToken(token: String) {
sharedPreferences.edit().putString(KEY_TOKEN, token)
}
fun getToken(): String? {
return sharedPreferences.getString(KEY_TOKEN, null)
}
fun removeToken() {
sharedPreference.edit().remove("KEY_TOKEN").apply()
}
}

จากนั้นก็แยกส่วนการเก็บ token ออกมา ทีนี้จะเก็บเป็น SharedPreference หรือ SQLite หรือเขียนลง file ก็ทำเฉพาะในนี้

class AuthService {  val http: HttpService = HttpService()
val storage: TokenStorageService = TokenStorageService()
fun login(em: String, pw: String) {
val token = http.get("https://blp.com/login?e=${em}&p=${pw}")
storage.putToken(token)
}
fun logout() {
storage.removeToken()
}
fun isLogin(): Boolean {
return storage.getToken() != null
}
}

สิ่งที่เหลือคือถ้าจะเปลี่ยน login endpoint ก็เปลี่ยนที่นี่ เย้! ดูเข้าที่เข้าทาง

เมื่อ Single Responsible Principle สั่นคลอน

สามวันต่อมาทาง backend บอกว่า
1. ต่อไปนี้ยิง login ด้วย http post นะ
2. ตอน logout ให้ยิง token ด้วย http delete นะ

อ้าวววว ยังไงดีล่ะทีนี้ มี 2 เหตุผลให้แก้ 1 คลาส ถึงเวลาต้องแยกคลาสแล้วมั้ง

// JUST FOR ILLUSTRATION. NOT A GOOD IDEA.class AuthService {
val storage = TokenStorageService()
val loginService = LoginService()
val logoutService = LogoutService()
fun login(em: String, pw: String) = loginService.login(em, pw)
fun logout() = logoutService.logout()
fun isLogin() = storage.getToken() != null
}
class LoginService {
val http = HttpService()
val storage = TokenStorageService()
fun login(em: String, pw: String) {
val token = http.post("https://blp.com/login", ...)
storage.putToken(token)
}
}
class LogoutService {
val http = HttpService()
val storage = TokenStorageService()
fun logout() {
val token = storage.getToken()
http.delete("https://blp.com/logout", token)
storage.removeToken()
}
}
// JUST FOR ILLUSTRATION. NOT A GOOD IDEA.

AuthService เราถูกแยกออกไปเป็น LoginService กับ LogoutService ละ ทีนี้แต่ละคลาสก็มีเพียงเหตุผลเดียวที่จะเปลี่ยนตรงตาม SRP

โดยรวมแล้วเขียนโค้ดเยอะขึ้น คลาสเยอะขึ้น AuthService / LoginService / LogoutService ทำงานอยู่นิดเดียว แล้วก็ส่งงานให้คนอื่นทำ logic กระจายไปตามที่ต่างๆ อ่านเข้าใจยากขึ้น

สรุปว่าจะใช้ Single Responsibility Principle ยังไงดี

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

ขอบเขตหน้าที่ของคลาส และ 1 เหตุผลที่อนุญาติให้เปลี่ยนแปลงคลาส เป็นสิ่งที่ไม่แน่นอนมากๆ ขึ้นกับว่าเราจะกำหนดยังไง

ไม่ว่าผมจะเข้าใจ SRP ผิดเอง หรือ SRP มันเป็นอย่างนั้นจริงๆ แต่ถ้าโค้ดมันเริ่มออกมาแปลกๆ เขียนเยอะ อ่านเข้าใจยาก ผมว่าผมถอยออกมาพิจารณาอีกที แล้วใช้ OLID principles อีก 4 ตัวช่วย ออกมาให้ได้โค้ดที่ผมสบายใจดีกว่า

--

--

Travis P
Black Lens

Android Developer, Kotlin & Flutter Enthusiast and Gamer