Computer >> 컴퓨터 >  >> 체계 >> Android

SOLID 원칙 소개

Kriptofolio 앱 시리즈 - 파트 1

소프트웨어는 항상 변화하는 상태에 있습니다. 각 변경 사항은 전체 프로젝트에 부정적인 영향을 미칠 수 있습니다. 따라서 중요한 것은 모든 새로운 변경 사항을 구현하는 동안 발생할 수 있는 피해를 방지하는 것입니다.

"Kriptofolio"(이전의 "My Crypto Coins") 앱을 사용하여 단계적으로 많은 새로운 코드를 생성할 것이며 좋은 방식으로 시작하고 싶습니다. 나는 내 프로젝트가 견고한 품질을 원합니다. 먼저 현대 소프트웨어를 만드는 기본 원칙을 이해해야 합니다. 그것들을 SOLID 원칙이라고 합니다. 너무 탐나는 이름! ?

시리즈 콘텐츠

  • 소개:2018–2019년 최신 Android 앱을 구축하기 위한 로드맵
  • 1부:SOLID 원칙 소개(현재 위치)
  • 2부:Android 앱 빌드 시작 방법:목업, UI 및 XML 레이아웃 만들기
  • 3부:아키텍처에 관한 모든 것:다양한 아키텍처 패턴 탐색 및 앱에서 이를 사용하는 방법
  • 4부:Dagger 2를 사용하여 앱에서 종속성 주입을 구현하는 방법
  • 5부:Retrofit, OkHttp, Gson, Glide 및 Coroutine을 사용하여 RESTful 웹 서비스 처리

원칙 슬로건

단단함 니모닉 약어입니다. 5가지 기본 객체 지향 설계 원칙을 정의하는 데 도움이 됩니다.

  1. S 단일 책임 원칙
  2. 폐쇄형 원칙
  3. iskov 치환 원리
  4. 인터페이스 분리 원칙
  5. ependency 반전 원리

다음으로 우리는 그들 각각에 대해 개별적으로 논의할 것입니다. 각각에 대해 나쁜 코드와 좋은 코드 예제를 제공할 것입니다. 이 예제는 Kotlin 언어를 사용하여 Android용으로 작성되었습니다.

단일 책임 원칙

클래스는 단일 책임만 가져야 합니다.

각 클래스 또는 모듈은 앱에서 제공하는 기능의 한 부분을 담당해야 합니다. 따라서 한 가지를 처리할 때 변경해야 하는 주된 이유는 한 가지뿐입니다. 클래스나 모듈이 둘 이상의 작업을 수행하는 경우 기능을 별도의 기능으로 분할해야 합니다.

이 원리를 더 잘 이해하기 위해 스위스 군용 칼을 예로 들어보겠습니다. 이 칼은 메인 칼날 외에도 다양한 기능으로 잘 알려져 있습니다. 드라이버, 깡통따개 등과 같은 기타 도구가 내부에 통합되어 있습니다.

여기에서 자연스러운 질문은 내가 이 칼을 단일 기능의 예로 제안하는 이유입니다. 그러나 한 번만 생각해 보십시오. 이 나이프의 또 다른 주요 특징은 포켓 사이즈이면서 이동성입니다. 따라서 몇 가지 다른 기능을 제공하더라도 편안하게 휴대할 수 있을 만큼 충분히 작아야 한다는 주된 목적에 부합합니다.

프로그래밍에도 동일한 규칙이 적용됩니다. 클래스나 모듈을 만들 때 주요 전역 목적이 있어야 합니다. 동시에 기능을 분리하여 모든 것을 너무 단순화하려고 할 때 과장할 수 없습니다. 따라서 균형을 유지하십시오.

SOLID 원칙 소개

고전적인 예는 자주 사용되는 방법 onBindViewHolder일 수 있습니다. RecyclerView 위젯 어댑터를 빌드할 때.

? 잘못된 코드 예:

class MusicVinylRecordRecyclerViewAdapter(private val vinyls: List<VinylRecord>, private val itemLayout: Int) 
 : RecyclerView.Adapter<MusicVinylRecordRecyclerViewAdapter.ViewHolder>() {
    ...
    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

        /**
         *  Here method violates the Single Responsibility Principle!!!
         *  Despite its main and only responsibility to be adapting a VinylRecord object
         *  to its view representation, it is also performing data formatting as well.
         *  It has multiple reasons to be changed in the future, which is wrong.
         */

        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
    }
    ...
}

? 좋은 코드 예:

