Введение в принципы SOLID

1656672613 vvedenie v princzipy solid

Серия приложений Kriptofolio — Часть 1

Программное обеспечение постоянно меняется. Каждое изменение может оказать негативное влияние на весь проект. Итак, главное — предотвратить ущерб, который может быть нанесен при внедрении всех новых изменений.

С приложением «Kriptofolio» (ранее «My Crypto Coins») я буду создавать много нового кода шаг за шагом, и я хочу начать делать это хорошо. Я хочу, чтобы мой проект был качественным. Сначала нам следует понять основные принципы создания современного программного обеспечения. Они называются принципами SOLID. Такое заманчивое название! ?

Содержимое серии

Лозунг принципов

ТВЕРДЫЙ является мнемоническим акронимом. Это помогает определить пять основных принципов объектно-ориентированного проектирования:

  1. Спринцип ответственности
  2. ОПринцип замкнутого пера
  3. ЛПринцип замещения исков
  4. яПринцип сегрегации интерфейса
  5. ДПринцип инверсии зависимости

Далее мы собираемся обсудить каждый из них в отдельности. Для каждого я собираюсь предоставить плохие и хорошие примеры кода. Эти примеры написаны для Android на языке Kotlin.

Принцип единой ответственности

Класс должен иметь только одну ответственность.

Каждый класс или модуль должен отвечать за одну часть функциональных возможностей программы. Следовательно, если он обрабатывает что-то одно, то должна быть только одна главная причина изменить это. Если класс или модуль выполняет несколько задач, вам следует разделить функциональные возможности на отдельные.

Для лучшего понимания этого принципа я бы взял в пример швейцарский армейский нож. Этот нож хорошо известен своими несколькими функциями, кроме основного лезвия. В нем встроены другие инструменты, такие как отвертки, нож и многие другие.

Вам возникает естественный вопрос: почему я предлагаю этот нож как пример для одной функции? Но подумайте об этом на миг. Еще одна главная особенность этого ножа – мобильность при карманном размере. Так что даже если он предлагает несколько разных функций, он все равно отвечает своей основной цели — быть достаточно малым, чтобы удобно брать его с собой.

Такие же правила действуют и по программированию. Когда вы создаете свой класс или модуль, он должен иметь какую-то главную глобальную цель. В то же время нельзя переигрывать, стараясь слишком упростить, разделив функциональность. Поэтому помните, держите баланс.

1*m1OGuBK5fYZM1Lof6GkH3Q

Классическим примером может быть часто используемый метод 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()
    }
}

Принцип «открыто-закрыто» заключает цели следующих двух принципов, о которых я говорю ниже. Так что переходим к ним.

Принцип подстановки Лескова

Объекты в программе должны заменяться экземплярами их подтипов без изменения корректности программы.

Этот принцип назван в честь Барбары Лисков – опытного информатика. Общая идея этого принципа состоит в том, что объекты должны быть заменены экземплярами своих подтипов без изменения поведения программы.

Скажем, в вашем приложении есть MainClass что зависит от BaseClassкоторый распространяется SubClass. Одним словом, следовать этому принципу, ваш MainClass код и ваша программа вообще должны работать без проблем, когда вы решите изменить ее BaseClass экземпляр к SubClass экземпляр.

1*72oYWBRb9y7HgiE-KkMQGw

Чтобы лучше понять этот принцип, позвольте мне привести вам классический, простой для понимания пример 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
}

Всегда думайте, прежде чем записывать свою иерархию. Как вы видите в этом примере, реальные объекты не всегда отражаются на тех же классах ООП. Необходимо найти другой подход.

Принцип разделения интерфейса

Многие клиентские интерфейсы лучше, чем один интерфейс общего назначения.

Даже заглавие звучит трудно, но сам принцип понять достаточно просто. В нем сказано, что клиент никогда не должен быть вынужден зависеть от методов или реализовывать неиспользуемый интерфейс. Класс должен быть разработан так, чтобы иметь наименьшее количество методов и атрибутов. Создавая интерфейс, старайтесь не делать его слишком большим. Разделите его на меньшие интерфейсы, чтобы клиент интерфейса знал только о релевантных методах.

Чтобы получить представление об этом принципе, я снова создал плохие и хорошие примеры кода с работами-бабочками и гуманоидами. ?

1*bogaw0ohgybpgEchiGSsLw

? ПРИМЕР НЕПРАВИЛЬНОГО КОДА:

/**
 * 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")
    }
}

Принцип инверсии зависимостей

Нужно «в зависимости от абстракций, [not] конкременты».

Последний принцип утверждает, что модули высокого уровня должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Основная идея принципа состоит в отсутствии прямых зависимостей между модулями и классами. Попробуйте сделать их зависимыми от абстракций (например, интерфейсов).

Чтобы упростить еще больше, если вы используете класс внутри другого класса, этот класс будет зависеть от внедренного класса. Это нарушает идею принципа, и вы не должны этого делать. Вы должны попытаться разделить все классы.

? ПРИМЕР НЕПРАВИЛЬНОГО КОДА:

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 даст нам много преимуществ. Они сделают нашу программу многократной, удобной для обслуживания, масштабируемой и тестируемой.

Конечно, не всегда возможно полностью соблюдать все эти принципы, поскольку все зависит от индивидуальных ситуаций при написании кода. Но как разработчику вы должны по крайней мере знать их, чтобы вы могли решить, когда их применять.

Репозиторий

Это первая часть, в которой мы изучаем и планируем наш проект вместо того, чтобы писать новый код. Вот ссылка на фиксацию ветви части 1, которая в основном является исходным кодом проекта Hello world.

Просмотреть исходный код на GitHub

Надеюсь, мне удалось хорошо объяснить принципы SOLID. Не стесняйтесь оставлять комментарии ниже.

Ačiū! Спасибо за чтение! Сначала я опубликовал эту публикацию для личного блога www.baruckis.com 23 февраля 2018 года.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *