Android
 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、およびコルーチンを使用して RESTful Web サービスを処理する

原則のスローガン

ソリッド ニーモニックの頭字語です。 5 つの基本的なオブジェクト指向設計原則を定義するのに役立ちます:

<オール>
  • S 責任原則
  • おお ペンクローズの原則
  • L iskov置換原則
  • インターフェイス分離の原則
  • D 依存性の逆転の原則
  • 次に、それぞれについて個別に説明します。それぞれについて、悪いコードと良いコードの例を提供します。これらの例は、Kotlin 言語を使用して Android 用に作成されています。

    単一責任の原則

    クラスは 1 つの責任しか持たない

    各クラスまたはモジュールは、アプリによって提供される機能の一部を担当する必要があります。したがって、1 つのことを処理する場合、それを変更する主な理由は 1 つだけである必要があります。クラスまたはモジュールが複数のことを行う場合は、機能を別々のものに分割する必要があります。

    この原則をよりよく理解するために、例としてスイスアーミーナイフを取り上げます。このナイフは、主刃以外にも多機能で知られています。ドライバー、缶切り、その他多くのツールが内部に組み込まれています。

    ここでの自然な質問は、なぜこのナイフを単一の機能の例として提案するのですか?しかし、ちょっと考えてみてください。このナイフのもう1つの主な特徴は、ポケットサイズでありながら機動性です。したがって、いくつかの異なる機能を提供する場合でも、快適に持ち運ぶのに十分小さいという主な目的に適合します.

    同じルールがプログラミングにも当てはまります。クラスまたはモジュールを作成するときは、主なグローバルな目的が必要です。同時に、機能を分離してすべてを簡素化しようとしても、やり過ぎは禁物です。バランスを保つことを忘れないでください。

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

    オープン/クローズの原則は、以下で説明する次の 2 つの原則の目標を要約したものです。それでは、それらに移りましょう。

    リスコフ置換原則

    プログラム内のオブジェクトは、そのプログラムの正確性を変更することなく、サブタイプのインスタンスに置き換え可能であるべきです.

    この原理は、熟練したコンピューター科学者である 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 クラスにマップされるとは限りません。別のアプローチを見つける必要があります。

    インターフェース分離の原則

    多くのクライアント固有のインターフェースは、1 つの汎用インターフェースより優れています。

    名前も難しそうですが、原理自体は分かりやすいです。クライアントがメソッドに依存したり、使用しないインターフェイスを実装したりすることを強制されるべきではないと述べています。クラスは、メソッドと属性が最も少なくなるように設計する必要があります。インターフェイスを作成するときは、大きすぎないようにしてください。代わりに、より小さなインターフェースに分割して、インターフェースのクライアントが関連するメソッドのみを認識できるようにします。

    この原理を理解するために、バタフライ ロボットとヒューマノイド ロボットを使用して、悪いコードと良いコードの例を再度作成しました。 ?

    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」初期コードであるパー​​ト 1 ブランチ コミットへのリンクです。

    GitHub でソースを表示

    SOLID の原則をうまく説明できたと思います。以下にコメントを残してください。

    あちゅ!読んでくれてありがとう!この投稿は、2018 年 2 月 23 日に個人ブログ www.baruckis.com で最初に公開したものです。


    1. Google の Project Fi:通話の未来の紹介

      ワイヤレス ネットワークの市場は過密状態であり、まだ多くの情報を見つけることができません。実際、携帯電話会社の評判は悪く、必ずしも間違った理由があるわけではありません。これは、それほど無制限ではないデータプラン、ますます上昇するデータ料金、およびデータ上限の減少に関連する論争によるものです.データプランに大金を費やすこととは別に、最大のカバレッジを提供するキャリアを把握する必要もあります.すでにたくさんのオプションが利用可能で、リストに追加されています。もう 1 つ、Google の Project Fi があります。 Google の Project Fi とは Project Fi は、

    2. LibreOffice 7.1 レビュー - 不確実性原理

      人は歳を重ねるごとに苦しくなると言います。それは年齢の関数ではなく、経験の関数です。希望は有限であり、人が人生を歩み、何度も何度も失望の果実を味わうにつれて、侵食され、削られます.しかし、希望は最後に死ぬものです。 そのため、ソフトウェアの世界がざわざわと通り過ぎるのを見て、私はただ幸せなユーザーになりたいと思っています。 LibreOffice は、私が Windows を使用せざるを得ない 2 つの重要事項のうちの 1 つをカバーするため、この方程式において大きな役割を果たしています。オフィスとゲームは、他のオペレーティング システムでは実行できません。したがって、新しい LibreOf