krsakai / iOSStudy

1 stars 0 forks source link

クラス設計について #38

Open krsakai opened 6 years ago

krsakai commented 6 years ago

クラス設計について

クラスとは

・class で宣言して { } で囲った物 ・class で宣言した者は イニシャライザでインスタンス化してメモリ上に置くことができる ・プロジェクト上でclassで宣言して作るものは、ユーザー(開発者)のカスタムクラスになる ・UIKitやらiOSライブラリ郡のパーツも全てクラスに分けてパーツ化されている

クラスの概念

・classは現実世界の物・事象の概念をプログラムに投影するための設計書になる ・物・事象を型どる物は、属性(propety)と機能(function)で表せるので、それを簡単にまとめられるように作られたプログラミング機能がclass ・この世の全ての物・事象は全てclassで表せるが、この世の全てをクラス設計しても使わない物がほとんどなため、何かの事柄に特化したその世界をその中だけで構成する (レストランボードは飲食店舗の業務をサポートするために、iPad/iPhoneの端末でそれに関する情報や操作を行えるシステムを構築する。その中で利用可能な機能のためにクラス設計がされている Reserve等)

クラス設計

クラス設計例

人に関するクラス設計をやってみる

internal class Hito {
    var name: String?
    var sintyou: Int?
    var taizyuu: Int?
    var seibetu: String?
    var color: Color?
    var kamigata: Kamigata?
    var mayuge: Mayuge?
    var hitomi: Hitomi?
    var hana: Hana?
    var kuti: Kuti?
    var rinkaku: Rinkaku?
    ...etc

    /// Objectの重さ と 自身の体重と身長により飛距離を返却する
    func nageru(object: Object) -> Int {
        return sintyou * taizyuu * object.omosa
    }
}

internal class Kuti {
    var color: Color?
    var width: Int?
    var height: Int?

     /// Objectの面積 と 口の大きさより食べる時間を返却する
    func taberu(object: Object) -> Time {
        return object.menseki / (width * height)
    }

    ...etc
}

internal class Kamigata {
    var color: Color?
    var width: Int?
    var height: Int?
    ..etc
}

internal class Mayuge {
    var color: Color?
    var width: Int?
    var height: Int?
    ...etc
}

internal class Hitomi {
    var color: Color?
    var width: Int?
    var height: Int?
    ...etc
}

internal class Hana {
    var color: Color?
    var width: Int?
    var height: Int?
    ...etc
}

internal class Rinkaku {
    var color: Color?
    var width: Int?
    var height: Int?
    ...etc
}
var nakamura = Hito() ← 本来はこの生成時のイニシャライザで色々値を与えて生成する
nakamura.name = "Nakamura"
nakamura.taizyuu = 100
nakamura.sintyou = 100

var object = Object(omosa: 100)
let hikyori = nakamura.nageru(object: object)

大体こんな感じ。細部まで分ければもっといっぱい(無限に近く。細胞とか)ある + 人が何をするか、何をするために人を存在させているかにより、クラス設計の中身が変わってくる。ただ何をするにも人という大枠概念は変わらず、やる事が変わってもクラスの中身を変更/追加すればいいだけ。これにより概念は変わらず、機能を追加・変更をできるという。なのでシステムの改変や変更が容易にできるようになるメリットがある。またクラス設計があるおかげで、そのカテゴリ(酒井や中村は人。酒井を生成する時にHitoを生成する時に名前を与えればよい)に関する事柄を登場させる毎にプログラム書かなくてよくなるというメリットもある

クラスがないと

let taizyu = 100
let sintyou = 100
let omosa = 100
let hikyori = taizyu * sintyou * omosa

一見シンプルだが、hikyori の仕様を人の性別の種別に比例させて欲しいとなった時、全てのこのhikyori(nageru)機能を使っている箇所で修正が必要になる。クラスを利用していた場合、Hito の性別をnageruメソッドに追加してやるだけで、hikyori 機能で算出される値を変更する事ができる。仕様変更はその世界で考えられる範疇を超えないので、しっかり世界(class設計)を構築しておけば、少量の変更で改変していく事ができるし、プログラマーがどういう世界が構築されています という事を聞いてからコードを見れば大体簡単に改変していく事ができる

ポイント

・クラス設計はある事柄に特化した世界を構築し、人間が捉えやすいようにするための物である ・クラスがあると重複プログラムをさける事ができる ・クラスがあるとシステムの改変などに容易に対応できるようになる

クラス設計について(継承)

クラス設計の悪いパターン

① 一つのクラスに詰めすぎ ② そのクラスに書くべき定義・事柄ではない

