Computer >> Máy Tính >  >> Hệ thống >> Android

Giới thiệu về các nguyên tắc SOLID

Loạt ứng dụng Kriptofolio - Phần 1

Phần mềm luôn trong trạng thái thay đổi. Mỗi thay đổi có thể có tác động tiêu cực đến toàn bộ dự án. Vì vậy, điều cần thiết là ngăn ngừa thiệt hại có thể xảy ra trong khi thực hiện tất cả các thay đổi mới.

Với ứng dụng “Kriptofolio” (trước đây là “My Crypto Coins”), tôi sẽ từng bước tạo ra rất nhiều mã mới và tôi muốn bắt đầu làm điều đó một cách tốt đẹp. Tôi muốn dự án của mình có chất lượng vững chắc. Đầu tiên chúng ta cần hiểu các nguyên tắc nền tảng của việc tạo ra phần mềm hiện đại. Chúng được gọi là các nguyên tắc RẮN. Thật là một cái tên hấp dẫn! ?

Nội dung sê-ri

  • Giới thiệu:Lộ trình xây dựng ứng dụng Android hiện đại trong năm 2018–2019
  • Phần 1:Giới thiệu về các nguyên tắc SOLID (bạn đang ở đây)
  • Phần 2:Cách bắt đầu xây dựng ứng dụng Android của bạn:tạo Mockups, giao diện người dùng và bố cục XML
  • Phần 3:Tất cả về Kiến trúc đó:khám phá các mẫu kiến ​​trúc khác nhau và cách sử dụng chúng trong ứng dụng của bạn
  • Phần 4:Cách triển khai Dependency Injection trong ứng dụng của bạn với Dagger 2
  • Phần 5:Xử lý các Dịch vụ Web RESTful bằng cách sử dụng Retrofit, OkHttp, Gson, Glide và Coroutines

Khẩu hiệu nguyên tắc

RẮN là một từ viết tắt dễ nhớ. Nó giúp xác định năm nguyên tắc thiết kế hướng đối tượng cơ bản:

  1. S nguyên tắc trách nhiệm ingle
  2. O Nguyên tắc đóng bút
  3. L Nguyên tắc thay thế iskov
  4. Tôi Nguyên tắc phân tách giao diện
  5. Đ Nguyên tắc nghịch đảo phụ thuộc

Tiếp theo, chúng ta sẽ thảo luận riêng từng người trong số chúng. Đối với mỗi loại, tôi sẽ cung cấp các ví dụ về mã xấu và mã tốt. Những ví dụ này được viết cho Android bằng ngôn ngữ Kotlin.

Nguyên tắc trách nhiệm duy nhất

Một lớp chỉ nên có một trách nhiệm duy nhất.

Mỗi lớp hoặc mô-đun phải chịu trách nhiệm về một phần chức năng được cung cấp bởi ứng dụng. Vì vậy, khi nó xử lý một việc, chỉ nên có một lý do chính để thay đổi nó. Nếu lớp hoặc mô-đun của bạn thực hiện nhiều hơn một việc, thì bạn nên chia các chức năng thành các chức năng riêng biệt.

Để hiểu rõ hơn về nguyên tắc này, tôi sẽ lấy một con dao của Quân đội Thụy Sĩ làm ví dụ. Con dao này nổi tiếng với nhiều chức năng bên cạnh lưỡi dao chính của nó. Nó có các công cụ khác được tích hợp bên trong, chẳng hạn như tua vít, dụng cụ mở hộp và nhiều công cụ khác.

Câu hỏi tự nhiên ở đây dành cho bạn là tại sao tôi lại đề xuất con dao này làm ví dụ cho chức năng đơn lẻ? Nhưng chỉ cần nghĩ về nó một lúc. Các tính năng chính khác của con dao này là tính di động trong khi có kích thước bỏ túi. Vì vậy, ngay cả khi nó cung cấp một số chức năng khác nhau, nó vẫn phù hợp với mục đích chính của nó là đủ nhỏ để mang theo bên mình một cách thoải mái.