class MusicVinylRecordRecyclerViewAdapter(private val vinyls: List<VinylRecord>, private val itemLayout: Int) 
 : RecyclerView.Adapter<MusicVinylRecordRecyclerViewAdapter.ViewHolder>() {
    ...
    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
        
        /**
         * Instead of performing data formatting operations here, we move that responsibility to
         * other class. Actually here you see only direct call of top-level function
         * convertArrayListToString - new Kotlin language feature. However don't be mistaken,
         * because Kotlin compiler behind the scenes still is going to create a Java class, and
         * than the individual top-level functions will be converted to static methods. So single
         * responsibility for each class.
         */

        holder.genre!!.text =  convertArrayListToString(vinyl.genres)
    }
    ...
}

단일 책임 원칙을 염두에 두고 특별히 설계된 코드는 우리가 논의할 다른 원칙에 가깝습니다.

개방형 원칙

소프트웨어 엔티티는 확장을 위해 열려야 하지만 수정을 위해 닫혀 있어야 합니다.

이 원칙은 클래스, 모듈 및 함수와 같은 모든 소프트웨어 부분을 작성할 때 확장을 위해 열어야 하고 수정을 위해 닫아야 한다는 것입니다. 그게 무슨 뜻인가요?

노동계급을 만든다고 합시다. 새로운 기능을 추가하거나 일부 변경을 수행해야 하는 경우 해당 클래스를 조정할 필요가 없습니다. 대신에 필요한 모든 새 기능을 쉽게 추가할 수 있는 새 하위 클래스를 만들어 해당 클래스를 확장할 수 있어야 합니다. 기능은 항상 하위 클래스가 재정의할 수 있는 방식으로 매개변수화되어야 합니다.

특별한 FeedbackManager을 생성하는 예를 살펴보겠습니다. 클래스를 사용하여 사용자에게 다른 유형의 맞춤 메시지를 표시합니다.

? 잘못된 코드 예:

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) {

    // Imagine that we need to add new type feedback message. What would happen?
    // We would need to modify this manager class. But to follow Open Closed Principle we
    // need to write a code that can be adapted automatically to the new requirements without
    // rewriting the 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
}

? 좋은 코드 예:

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) {

    // Again the same situation - we need to add new type feedback message. We have to write code
    // that can be adapted to new requirements without changing the old class implementation.
    // Here the solution is to focus on extending the functionality by using interfaces and it
    // follows the Open Closed Principle.

    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()
    }
}

개방형 원칙은 내가 아래에서 이야기하는 다음 두 원칙의 목표를 요약한 것입니다. 다음으로 넘어가겠습니다.

리스코프 대체 원칙

프로그램의 개체는 해당 프로그램의 정확성을 변경하지 않고 하위 유형의 인스턴스로 교체할 수 있어야 합니다.

이 원리는 뛰어난 컴퓨터 과학자인 Barbara Liskov의 이름을 따서 명명되었습니다. 이 원칙의 일반적인 개념은 프로그램의 동작을 변경하지 않고 개체를 하위 유형의 인스턴스로 대체할 수 있어야 한다는 것입니다.

앱에 MainClass이 있다고 가정해 보겠습니다. BaseClass에 따라 다릅니다. , SubClass 확장 . 간단히 말해서 이 원칙을 따르려면 MainClass BaseClass을 변경하기로 결정할 때 코드와 앱이 일반적으로 문제 없이 원활하게 작동해야 합니다. SubClass 인스턴스 인스턴스.

SOLID 원칙 소개

이 원칙을 더 잘 이해하기 위해 Square를 사용하여 이해하기 쉬운 고전적인 예를 보여 드리겠습니다. 및 Rectangle 상속.

? 잘못된 코드 예:

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()
        // The result of the first rectangle area is 6, which is correct as 2 x 3 = 6.

        // The Liskov Substitution Principle states that a subclass (Square) should override
        // the parent class (Rectangle) in a way that does not break functionality from a
        // consumers’s point of view. Let's see.
        val rectangleSecond: Rectangle = Square()
        // The user assumes that it is a rectangle and try to set the width and the height as usual
        rectangleSecond.width = 2
        rectangleSecond.height = 3

        textViewRectangleSecond.text = rectangleSecond.area().toString()
        // The expected result of the second rectangle should be 6 again, but instead it is 9.
        // So as you see this object oriented approach for Square extending Rectangle is 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
        }
}

? 좋은 코드 예:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Here it is presented a way how to organize these Rectangle and Square classes better to
        // meet the Liskov Substitution Principle. No more 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
}

계층 구조를 작성하기 전에 항상 생각하십시오. 이 예제에서 볼 수 있듯이 실제 개체가 항상 동일한 OOP 클래스에 매핑되는 것은 아닙니다. 다른 접근 방식을 찾아야 합니다.

인터페이스 분리 원칙

많은 클라이언트 전용 인터페이스가 하나의 범용 인터페이스보다 낫습니다.