一つのクラスに詰め過ぎたり、そのクラスに持たせるべきではない処理を記載すると、クラス肥大化し可読性が悪くなるし、いろんな粒度の属性や機能を持ちすぎてて理解(世界が壊れる)できなくなる 前述のコードはある程度パーツ化されていたが、Hitoクラスに書かなくても良いものが結構ある。Hitoは「哺乳類の動物で存在する物」という派生と考えるとHonyuruiDoubutuObjectを作り、Honyuuruiを継承させたほうがいい。Objectは大きさとかを持っている。DoubutuはObjectの全てを持っていて動く事とかができる。HonyuuruiはDoubutuの全てを持っていて呼吸するとかができる

↑ の事を継承という。いかに現実世界の事柄を整理する事ができるかがクラス設計に必要とさせる事なので、コーディングを始める前にこーいった整理を行うことが必然必要となる。これが実装観点における詳細設計の一つであり、こーゆう整理をやっていると、世界を構築してるのにも関わらずクラスに流し込むデータであるAPIの返却値がおかしいとかに気づき始めて「APIクソだ」というWeb側との争いが生まれている

ポイント

・持たせる属性(property)は最大限パーツ化すると、クラスの見通しがよくなり、人間が理解しやすくなる ・そのクラス固有で持たせなければいけない物以外は、そのクラスに定義しないほうがいい。そのクラスの基底(Doubutuという事実)があれば、基底クラスを定義してそちらに属性、機能を持たせると、本来作成したHitoクラスでやりたかった事柄が明瞭になり、人間が理解しやすくなる

クラス設計について(プロトコル)

前述したnageru機能は必ずしもHito(Doubutu)だけが使える機能ではない事がある。例えばピッチングマシーンとかDoubutuじゃないけど物を投げてる。こういう場合にprotocol(インターフェース)という機能を使う。Hitoとピッチングマシーンにこの投げるprotocolを持たせる事で、Hitoもピッチングマシーンも投げるアクションに関しては同一の機能を持った存在として扱えるようになる 。これにより別々の概念のオブジェクトでも、あるアクションに関しては同一として扱えるようになり、処理をまとめられ、コードが明瞭になる

NageruProtocolを使った場合

let nakamura = Hito()
let sakaiMachine = PitcingMachine()
let object = Object()
let pitchers: NageruProtocol = [nakamura, sakaiMachine]
let kyoriGoukei = pitchers.reduce(0) { $0 + $1.nageru(object) }

NageruProtocolを使わなかった場合

let nakamura = Hito()
let sakaiMachine = PitcingMachine()
let object = Object()
let nakamuraKyori = nakamura.nageru(object)
let sakaiKyori = sakaiMachine.nageru(object)
let kyoriGoukei = nakamuraKyori + sakaiKyori

またnageruというアクションは一律なので、Hitoにも持たせてピッチングマシーンにも処理を書くと二重管理になるし、投げるアクションの仕様変更があった際にHitoの投げるだけ変わってて、ピッチングマシーンの投げるは変わってないみたいな漏れが発生したりする。なので一律のアクション(機能)に関してはprotocol化して実装を共通化してあげるといい

クラス設計について(ジェネリクス)

わかりやすい説明は以下に書いてある https://qiita.com/ktaguchi/items/fc260a0af506f258177d

○ GenericsFunctionについて

class Object {

    var height: Int!
    var width: Int!
    var wide: Int!
    var weight: Int!
}

class Doubutu: Object {

    var sintyou: Int {
        return height
    }

    var taizyuu: Int {
        return weight
    }
}

class Iruka: Doubutu {
    var name: String

    init(name: String) {
        self.name = name
    }
}

class Hito: Doubutu {
    var name: String

    init(name: String) {
        self.name = name
    }
}

/// 身長と体重を2倍にする
func ookikusuru<T: Object>(object: inout T) {
    object.height = object.height * 2
    object.weight = object.weight * 2
}

/// 身長と体重を1/2倍にする
func tiisakusuru<T: Object>(object: inout T) {
    object.height = object.height / 2
    object.weight = object.weight / 2
}

class Test {

    func testA() {
        var nakamura = Hito(name: "nakamura")
        ookikusuru(object: &nakamura)

        var iruka = Iruka(name: "iruka")
        ookikusuru(object: &iruka)
        tiisakusuru(object: &iruka)
    }
}