Các quy tắc tương tự cũng đi với lập trình. Khi bạn tạo lớp hoặc mô-đun của mình, nó phải có một số mục đích chung chính. Đồng thời, bạn không thể làm quá khi cố gắng đơn giản hóa mọi thứ quá nhiều bằng cách tách rời chức năng. Vì vậy, hãy nhớ, giữ cân bằng.

Giới thiệu về các nguyên tắc SOLID

Một ví dụ cổ điển có thể là phương pháp thường được sử dụng onBindViewHolder khi xây dựng bộ điều hợp tiện ích con RecyclerView.

? VÍ DỤ VỀ MÃ XẤU:

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

? VÍ DỤ VỀ MÃ TỐT:

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

Quy tắc được thiết kế đặc biệt với Nguyên tắc trách nhiệm duy nhất sẽ gần với các nguyên tắc khác mà chúng ta sẽ thảo luận.

Nguyên tắc Đóng mở

Các thực thể phần mềm phải được mở để mở rộng, nhưng bị đóng để sửa đổi.

Nguyên tắc này nói rằng khi bạn viết tất cả các phần phần mềm như lớp, mô-đun và chức năng, bạn nên mở chúng để mở rộng nhưng đóng lại cho bất kỳ sửa đổi nào. Điều đó có nghĩa là gì?

Giả sử chúng tôi tạo ra một tầng lớp lao động. Không cần phải tinh chỉnh lớp đó nếu chúng ta cần thêm chức năng mới hoặc thực hiện một số thay đổi. Thay vào đó, chúng ta có thể mở rộng lớp đó bằng cách tạo lớp con mới của nó, nơi chúng ta có thể dễ dàng thêm tất cả các tính năng cần thiết mới. Các tính năng phải luôn được tham số hóa theo cách mà một lớp con có thể ghi đè.

Hãy xem một ví dụ mà chúng ta sẽ tạo một FeedbackManager đặc biệt để hiển thị một loại thông báo tùy chỉnh khác cho người dùng.

? VÍ DỤ VỀ MÃ XẤU:

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
}

? VÍ DỤ VỀ MÃ TỐT:

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

Nguyên tắc đóng mở tóm tắt mục tiêu của hai nguyên tắc tiếp theo mà tôi nói đến dưới đây. Vì vậy, hãy chuyển sang chúng.

Nguyên tắc thay thế Liskov

Các đối tượng trong chương trình phải có thể thay thế được bằng các phiên bản thuộc kiểu con của chúng mà không làm thay đổi tính đúng đắn của chương trình đó.

Nguyên tắc này được đặt theo tên của Barbara Liskov - một nhà khoa học máy tính tài ba. Ý tưởng chung của nguyên tắc này là các đối tượng có thể được thay thế bởi các thể hiện của kiểu con của chúng mà không làm thay đổi hành vi của chương trình.

Giả sử trong ứng dụng của bạn, bạn có MainClass phụ thuộc vào BaseClass , mở rộng SubClass . Tóm lại, để tuân theo nguyên tắc này, MainClass của bạn mã và ứng dụng của bạn nói chung sẽ hoạt động trơn tru mà không gặp bất kỳ sự cố nào khi bạn quyết định thay đổi BaseClass ví dụ cho SubClass ví dụ.

Giới thiệu về các nguyên tắc SOLID

Để hiểu rõ hơn về nguyên tắc này, hãy để tôi cung cấp cho bạn một ví dụ cổ điển, dễ hiểu với SquareRectangle kế thừa.

? VÍ DỤ VỀ MÃ XẤU:

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

? VÍ DỤ VỀ MÃ TỐT:

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
}

Luôn suy nghĩ trước khi viết ra hệ thống phân cấp của bạn. Như bạn thấy trong ví dụ này, các đối tượng trong đời thực không phải lúc nào cũng ánh xạ đến các lớp OOP giống nhau. Bạn cần tìm một cách tiếp cận khác.