이름만 들어도 복잡해 보이지만 원리 자체는 이해하기 쉽습니다. 클라이언트가 사용하지 않는 인터페이스를 구현하거나 메서드에 의존하도록 강요해서는 안 된다고 명시되어 있습니다. 클래스는 최소한의 메서드와 속성을 갖도록 설계해야 합니다. 인터페이스를 만들 때 너무 크게 만들지 마십시오. 대신 인터페이스의 클라이언트가 관련 메서드에 대해서만 알 수 있도록 더 작은 인터페이스로 분할합니다.

이 원칙에 대한 아이디어를 얻기 위해 나비와 휴머노이드 로봇을 사용하여 나쁜 코드와 좋은 코드 예제를 다시 만들었습니다. ?

SOLID 원칙 소개

? 잘못된 코드 예:

/**
 * Let's imagine we are creating some undefined robot. We decide to create an interface with all
 * possible functions to it.
 */
interface Robot {
    fun giveName(newName: String)
    fun reset()
    fun fly()
    fun talk()
}

/**
 * First we are creating butterfly robot which implements that interface.
 */
class ButterflyRobot : Robot {
    var name: String = ""

    override fun giveName(newName: String) {
        name = newName
    }

    override fun reset() {
        // Calls reset command for the robot. Any robot's software should be possible to reset.
        // That is reasonable and we will implement this.
        TODO("not implemented")
    }

    override fun fly() {
        // Calls fly command for the robot. This is specific functionality of our butterfly robot.
        // We will definitely implement this.
        TODO("not implemented")
    }

    override fun talk() {
        // Calls talk command for the robot.
        // WRONG!!! Our butterfly robot is not going to talk, just fly! Why we need implement this?
        // Here it is a violation of Interface Segregation Principle as we are forced to implement
        // a method that we are not going to use.
        TODO("???")
    }
}

/**
 * Next we are creating humanoid robot which should be able to do similar actions as human and it
 * also implements same interface.
 */
class HumanoidRobot : Robot {
    var name: String = ""

    override fun giveName(newName: String) {
        name = newName
    }

    override fun reset() {
        // Calls reset command for the robot. Any robot's software should be possible to reset.
        // That is reasonable and we will implement this.
        TODO("not implemented")
    }

    override fun fly() {
        // Calls fly command for the robot.
        // That the problem! We have never had any intentions for our humanoid robot to fly.
        // Here it is a violation of Interface Segregation Principle as we are forced to implement
        // a method that we are not going to use.
        TODO("???")
    }

    override fun talk() {
        // Calls talk command for the robot. This is specific functionality of our humanoid robot.
        // We will definitely implement this.
        TODO("not implemented")
    }
}

? 좋은 코드 예:

/**
 * Let's imagine we are creating some undefined robot. We should create a generic interface with all
 * possible functions common to all types of robots.
 */
interface Robot {
    fun giveName(newName: String)
    fun reset()
}

/**
 * Specific robots which can fly should have their own interface defined.
 */
interface Flyable {
    fun fly()
}

/**
 * Specific robots which can talk should have their own interface defined.
 */
interface Talkable {
    fun talk()
}

/**
 * First we are creating butterfly robot which implements a generic interface and a specific one.
 * As you see we are not required anymore to implement functions which are not related to our robot!
 */
class ButterflyRobot : Robot, Flyable {
    var name: String = ""

    override fun giveName(newName: String) {
        name = newName
    }

    override fun reset() {
        // Calls reset command for the robot. Any robot's software should be possible to reset.
        // That is reasonable and we will implement this.
        TODO("not implemented")
    }

    // Calls fly command for the robot. This is specific functionality of our butterfly robot.
    // We will definitely implement this.
    override fun fly() {
        TODO("not implemented")
    }
}

/**
 * Next we are creating humanoid robot which should be able to do similar actions as human and it
 * also implements generic interface and specific one for it's type.
 * As you see we are not required anymore to implement functions which are not related to our robot!
 */
class HumanoidRobot : Robot, Talkable {
    var name: String = ""

    override fun giveName(newName: String) {
        name = newName
    }

    override fun reset() {
        // Calls reset command for the robot. Any robot's software should be possible to reset.
        // That is reasonable and we will implement this.
        TODO("not implemented")
    }

    override fun talk() {
        // Calls talk command for the robot. This is specific functionality of our humanoid robot.
        // We will definitely implement this.
        TODO("not implemented")
    }
}

종속성 반전 원칙

“구체화가 아니라 추상화에 의존”해야 합니다.

마지막 원칙은 고수준 모듈이 저수준 모듈에 의존해서는 안 된다는 것입니다. 둘 다 추상화에 의존해야 합니다. 추상화는 세부 사항에 의존해서는 안됩니다. 세부 사항은 추상화에 따라 달라야 합니다.

