Swift Language
Komma igång med protokollorienterad programmering
Sök…
Anmärkningar
För mer information om detta ämne, se WWDC 2015- protokollorienterad programmering i Swift .
Det finns också en bra skriftlig guide om samma: Introduktion av protokollorienterad programmering i Swift 2 .
Utnyttja protokollorienterad programmering för enhetstestning
Protokollorienterad programmering är ett användbart verktyg för att enkelt kunna skriva bättre enhetstester för vår kod.
Låt oss säga att vi vill testa en UIViewController som förlitar sig på en ViewModel-klass.
De nödvändiga stegen på produktionskoden är:
- Definiera ett protokoll som visar det offentliga gränssnittet för klassen ViewModel, med alla egenskaper och metoder som behövs av UIViewController.
- Implementera den verkliga ViewModel-klassen, i enlighet med det protokollet.
- Använd en beroende-injektionsteknik för att låta visningskontrollern använda den implementering vi vill, och skickar den som protokollet och inte som den konkreta instansen.
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")
Sedan vid enhetstest:
- Implementera en hålig ViewModel som överensstämmer med samma protokoll
- Vidarebefordra den till UIViewController under test med beroendeinjektion istället för den verkliga instansen.
- Testa!
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)
}
}
Använda protokoll som första klassstyper
Protokollorienterad programmering kan användas som ett centralt designmönster för Swift.
Olika typer kan överensstämma med samma protokoll, värdetyper kan till och med överensstämma med flera protokoll och till och med ge standardmetodimplementering.
Ursprungligen definieras protokoll som kan representera vanliga egenskaper och / eller metoder med antingen specifika eller generiska typer.
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)
}
En standardimplementering för get-metoden kan skapas, men om så önskas kan överensstämmande typer åsidosätta implementeringen.
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 { }
En värdetyp som överensstämmer med ItemData-protokollet, denna värdetyp kan också överensstämma med andra protokoll.
struct Item: ItemData {
let title: String
let description: String
let thumbnailURL: NSURL
let created: NSDate
let updated: NSDate
}
Här utvidgas artikelstrukturen så att den överensstämmer med ett visningsobjekt.
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())
}
}
Ett exempel samtalssida för att använda den statiska get-metoden.
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
})
}
Olika användningsfall kräver olika implementeringar. Huvudtanken här är att visa överensstämmelse från olika typer där protokollet är huvudpunkten för fokus i designen. I det här exemplet sparas kanske API-data villkorligt i en kärndataenhet.
// 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
}
Här kan klassen Core Data backas också anpassas till DisplayItem-protokollet.
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)
}
}
}