Nguyên tắc Phân tách Giao diện

Nhiều giao diện dành riêng cho máy khách tốt hơn một giao diện dành cho mục đích chung.

Ngay cả cái tên nghe có vẻ phức tạp, nhưng bản thân nguyên tắc này khá dễ hiểu. Nó nói rằng khách hàng không bao giờ được buộc phải phụ thuộc vào các phương pháp hoặc triển khai một giao diện mà nó không sử dụng. Một lớp cần được thiết kế để có ít phương thức và thuộc tính nhất. Khi tạo một giao diện cố gắng không làm cho nó quá lớn. Thay vào đó, hãy chia nó thành các giao diện nhỏ hơn để khách hàng của giao diện sẽ chỉ biết về các phương pháp có liên quan.

Để có được ý tưởng về nguyên tắc này, tôi đã tạo lại các ví dụ mã xấu và tốt với các robot hình người và bướm. ?

Giới thiệu về các nguyên tắc SOLID

? VÍ DỤ VỀ MÃ XẤU:

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

? VÍ DỤ VỀ MÃ TỐT:

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

Nguyên tắc Đảo ngược Phụ thuộc

Một người nên “phụ thuộc vào những điều trừu tượng, [chứ không phải] cụ thể hóa.”

Nguyên tắc cuối cùng nói rằng các mô-đun cấp cao không nên phụ thuộc vào các mô-đun cấp thấp. Cả hai đều nên phụ thuộc vào sự trừu tượng. Sự trừu tượng không nên phụ thuộc vào chi tiết. Thông tin chi tiết phải phụ thuộc vào sự tóm tắt.

Ý tưởng chính của nguyên tắc là không có sự phụ thuộc trực tiếp giữa các mô-đun và các lớp. Thay vào đó, hãy cố gắng làm cho chúng phụ thuộc vào các yếu tố trừu tượng (ví dụ:giao diện).

Để đơn giản hóa nó hơn nữa, nếu bạn sử dụng một lớp bên trong một lớp khác, lớp này sẽ phụ thuộc vào lớp được đưa vào. Điều này vi phạm ý tưởng của nguyên tắc và bạn không nên làm điều đó. Bạn nên cố gắng tách tất cả các lớp.

? VÍ DỤ VỀ MÃ XẤU:

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

? VÍ DỤ VỀ MÃ TỐT:

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

Tóm tắt ngắn gọn

Nếu chúng ta nghĩ về tất cả những nguyên tắc này, chúng ta có thể nhận thấy rằng chúng bổ sung cho nhau. Tuân theo các nguyên tắc SOLID sẽ mang lại cho chúng ta nhiều lợi ích. Họ sẽ làm cho ứng dụng của chúng tôi có thể tái sử dụng, có thể bảo trì, có thể mở rộng và có thể kiểm tra được.

Tất nhiên, không phải lúc nào bạn cũng có thể tuân theo tất cả các nguyên tắc này một cách hoàn toàn, vì mọi thứ phụ thuộc vào các tình huống cá nhân khi viết mã. Nhưng với tư cách là một nhà phát triển, ít nhất bạn nên biết chúng để bạn có thể quyết định khi nào áp dụng chúng.

Kho lưu trữ

Đây là phần đầu tiên chúng ta tìm hiểu và lập kế hoạch cho dự án của mình thay vì viết mã mới. Đây là liên kết đến cam kết nhánh Phần 1, về cơ bản là mã ban đầu “Hello world” của dự án.

Xem nguồn trên GitHub

Tôi hy vọng tôi đã giải thích tốt các nguyên tắc SOLID. Vui lòng để lại bình luận bên dưới.

Ačiū! Cảm ơn vì đã đọc! Ban đầu tôi đã xuất bản bài đăng này cho blog cá nhân của mình www.baruckis.com vào ngày 23 tháng 2 năm 2018.