Swift Language
プロトコル指向プログラミング入門
サーチ…
備考
このトピックの詳細については、WWDC 2015 talk プロトコル指向プログラミング(Swift )を参照してください。
また、同じことについて書かれた素晴らしいガイドがあります。Swift 2でのプロトコル指向プログラミングの紹介 。
ユニットテストのためのプロトコル指向プログラミングの活用
プロトコル指向プログラミングは、コードの単体テストを簡単に書くための便利なツールです。
ViewModelクラスに依存するUIViewControllerをテストしたいとしましょう。
プロダクションコードに必要な手順は次のとおりです。
- UIViewControllerで必要とされるすべてのプロパティとメソッドを使用して、クラスViewModelのパブリックインターフェイスを公開するプロトコルを定義します。
- そのプロトコルに準拠した実際のViewModelクラスを実装します。
- 依存関係注入技術を使用して、View Controllerが必要な実装を使用し、具体的なインスタンスではなくプロトコルとして渡します。
protocol ViewModelType {
var title : String {get}
func confirm()
}
class ViewModel : ViewModelType {
let title : String
init(title: String) {
self.title = title
}
func confirm() { ... }
}
class ViewController : UIViewController {
// We declare the viewModel property as an object conforming to the protocol
// so we can swap the implementations without any friction.
var viewModel : ViewModelType!
@IBOutlet var titleLabel : UILabel!
override func viewDidLoad() {
super.viewDidLoad()
titleLabel.text = viewModel.title
}
@IBAction func didTapOnButton(sender: UIButton) {
viewModel.confirm()
}
}
// With DI we setup the view controller and assign the view model.
// The view controller doesn't know the concrete class of the view model,
// but just relies on the declared interface on the protocol.
let viewController = //... Instantiate view controller
viewController.viewModel = ViewModel(title: "MyTitle")
次に、ユニットテストで:
- 同じプロトコルに準拠したモックViewModelを実装する
- 実際のインスタンスではなく、依存関係注入を使用してテスト中のUIViewControllerに渡します。
- テスト!
class FakeViewModel : ViewModelType {
let title : String = "FakeTitle"
var didConfirm = false
func confirm() {
didConfirm = true
}
}
class ViewControllerTest : XCTestCase {
var sut : ViewController!
var viewModel : FakeViewModel!
override func setUp() {
super.setUp()
viewModel = FakeViewModel()
sut = // ... initialization for view controller
sut.viewModel = viewModel
XCTAssertNotNil(self.sut.view) // Needed to trigger view loading
}
func testTitleLabel() {
XCTAssertEqual(self.sut.titleLabel.text, "FakeTitle")
}
func testTapOnButton() {
sut.didTapOnButton(UIButton())
XCTAssertTrue(self.viewModel.didConfirm)
}
}
プロトコルをファーストクラスの型として使用する
プロトコル指向のプログラミングは、コアの迅速な設計パターンとして使用できます。
異なるタイプは同じプロトコルに準拠することができ、値タイプは複数のプロトコルに準拠し、さらにデフォルトのメソッド実装を提供することさえできます。
最初に、一般的に使用されるプロパティおよび/または特定のタイプまたは汎用タイプのメソッドを表すプロトコルが定義されています。
protocol ItemData {
var title: String { get }
var description: String { get }
var thumbnailURL: NSURL { get }
var created: NSDate { get }
var updated: NSDate { get }
}
protocol DisplayItem {
func hasBeenUpdated() -> Bool
func getFormattedTitle() -> String
func getFormattedDescription() -> String
}
protocol GetAPIItemDataOperation {
static func get(url: NSURL, completed: ([ItemData]) -> Void)
}
getメソッドのデフォルトの実装を作成することができますが、適合している型が実装をオーバーライドすることもあります。
extension GetAPIItemDataOperation {
static func get(url: NSURL, completed: ([ItemData]) -> Void) {
let date = NSDate(
timeIntervalSinceNow: NSDate().timeIntervalSince1970
+ 5000)
// get data from url
let urlData: [String: AnyObject] = [
"title": "Red Camaro",
"desc": "A fast red car.",
"thumb":"http://cars.images.com/red-camaro.png",
"created": NSDate(), "updated": date]
// in this example forced unwrapping is used
// forced unwrapping should never be used in practice
// instead conditional unwrapping should be used (guard or if/let)
let item = Item(
title: urlData["title"] as! String,
description: urlData["desc"] as! String,
thumbnailURL: NSURL(string: urlData["thumb"] as! String)!,
created: urlData["created"] as! NSDate,
updated: urlData["updated"] as! NSDate)
completed([item])
}
}
struct ItemOperation: GetAPIItemDataOperation { }
ItemDataプロトコルに準拠した値の型で、この値の型も他のプロトコルに準拠することができます。
struct Item: ItemData {
let title: String
let description: String
let thumbnailURL: NSURL
let created: NSDate
let updated: NSDate
}
ここでアイテム構造体は、表示アイテムに適合するように拡張されています。
extension Item: DisplayItem {
func hasBeenUpdated() -> Bool {
return updated.timeIntervalSince1970 >
created.timeIntervalSince1970
}
func getFormattedTitle() -> String {
return title.stringByTrimmingCharactersInSet(
.whitespaceAndNewlineCharacterSet())
}
func getFormattedDescription() -> String {
return description.stringByTrimmingCharactersInSet(
.whitespaceAndNewlineCharacterSet())
}
}
static getメソッドを使用するコールサイトの例
ItemOperation.get(NSURL()) { (itemData) in
// perhaps inform a view of new data
// or parse the data for user requested info, etc.
dispatch_async(dispatch_get_main_queue(), {
// self.items = itemData
})
}
異なるユースケースでは、異なる実装が必要になります。ここでの主なアイデアは、プロトコルが設計の焦点の主要なポイントである様々なタイプからの適合性を示すことです。この例では、おそらくAPIデータがコアデータエンティティに条件付きで保存されます。
// the default core data created classes + extension
class LocalItem: NSManagedObject { }
extension LocalItem {
@NSManaged var title: String
@NSManaged var itemDescription: String
@NSManaged var thumbnailURLStr: String
@NSManaged var createdAt: NSDate
@NSManaged var updatedAt: NSDate
}
ここでは、コアデータバックアップクラスもDisplayItemプロトコルに準拠することができます。
extension LocalItem: DisplayItem {
func hasBeenUpdated() -> Bool {
return updatedAt.timeIntervalSince1970 >
createdAt.timeIntervalSince1970
}
func getFormattedTitle() -> String {
return title.stringByTrimmingCharactersInSet(
.whitespaceAndNewlineCharacterSet())
}
func getFormattedDescription() -> String {
return itemDescription.stringByTrimmingCharactersInSet(
.whitespaceAndNewlineCharacterSet())
}
}
// In use, the core data results can be
// conditionally casts as a protocol
class MyController: UIViewController {
override func viewDidLoad() {
let fr: NSFetchRequest = NSFetchRequest(
entityName: "Items")
let context = NSManagedObjectContext(
concurrencyType: .MainQueueConcurrencyType)
do {
let items: AnyObject = try context.executeFetchRequest(fr)
if let displayItems = items as? [DisplayItem] {
print(displayItems)
}
} catch let error as NSError {
print(error.localizedDescription)
}
}
}