Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> ระบบ >> Android

เชี่ยวชาญ SOLID: คู่มือครบวงจรสำหรับ Software Design ที่แข็งแกร่ง

ซีรีส์ 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:

  1. Single Responsibility Principle (SRP)
  2. Open-Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. 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

เชี่ยวชาญ SOLID: คู่มือครบวงจรสำหรับ Software Design ที่แข็งแกร่ง

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

เชี่ยวชาญ SOLID: คู่มือครบวงจรสำหรับ Software Design ที่แข็งแกร่ง

เข้าใจง่ายด้วย 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 🦋🤖

เชี่ยวชาญ SOLID: คู่มือครบวงจรสำหรับ Software Design ที่แข็งแกร่ง

❌ 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