こんな感じで、イルカと人は種類は違えど、Objectとしての大きさを持っている GenericsFunctionはそういった物を総称(T)として捉えて扱えるメソッドのこと イルカと人は種類(クラス)は違えど、一つの大きくするメソッドで大きさを変更できる 総称として捉えた物に対して操作、またはそれを利用したデータを返却する機能等を共通処理として書ける機能のこと

○ GenericsType Genericsの本流はGenericsTypeの方。ObjectMapperの使い方を知ってればわかるはず デザインパターンでいう、Factoryパターンを作る時に便利 ObjectMapperはMappableに準拠している物を総称として捉えて、そのインスタンスをJSONデータから作り出すためのFactoryの機能を備えてる

class Object: Mappable {

    var height: Int!
    var width: Int!
    var wide: Int!
    var weight: Int!

    convenience required init?(map: Map) {
        self.init()
    }

    func mapping(map: Map) {
        height      <- map["height"]
        width       <- map["width"]
        wide        <- map["wide"]
        weight      <- map["weight"]
    }
}

class Doubutu: Object {

    var sintyou: Int {
        return height
    }

    var taizyuu: Int {
        return weight
    }
}

class Iruka: Doubutu {
    var name: String

    init(name: String) {
        self.name = name
    }

    convenience required init?(map: Map) {
        self.init(name: "")
    }

    override func mapping(map: Map) {
        super.mapping(map: map)
        name  <- map["name"]
    }
}

class Hito: Doubutu {
    var name: String

    init(name: String) {
        self.name = name
    }

    convenience required init?(map: Map) {
        self.init(name: "")
    }

    override func mapping(map: Map) {
        super.mapping(map: map)
        name  <- map["name"]
    }
}

class Test {

    func testA() {
        let nakamura = Mapper<Hito>().map(JSON: ["name": "nakamura", "height": 100, "width": 50, "wide": 50, "weight": 70])
        let iruka = Mapper<Iruka>().map(JSON: ["name": "iruka", "height": 30, "width": 20, "wide": 20, "weight": 30])
    }
}

ポイント

要は、そのクラスやファンクションで利用する引数などを、「あるクラスやそのサブクラス物 ・あるプロトコルに準拠している物」を指定して利用するように縛る仕組み。縛ったことにより、 あるクラスの派生の物 や プロトコルで縛った物 を総じて処理できるようになり、引数のクラス毎に分けた処理等を冗長に書かなくて済むようになり、簡潔な記述になる

デザインパターン

オブジェクト指向の言語で出来うる事を全て知った上で、これらを利用したコード設計の手法で一般的な物が定義されていて(↓)、これらを全て理解し念頭においとけると、オブジェクト指向プログラミングはマスターしたと言ってもいいレベルになる (便利なライブラリはこれらを利用した設計になっている事が多いので、理解していると、使おうと思った時に瞬時に使えるようになる。また自分で複雑 かつ 法則に則った機能を設計するときも然り) http://www.nulab.co.jp/designPatterns/designPatterns1/designPatterns1-2.html

まとめ

○ クラス設計は作成する世界の登場人物の整理(データ・粒度・パターンの整理)から始める

○ 整理した物でまとめられそうなデータや処理があれば、基底クラス、プロトコルを利用し縛ったり、総称型を利用してまとめて処理を行えるような設計にすると重複コードがなくなり、明瞭なコードになる

○ 既に定義されているデザインパターンを覚えておくと、パット見よくわからないライブラリの処理が読めるようになる。し、自分もその設計パターンに当てはめてコーディングしていく事により、その設計さえ理解していれば複雑な処理をまとめて扱えて便利 かつ 簡潔に書ける事で読み手もすぐに入り込める

○ クラス設計があべこべだと、保守が困難になるので、エンハンス開発 や チーム開発 が必須とされるプロジェクトではしっかりと設計思想を入れて実装しておくと幸せになれる

○ デザインパターンを全部に無理くり当て込めようとすると矛盾が生じた時に詰むので、確実に利用できると確信した時(ちゃんと設計できてる時)に利用する。コード上でとにかく多様してるが、設計についての説明が無いと逆に理解できない読みづらいコードになってしまうので、まずはパターンの使い所を色んなコードを見て覚えていくのが近道だと思う

○ MVCとかMVPとかMVVM とかiOS Clean Architecture とか全てデザインパターンのこと。RxはMVVMになるようにするライブラリのこと。コードの一部のデザインパターン と アプリケーション全体の設計のデザインパターンがあるという風に覚えておくと 混乱しない

krsakai commented 6 years ago

かなり箇条書き+長文になった(´・ω・`) とりあえず、そういうもんだという事を認識しておいて、色んなコードを読むことで少しずつ理解していくのがよい