Upgrade to Pro — share decks privately, control downloads, hide ads and more …

SOLID原則のSとDとテストの話 - 「Swiftらしく設計する」Another / 20181221 #roppongiswift

takasek
December 21, 2018

SOLID原則のSとDとテストの話 - 「Swiftらしく設計する」Another / 20181221 #roppongiswift

ROPPONGI.swift 第6回 望年会 - connpass
https://visits.connpass.com/event/111355/
での発表資料です。

一般発売目前となった「iOSアプリ設計パターン入門」
https://peaks.cc/iOS_architecture
の第3章「Swiftらしく設計する」
で説明したSOLID原則のS(単一責任原則)とD(依存関係逆転の原則)について、
ちょっと角度を変えてテストの側面から捉えてみます。

takasek

December 21, 2018
Tweet

More Decks by takasek

Other Decks in Programming

Transcript

  1. SOLIDݪଇͷSͱDͱςετͷ࿩
    ʮSwiftΒ͘͠ઃܭ͢ΔʯAnother
    by.
    2018/12/21 Roppongi.swift
    1

    View full-size slide

  2. takasek
    @takasek
    Works
    OSS: ActionClosurable౳
    App: PasteTheType
    Articles
    ʮίϯύΠϧΤϥʔ΍ϥϯλΠϜΤϥʔΛ௚͍ͯ͘͠
    ͚ͩͰiOSΞϓϦͷ࡞Γํ͕Θ͔ΔϓϩδΣΫτʯ
    ʮ͓લΒ͕ModelͱݺͿΞϨΛͳΜͱݺͿ΂͖͔ʯ
    ʮiOSΞϓϦઃܭύλʔϯೖ໳ʯʢڞஶʣ
    2

    View full-size slide

  3. ʮiOSΞϓϦઃܭύλʔϯೖ໳ʯ
    ઌߦൃച഑ૹதʂ
    Ұൠൃചؒۙʂ
    takasek୲౰ষ
    ୈ1ষʮઃܭ͢Δͱ͍͏͜ͱʯ
    ୈ2ষʮઃܭʹύλʔϯΛద༻͢Δલʹʯ
    ୈ3ষʮSwiftΒ͘͠ઃܭ͢Δʯ
    ୈ4ষʮΞʔΩςΫνϟͷύλʔϯΛௗᛌ͢
    Δʯ
    3

    View full-size slide

  4. App Client
    Melting Pod #1
    1/10ʹ΍Γ·͢
    4

    View full-size slide

  5. ୈ3ষʮSwiftΒ͘͠ઃܭ͢Δʯ
    SOLIDݪଇ΍ɺProtocol Oriented Programming,
    Swiftͷݴޠػೳʢenum, generics, etcʣΛ࢖ͬͯ
    ίʔυΛʮΑ͍ઃܭʯ΁ͱվળ͍ͯ͘͠ষ
    5

    View full-size slide

  6. Α͍ઃܭ #ͱ͸
    6

    View full-size slide

  7. ୈ2ষʮઃܭʹύλʔϯΛద༻͢Δલʹʯ
    ཁ໿
    • ໊લʹΑͬͯ੹຿ͷڥքΛ໌ࣔ͢Δ
    • ෆద੾ͳ੹຿ʢίʔυͷष͍ʣ͸ɺ
    ɹઃܭͷݪଇʹΑͬͯݴޠԽͰ͖Δ
    • ඞཁʹԠͯ͡ઃܭͷݪଇɺύλʔϯΛద༻͢Δ͕ɺ΍Γ͗͢͸ې෺
    • ઃܭ͸୯७ʹ࢝ΊɺϦϑΝΫλϦϯάΛਐΊͳ͕Β
    ɹΠςϨʔςΟϒʹਐԽͤ͞Δ
    • ϦϑΝΫλϦϯάͷͨΊʹ͸ɺςετ͠΍͍͢ઃܭ͕લఏ
    7

    View full-size slide

  8. 3ষͷվળର৅ίʔυʢൈਮʣ
    final class MessageSender {
    private let api = CommonMessageAPI()
    init(messageType: MessageType) { ... }
    private var isTextValid: Bool { ... }
    private var isImageValid: Bool { ... }
    var isValid: Bool {
    switch messageType {
    case .text: return isTextValid
    case .image: return isTextValid && isImageValid
    case .official: return false // OfficialMessage͸͋Γ͑ͳ͍
    }
    }
    ...
    8

    View full-size slide

  9. MessageSender ͷ
    ໰୊఺
    όϦσʔγϣϯϩδοΫ͕
    MessageSender ͷதͰ
    ๲ΒΈ͍͗ͯ͢Δ
    9

    View full-size slide

  10. όϦσʔγϣϯϩδοΫ
    ͕๲ΒΈ͍͗ͯ͢Δͱ
    Կ͕ਏ͍͔
    10

    View full-size slide

  11. MessageSender ͷ
    ςετΛ
    ߟ͑ͯΈ·͠ΐ͏
    11

    View full-size slide

  12. let sut = MessageSender(messageType: .image)
    let cases: [(line: UInt, text: String?, image: UIImage?, expects: Bool)] = [
    (line: #line, text: nil, image: UIImage(), expects: true),
    // text͕͋Δ৔߹ɺ80จࣈҎ಺Ͱͳ͍ͱinvalid
    (line: #line, text: String(repeating: "x", count: 80), image: UIImage(), expects: true),
    (line: #line, text: String(repeating: "x", count: 81), image: UIImage(), expects: false),
    // image͕ͳ͚Ε͹͍͔ͳΔ৔߹΋invalid
    (line: #line, text: nil, image: UIImage(), expects: false),
    (line: #line, text: String(repeating: "x", count: 80), image: UIImage(), expects: false),
    (line: #line, text: String(repeating: "x", count: 81), image: UIImage(), expects: false),
    ]
    cases.forEach {
    sut.text = $0.text
    sut.image = $0.image
    XCTAssertEqual(sut.isValid, $0.expects, line: $0.line)
    }
    12

    View full-size slide

  13. ໰୊ͳ͍
    13

    View full-size slide

  14. ຊ౰ʹʁ
    14

    View full-size slide

  15. MessageSender ͷ
    ผͷ໰୊఺
    API͕۩ମతͳ௨৴ϩδοΫͱ
    ີ݁߹͍ͯ͠Δ
    15

    View full-size slide

  16. ղܾࡦɿ ґଘؔ܎ٯసͷݪଇ

    final class MessageSender {
    private let api = CommonMessageAPI()
    init(messageType: MessageType) {
    ...
    }
    }
    16

    View full-size slide

  17. ղܾࡦɿ ґଘؔ܎ٯసͷݪଇ

    final class MessageSender {
    private let api: CommonMessageAPIProtocol // protocolԽ
    init(messageType: MessageType,
    api: CommonMessageAPIProtocol) { // DI
    ...
    }
    }
    17

    View full-size slide

  18. Swift Complier Error
    18

    View full-size slide

  19. let sut = MessageSender(
    api: /*
    !
    ͜͜ʹԿΛೖΕΑ͏…ʁ */,
    messageType: .image
    )
    let cases = [
    ...
    ]
    cases.forEach {
    sut.text = $0.text
    sut.image = $0.image
    XCTAssertEqual(sut.isValid, $0.expects, line: $0.line)
    }
    19

    View full-size slide

  20. όϦσʔγϣϯͷςετ
    Λ͍ͨ͠ͷʹ
    ؔ܎ͷͳ͍ґଘ͕͋Δ
    20

    View full-size slide

  21. ղܾࡦɿ ୯Ұ੹೚ݪଇ
    • όϦσʔγϣϯϩδοΫΛ੾Γग़͢
    • ޓ͍ʹ஌Δඞཁͷͳ͍TextMessageͱImageMessageͷ
    ValidatorΛ෼ׂ
    21

    View full-size slide

  22. struct ImageMessageInputValidator {
    let image: UIImage?
    let text: String?
    var isValid: Bool {
    if image == nil { return false }
    if let text = text, text.count > 80 { return false }
    return true
    }
    }
    ※ iOSઃܭຊͰ͸͞ΒʹઌͷਐԽ͕͋Γ·͢ɻ
    ɹؾʹͳΔਓ͸ຊΛಡΜͰ͔֬ΈͯΈΖʂ
    22

    View full-size slide

  23. վળςετίʔυ
    // apiͷ͜ͱΛҙࣝ͢Δඞཁ͕ͳ͘ͳͬͨ
    // messageType΋ཁΒͳ͘ͳͬͨ
    let cases = [
    ...
    ]
    cases.forEach {
    let sut = ImageMessageInputValidator(
    text: $0.text,
    image: $0.image
    )
    XCTAssertEqual(sut.isValid, $0.expects, line: $0.line)
    }
    23

    View full-size slide

  24. ܕͷ੹຿Λখ͘͞อͭͱ
    ςετ΋͠΍͍͢͠ංେԽ΋๷͛Δ
    25

    View full-size slide

  25. Α͘ݟΔίʔυ
    final class HogeListPresenter {
    private func didReceiveResponse(hoges: [Hoge]) {
    updateViewDataArray(with: hoges)
    }
    private func updateViewDataArray(with hoges: [Hoge]) {
    self.viewDataArray = hoges.map {
    if hoge.isXXX {
    return HogeViewData(...)
    } else {
    return HogeViewData(...)
    }
    }
    }
    }
    27

    View full-size slide

  26. Α͘ݟΔίʔυ
    final class HogeListPresenter {
    private func didReceiveResponse(hoges: [Hoge]) {
    updateViewDataArray(with: hoges) // Ͳ͏͍͏෭࡞༻͕͋Δͷ͔҉໧త
    }
    private func updateViewDataArray(with hoges: [Hoge]) {
    self.viewDataArray = hoges.map {
    if hoge.isXXX {
    return HogeViewData(...)
    } else {
    return HogeViewData(...)
    }
    }
    }
    }
    28

    View full-size slide

  27. ͡Ό͋͜͏͢Δʁ
    final class HogeListPresenter {
    private func didReceiveResponse(hoges: [Hoge]) {
    self.viewDataArray = makeViewDataArray(from: hoges)
    }
    private func makeViewDataArray(from hoges: [Hoge]) -> [HogeViewData] {
    return hoges.map {
    if hoge.isXXX {
    return HogeViewData(...)
    } else {
    return HogeViewData(...)
    }
    }
    }
    }
    29

    View full-size slide

  28. [Hoge] Λ [HogeViewData] ʹ
    ม׵͢Δͷ͸ผʹ
    HogeListPresenter
    Πϯελϯε͡Όͳͯ͘΋
    Ͱ͖Δ
    30

    View full-size slide

  29. ͦΕͳΒ͜͏Ͱ͸
    final class HogeListPresenter {
    func didReceiveResponse(hoges: [Hoge]) {
    self.viewDataArray = hoges.map(HogeViewData.init)
    }
    }
    private extension HogeViewData {
    init(hoge: Hoge) {
    ...
    }
    }
    31

    View full-size slide

  30. • makeViewDataArray(from:) ͱ͍͏Α͏ͳ
    ৑௕ͳ໊લͰͳ͘ͳͬͨ
    • HogeListPresenter ʹର͢Δ෭࡞༻͕ͳ͍͜ͱ͕໌֬
    • HogeViewData ୯ମͰςετ΋Ͱ͖Δߏ଄ʹͳͬͨ
    32

    View full-size slide

  31. ੹຿Λਖ਼͘͠อͭ͜ͱͰ
    ߟ͑Δ͜ͱ͕ݮΓ
    ςετ͠΍͘͢ͳΓ
    API͕γϯϓϧʹͳͬͨ
    33

    View full-size slide

  32. Happy
    ͳઃܭ͍͖ͯ͠·͠ΐ͏
    35

    View full-size slide