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

704056da9a4c4e075ad14479beaebab7?s=47 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(依存関係逆転の原則)について、
ちょっと角度を変えてテストの側面から捉えてみます。

704056da9a4c4e075ad14479beaebab7?s=128

takasek

December 21, 2018
Tweet

Transcript

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

  2. takasek @takasek Works OSS: ActionClosurable౳ App: PasteTheType Articles ʮίϯύΠϧΤϥʔ΍ϥϯλΠϜΤϥʔΛ௚͍ͯ͘͠ ͚ͩͰiOSΞϓϦͷ࡞Γํ͕Θ͔ΔϓϩδΣΫτʯ

    ʮ͓લΒ͕ModelͱݺͿΞϨΛͳΜͱݺͿ΂͖͔ʯ ʮiOSΞϓϦઃܭύλʔϯೖ໳ʯʢڞஶʣ 2
  3. ʮiOSΞϓϦઃܭύλʔϯೖ໳ʯ ઌߦൃച഑ૹதʂ Ұൠൃചؒۙʂ takasek୲౰ষ ୈ1ষʮઃܭ͢Δͱ͍͏͜ͱʯ ୈ2ষʮઃܭʹύλʔϯΛద༻͢Δલʹʯ ୈ3ষʮSwiftΒ͘͠ઃܭ͢Δʯ ୈ4ষʮΞʔΩςΫνϟͷύλʔϯΛௗᛌ͢ Δʯ 3

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

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

  6. Α͍ઃܭ #ͱ͸ 6

  7. ୈ2ষʮઃܭʹύλʔϯΛద༻͢Δલʹʯ ཁ໿ • ໊લʹΑͬͯ੹຿ͷڥքΛ໌ࣔ͢Δ • ෆద੾ͳ੹຿ʢίʔυͷष͍ʣ͸ɺ ɹઃܭͷݪଇʹΑͬͯݴޠԽͰ͖Δ • ඞཁʹԠͯ͡ઃܭͷݪଇɺύλʔϯΛద༻͢Δ͕ɺ΍Γ͗͢͸ې෺ •

    ઃܭ͸୯७ʹ࢝ΊɺϦϑΝΫλϦϯάΛਐΊͳ͕Β ɹΠςϨʔςΟϒʹਐԽͤ͞Δ • ϦϑΝΫλϦϯάͷͨΊʹ͸ɺςετ͠΍͍͢ઃܭ͕લఏ 7
  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
  9. MessageSender ͷ ໰୊఺ όϦσʔγϣϯϩδοΫ͕ MessageSender ͷதͰ ๲ΒΈ͍͗ͯ͢Δ 9

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

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

  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
  13. ໰୊ͳ͍ 13

  14. ຊ౰ʹʁ 14

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

  16. ղܾࡦɿ ґଘؔ܎ٯసͷݪଇ ❌ final class MessageSender { private let api

    = CommonMessageAPI() init(messageType: MessageType) { ... } } 16
  17. ղܾࡦɿ ґଘؔ܎ٯసͷݪଇ ⭕ final class MessageSender { private let api:

    CommonMessageAPIProtocol // protocolԽ init(messageType: MessageType, api: CommonMessageAPIProtocol) { // DI ... } } 17
  18. Swift Complier Error 18

  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
  20. όϦσʔγϣϯͷςετ Λ͍ͨ͠ͷʹ ؔ܎ͷͳ͍ґଘ͕͋Δ 20

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

  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
  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
  24. Happy 24

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

  26. ͱ͜ΖͰ 26

  27. Α͘ݟΔίʔυ 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
  28. Α͘ݟΔίʔυ 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
  29. ͡Ό͋͜͏͢Δʁ 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
  30. [Hoge] Λ [HogeViewData] ʹ ม׵͢Δͷ͸ผʹ HogeListPresenter Πϯελϯε͡Όͳͯ͘΋ Ͱ͖Δ 30

  31. ͦΕͳΒ͜͏Ͱ͸ final class HogeListPresenter { func didReceiveResponse(hoges: [Hoge]) { self.viewDataArray

    = hoges.map(HogeViewData.init) } } private extension HogeViewData { init(hoge: Hoge) { ... } } 31
  32. • makeViewDataArray(from:) ͱ͍͏Α͏ͳ ৑௕ͳ໊લͰͳ͘ͳͬͨ • HogeListPresenter ʹର͢Δ෭࡞༻͕ͳ͍͜ͱ͕໌֬ • HogeViewData ୯ମͰςετ΋Ͱ͖Δߏ଄ʹͳͬͨ

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

  34. Happy 34

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