ซีรีส์ Kriptofolio App - ภาค 1
Software อยู่ในสถานะเปลี่ยนแปลงตลอด แต่ละการเปลี่ยนแปลงอาจกระทบ Negative ทั่วโปรเจกต์ Essential: ป้องกัน Damage ระหว่าง Implement Changes
กับ "Kriptofolio" (เดิม "My Crypto Coins") จะสร้างโค้ดใหม่เยอะ Step by Step อยากเริ่มต้นที่ดี Project ต้อง Solid Quality ก่อนเข้าใจ Foundation Principles สร้าง Modern Software เรียกว่า SOLID Principles ชื่อดูจับตา! 😎
เนื้อหาซีรีส์
- Introduction: Roadmap สร้าง Modern Android App 2018–2019
- Part 1: แนะนำ SOLID Principles (คุณอยู่ที่นี่)
- Part 2: เริ่มสร้าง Android App: Mockups, UI, XML Layouts
- Part 3: Architecture Patterns และการนำไปใช้
- Part 4: Dependency Injection ด้วย Dagger 2
- Part 5: RESTful Web Services ด้วย Retrofit, OkHttp, Gson, Glide, Coroutines
Principles Slogan
SOLID = Mnemonic Acronym จำง่าย กำหนด 5 Basic Object-Oriented Design Principles:
- Single Responsibility Principle (SRP)
- Open-Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
จะอภิปรายแต่ละ Principle พร้อม Bad vs Good Code Examples (Kotlin สำหรับ Android)
Single Responsibility Principle (SRP)
Class ควรมี Responsibility เดียวเท่านั้น
แต่ละ Class/Module รับผิดชอบหนึ่งส่วนของ Functionality App ถ้าทำหนึ่งอย่าง ควรมี Reason to Change เดียว ถ้า Class ทำหลายอย่าง ให้แยก Functionalities
ตัวอย่างเข้าใจง่าย: Swiss Army Knife มีหลาย Functions: Blade, Screwdrivers, Can Opener ฯลฯ
คำถาม: ทำไมผมนำ Knife นี้มา Example Single Functionality? คิดดู: Feature หลักอีกอย่างคือ Mobility ขนาดกระเป๋า แม้มีหลาย Functions ยัง Fit Main Purpose: Portable
Programming เหมือนกัน Class/Module ต้องมี Main Global Purpose แต่ไม่ควร Over-simplify แยก Functionality มากเกินไป จงจำ: Balance
Classic Example: Method onBindViewHolder ใน RecyclerView Adapter
❌ BAD CODE EXAMPLE:
class MusicVinylRecordRecyclerViewAdapter(
private val vinyls: List,
private val itemLayout: Int
) : RecyclerView.Adapter() {
...
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val vinyl = vinyls[position]
holder.itemView.tag = vinyl
holder.title!!.text = vinyl.title
holder.author!!.text = vinyl.author
holder.releaseYear!!.text = vinyl.releaseYear
holder.country!!.text = vinyl.country
holder.condition!!.text = vinyl.condition
/**
* Method นี้違反 SRP!!!
* Responsibility หลักคือ Adapt VinylRecord Object ไป View Representation
* แต่มันยังทำ Data Formatting ด้วย
* มี Multiple Reasons to Change ในอนาคต = ผิด
*/
var genreStr = ""
for (genre in vinyl.genres!!) {
genreStr += genre + ", "
}
genreStr = if (genreStr.isNotEmpty()) genreStr.substring(0, genreStr.length - 2) else genreStr
holder.genre!!.text = genreStr
}
...
}
✅ GOOD CODE EXAMPLE:
class MusicVinylRecordRecyclerViewAdapter(
private val vinyls: List,
private val itemLayout: Int
) : RecyclerView.Adapter() {
...
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val vinyl = vinyls[position]
holder.itemView.tag = vinyl
holder.title!!.text = vinyl.title
holder.author!!.text = vinyl.author
holder.releaseYear!!.text = vinyl.releaseYear
holder.country!!.text = vinyl.country
holder.condition!!.text = vinyl.condition
/**
* แทนที่จะ Format Data ที่นี่ ย้าย Responsibility ไป Class อื่น
* ที่นี่เห็นแค่ Direct Call Top-level Function convertArrayListToString (Kotlin Feature)
* แต่ Kotlin Compiler ทำให้เป็น Java Class Top-level Functions → Static Methods
* แต่ละ Class ยังคง Single Responsibility
*/
holder.genre!!.text = convertArrayListToString(vinyl.genres)
}
...
}
Code ที่ Design ตาม SRP จะใกล้ Principles อื่นๆ ที่จะพูดต่อไป
Open-Closed Principle (OCP)
Software Entities ควร Open for Extension แต่ Closed for Modification
Principle นี้: เขียน Classes, Modules, Functions ทำให้ Open for Extension Closed for Modification หมายถึงอะไร?
สร้าง Working Class ไปแล้ว ไม่ควร Tweak Class นั้นถ้าอยาก Add Features/Changes ควร Extend Class โดยสร้าง Subclass ใหม่ Add Features ได้ง่าย Features ควร Parameterized ให้ Subclass Override ได้
ตัวอย่าง: สร้าง FeedbackManager แสดง Custom Messages ต่างๆ ให้ User
❌ BAD CODE EXAMPLE:
class MainActivity : AppCompatActivity() {
lateinit var feedbackManager: FeedbackManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
feedbackManager = FeedbackManager(findViewById(android.R.id.content));
}
override fun onStart() {
super.onStart()
feedbackManager.showToast(CustomToast())
}
}
class FeedbackManager(var view: View) {
// ต้อง Add Feedback Message Type ใหม่ จะเกิดอะไร?
// ต้อง Modify Manager Class แต่ OCP ต้องเขียน Code Adapt ให้ Requirements ใหม่โดยไม่ Rewrite Old Classes
fun showToast(customToast: CustomToast) {
Toast.makeText(view.context, customToast.welcomeText, customToast.welcomeDuration).show()
}
fun showSnackbar(customSnackbar: CustomSnackbar) {
Snackbar.make(view, customSnackbar.goodbyeText, customSnackbar.goodbyeDuration).show()
}
}
class CustomToast {
var welcomeText: String = "Hello, this is toast message!"
var welcomeDuration: Int = Toast.LENGTH_SHORT
}
class CustomSnackbar {
var goodbyeText: String = "Goodbye, this is snackbar message.."
var goodbyeDuration: Int = Toast.LENGTH_LONG
}
✅ GOOD CODE EXAMPLE:
class MainActivity : AppCompatActivity() {
lateinit var feedbackManager: FeedbackManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
feedbackManager = FeedbackManager(findViewById(android.R.id.content));
}
override fun onStart() {
super.onStart()
feedbackManager.showSpecialMessage(CustomToast())
}
}
class FeedbackManager(var view: View) {
// Add Feedback Message Type ใหม่ เขียน Code Adapt Requirements ใหม่โดยไม่เปลี่ยน Old Class
// Solution: Focus Extend Functionality โดยใช้ Interfaces ตาม OCP
fun showSpecialMessage(message: Message) {
message.showMessage(view)
}
}
interface Message {
fun showMessage(view: View)
}
class CustomToast: Message {
var welcomeText: String = "Hello, this is toast message!"
var welcomeDuration: Int = Toast.LENGTH_SHORT
override fun showMessage(view: View) {
Toast.makeText(view.context, welcomeText, welcomeDuration).show()
}
}
class CustomSnackbar: Message {
var goodbyeText: String = "Goodbye, this is snackbar message.."
var goodbyeDuration: Int = Toast.LENGTH_LONG
override fun showMessage(view: View) {
Snackbar.make(view, goodbyeText, goodbyeDuration).show()
}
}
OCP สรุป Goals ของ 2 Principles ถัดไป ไปต่อกันเลย
Liskov Substitution Principle (LSP)
Objects ใน Program ควร Replaceable ด้วย Instances ของ Subtypes โดยไม่เปลี่ยน Correctness ของ Program
Principle นี้ชื่อจาก Barbara Liskov — Computer Scientist ตัวยง Idea: Objects ควร Replaceable ด้วย Subtype Instances โดยไม่เปลี่ยน Behavior ของ Program
สมมติ App มี MainClass Depend BaseClass Extends SubClass ให้ Follow LSP: MainClass Code และ App ทำงาน Seamless ไม่มีปัญหาเมื่อเปลี่ยน BaseClass Instance เป็น SubClass Instance
เข้าใจง่ายด้วย Example คลาสสิค Square และ Rectangle Inheritance
❌ BAD CODE EXAMPLE:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val rectangleFirst: Rectangle = Rectangle()
rectangleFirst.width = 2
rectangleFirst.height = 3
textViewRectangleFirst.text = rectangleFirst.area().toString()
// Result = 6 ถูกต้อง (2 x 3 = 6)
// LSP: Subclass (Square) ควร Override Parent (Rectangle) อย่างไม่ Break Functionality จาก Consumer Perspective
val rectangleSecond: Rectangle = Square()
// User สมมติว่า Rectangle ตั้ง Width/Height ตามปกติ
rectangleSecond.width = 2
rectangleSecond.height = 3
textViewRectangleSecond.text = rectangleSecond.area().toString()
// Expected = 6 แต่ได้ 9 → OOP Approach Square Extends Rectangle = Wrong
}
}
open class Rectangle {
open var width: Int = 0
open var height: Int = 0
open fun area(): Int { return width * height }
}
class Square : Rectangle() {
override var width: Int
get() = super.width
set(width) { super.width = width; super.height = width }
override var height: Int
get() = super.height
set(height) { super.width = height; super.height = height }
}
✅ GOOD CODE EXAMPLE:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// จัด Rectangle และ Square ให้ดีกว่า ตาม LSP ไม่มี Unexpected Result
val rectangleFirst: Shape = Rectangle(2,3)
val rectangleSecond: Shape = Square(3)
textViewRectangleFirst.text = rectangleFirst.area().toString()
textViewRectangleSecond.text = rectangleSecond.area().toString()
}
}
class Rectangle(var width: Int, var height: Int) : Shape() {
override fun area(): Int { return width * height }
}
class Square(var edge: Int) : Shape() {
override fun area(): Int { return edge * edge }
}
abstract class Shape {
abstract fun area(): Int
}
คิดก่อนเขียน Hierarchy Real-life Objects ไม่ได้ Map ไป OOP Classes ตรงๆ ต้องหา Approach อื่น
Interface Segregation Principle (ISP)
Many Client-specific Interfaces ดีกว่า One General-purpose Interface
ชื่อดูซับซ้อน Principle เองเข้าใจง่าย: Client ไม่ควรถูกบังคับ Depend Methods หรือ Implement Interface ที่ไม่ใช้ Class ควร Design ให้มี Methods/Attributes น้อยที่สุด สร้าง Interface อย่าทำใหญ่เกินไป แยกเป็น Smaller Interfaces Client จะรู้แค่ Methods ที่ Relevant
เข้าใจด้วย Example Butterfly และ Humanoid Robots 🦋🤖
❌ BAD CODE EXAMPLE:
/**
* สร้าง Robot ไม่แน่ใจ Interface ครอบ Functions ทั้งหมด
*/
interface Robot {
fun giveName(newName: String)
fun reset()
fun fly()
fun talk()
}
/** Butterfly Robot Implement Interface */
class ButterflyRobot : Robot {
var name: String = ""
override fun giveName(newName: String) { name = newName }
override fun reset() { TODO("not implemented") }
override fun fly() { TODO("not implemented") }
override fun talk() {
// WRONG!!! Butterfly Robot ไม่ Talk แค่ Fly ทำไมต้อง Implement?
// Violate ISP: Forced to Implement Method ที่ไม่ใช้
TODO("???")
}
}
/** Humanoid Robot Implement Same Interface */
class HumanoidRobot : Robot {
var name: String = ""
override fun giveName(newName: String) { name = newName }
override fun reset() { TODO("not implemented") }
override fun fly() {
// Problem! Humanoid ไม่ได้ Fly
// Violate ISP: Forced to Implement Method ที่ไม่ใช้
TODO("???")
}
override fun talk() { TODO("not implemented") }
}
✅ GOOD CODE EXAMPLE:
/** Generic Interface: Functions Common ทุก Robots */
interface Robot {
fun giveName(newName: String)
fun reset()
}
/** Robots ที่บินได้ Interface แยก */
interface Flyable { fun fly() }
/** Robots ที่พูดได้ Interface แยก */
interface Talkable { fun talk() }
/** Butterfly Robot: Generic + Specific */
class ButterflyRobot : Robot, Flyable {
var name: String = ""
override fun giveName(newName: String) { name = newName }
override fun reset() { TODO("not implemented") }
override fun fly() { TODO("not implemented") }
// ไม่ต้อง Implement talk() แล้ว!
}
/** Humanoid Robot: Generic + Specific */
class HumanoidRobot : Robot, Talkable {
var name: String = ""
override fun giveName(newName: String) { name = newName }
override fun reset() { TODO("not implemented") }
override fun talk() { TODO("not implemented") }
// ไม่ต้อง Implement fly() แล้ว!
}
Dependency Inversion Principle (DIP)
ควร "Depend upon Abstractions, [not] Concretions."
Principle สุดท้าย: High-level Modules ไม่ควร Depend Low-level Modules ทั้งคู่ควร Depend Abstractions Abstractions ไม่ควร Depend Details Details ควร Depend Abstractions
Idea หลัก: ไม่ให้ Direct Dependencies ระหว่าง Modules/Classes พยายามทำให้ Depend Abstractions (Interfaces) แทน
Simple: ใช้ Class ใน Class อีกตัว Class นั้น Depend Injected Class Violates Principle ควร Decouple Classes
❌ BAD CODE EXAMPLE:
class Radiator {
var temperatureCelsius: Int = 0
fun turnOnHeating(newTemperatureCelsius: Int) {
temperatureCelsius = newTemperatureCelsius
// Radiator มี Technical Procedure ของตัวเอง
TODO("not implemented")
}
}
class AirConditioner {
var temperatureFahrenheit: Int = 0
fun turnOnHeating(newTemperatureFahrenheit: Int) {
temperatureFahrenheit = newTemperatureFahrenheit
// Air Conditioner Procedure ต่างจาก Radiator
TODO("not implemented")
}
}
class SmartHome {
var radiator: Radiator = Radiator()
// เปลี่ยน Radiator เป็น Air Conditioner จะเป็นยังไง?
// var airConditioner: AirConditioner = AirConditioner()
// SmartHome Depend Radiator Violates DIP
var recommendedTemperatureCelsius: Int = 20
fun warmUpRoom() {
radiator.turnOnHeating(recommendedTemperatureCelsius)
// Ignore Principle → Mistakes: ส่ง Celsius แต่ Air Conditioner คาดหวัง Fahrenheit
// airConditioner.turnOnHeating(recommendedTemperatureCelsius)
}
}
✅ GOOD CODE EXAMPLE:
// สร้าง Abstraction - Interface ก่อน
interface Heating {
fun turnOnHeating(newTemperatureCelsius: Int)
}
// Class Implement Heating Interface
class Radiator : Heating {
var temperatureCelsius: Int = 0
override fun turnOnHeating(newTemperatureCelsius: Int) {
temperatureCelsius = newTemperatureCelsius
// Radiator Procedure
TODO("not implemented")
}
}
// Class Implement Heating Interface
class AirConditioner : Heating {
var temperatureFahrenheit: Int = 0
override fun turnOnHeating(newTemperatureCelsius: Int) {
temperatureFahrenheit = newTemperatureCelsius * 9/5 + 32
// Air Conditioner Procedure
TODO("not implemented")
}
}
class SmartHome {
var radiator: Heating = Radiator()
// คำตอบ: เปลี่ยนเป็น Air Conditioner ได้เลย Class Depend Interface แทน Injected Class
// var airConditioner: Heating = AirConditioner()
var recommendedTemperatureCelsius: Int = 20
fun warmUpRoom() {
radiator.turnOnHeating(recommendedTemperatureCelsius)
// Depend Common Interface = No More Mistakes
// airConditioner.turnOnHeating(recommendedTemperatureCelsius)
}
}
สรุปสั้นๆ
คิด Principles ทั้ง 5 จะเห็นว่า Complementary กัน Follow SOLID = Benefits: Reusable, Maintainable, Scalable, Testable
ไม่ได้ Follow ครบทุก Principle ได้ตลอด ขึ้นกับ Situation แต่ Developer ควรรู้จัก เพื่อตัดสินใจเมื่อไหร่ควร Apply
Repository
Part 1 เรียนรู้วางแผน ไม่ได้เขียนโค้ดใหม่ Link Part 1 Branch Commit = "Hello World" Initial Code
View Source On GitHub
หวังว่าเข้าใจ SOLID Principles ดี แนะนำ Comment ด้านล่าง
Ačiū! ขอบคุณที่อ่าน! ประกาศบน Blog ส่วนตัว www.baruckis.com วันที่ 23 กุมภาพันธ์ 2018