원칙의 주요 아이디어는 모듈과 클래스 간에 직접적인 종속성을 갖지 않는 것입니다. 대신 추상화(예:인터페이스)에 종속되도록 하십시오.

더 단순화하기 위해 다른 클래스 내부에서 클래스를 사용하는 경우 이 클래스는 주입된 클래스에 종속됩니다. 이는 원칙에 위배되는 행위이므로 해서는 안 됩니다. 모든 클래스를 분리해야 합니다.

? 잘못된 코드 예:

class Radiator {
    var temperatureCelsius : Int = 0

    fun turnOnHeating(newTemperatureCelsius : Int) {
        temperatureCelsius  = newTemperatureCelsius
        // To turn on heating for the radiator we will have to do specific steps for this device.
        // Radiator will have it's own technical procedure of how it will be turned on.
        // Procedure implemented here.
        TODO("not implemented")
    }
}

class AirConditioner {
    var temperatureFahrenheit: Int = 0

    fun turnOnHeating(newTemperatureFahrenheit: Int) {
        temperatureFahrenheit = newTemperatureFahrenheit
        // To turn on heating for air conditioner we will have to do some specific steps
        // just for this device, as air conditioner will have it's own technical procedure.
        // This procedure is different compared to radiator and will be implemented here.
        TODO("not implemented")
    }
}

class SmartHome {

    // To our smart home control system we added a radiator control.
    var radiator: Radiator = Radiator()
    // But what will be if later we decide to change our radiator to air conditioner instead?
    // var airConditioner: AirConditioner = AirConditioner()
    // This SmartHome class is dependent of the class Radiator and violates Dependency Inversion Principle.

    var recommendedTemperatureCelsius : Int = 20

    fun warmUpRoom() {
        radiator.turnOnHeating(recommendedTemperatureCelsius)
        // If we decide to ignore the principle there may occur some important mistakes, like this
        // one. Here we pass recommended temperature in celsius but our air conditioner expects to
        // get it in Fahrenheit.
        // airConditioner.turnOnHeating(recommendedTemperatureCelsius)
    }
}

? 좋은 코드 예:

// First let's create an abstraction - interface.
interface Heating {
    fun turnOnHeating(newTemperatureCelsius : Int)
}

// Class should implement the Heating interface.
class Radiator : Heating {
    var temperatureCelsius : Int = 0

    override fun turnOnHeating(newTemperatureCelsius: Int) {
        temperatureCelsius  = newTemperatureCelsius
        // Here radiator will have it's own technical procedure implemented of how it will be turned on.
        TODO("not implemented")
    }
}

// Class should implement the Heating interface.
class AirConditioner : Heating {
    var temperatureFahrenheit: Int = 0

    override fun turnOnHeating(newTemperatureCelsius: Int) {
        temperatureFahrenheit = newTemperatureCelsius * 9/5 + 32
        // Air conditioner's turning on technical procedure will be implemented here.
        TODO("not implemented")
    }
}

class SmartHome {

    // To our smart home control system we added a radiator control.
    var radiator: Heating = Radiator()
    // Now we have an answer to the question what will be if later we decide to change our radiator
    // to air conditioner. Our class is going to depend on the interface instead of another
    // injected class.
    // var airConditioner: Heating = AirConditioner()

    var recommendedTemperatureCelsius : Int = 20

    fun warmUpRoom() {
        radiator.turnOnHeating(recommendedTemperatureCelsius)
        // As we depend on the common interface, there is no more chance for mistakes.
        // airConditioner.turnOnHeating(recommendedTemperatureCelsius)
    }
}

간단히 요약하자면

이 모든 원칙을 생각해 보면 서로 보완적임을 알 수 있습니다. SOLID 원칙을 따르면 많은 이점을 얻을 수 있습니다. 그들은 우리 앱을 재사용 가능하고 유지보수 가능하며 확장 가능하고 테스트 가능하게 만들 것입니다.

물론 코드를 작성할 때 모든 것이 개별 상황에 따라 달라지므로 이러한 원칙을 모두 완벽하게 따르는 것이 항상 가능한 것은 아닙니다. 그러나 개발자로서 최소한 그것들을 알고 있어야 언제 적용할지 결정할 수 있습니다.

저장소

이것은 새로운 코드를 작성하는 대신 프로젝트를 배우고 계획하는 첫 번째 부분입니다. 기본적으로 프로젝트의 "Hello world" 초기 코드인 Part 1 분기 커밋에 대한 링크입니다.

GitHub에서 소스 보기

SOLID 원칙을 잘 설명할 수 있기를 바랍니다. 아래에 자유롭게 의견을 남겨주세요.

아츄! 읽어 주셔서 감사합니다! 저는 원래 2018년 2월 23일에 제 개인 블로그 www.baruckis.com에 이 게시물을 게시했습니다.