Swift Language
Introduzione alla programmazione orientata ai protocolli
Ricerca…
Osservazioni
Per ulteriori informazioni su questo argomento, consultare la programmazione orientata al protocollo talk di WWDC 2015 in Swift .
C'è anche una grande guida scritta sullo stesso: Introduzione alla programmazione orientata ai protocolli in Swift 2 .
Utilizzo della programmazione orientata ai protocolli per il test delle unità
La programmazione orientata al protocollo è uno strumento utile per scrivere facilmente test unitari migliori per il nostro codice.
Diciamo che vogliamo testare un UIViewController che si basa su una classe ViewModel.
I passaggi necessari sul codice di produzione sono:
- Definire un protocollo che espone l'interfaccia pubblica della classe ViewModel, con tutte le proprietà e i metodi necessari da UIViewController.
- Implementare la classe ViewModel reale, conforme a tale protocollo.
- Utilizzare una tecnica di iniezione delle dipendenze per consentire al controller di visualizzazione di utilizzare l'implementazione desiderata, passandola come protocollo e non come istanza concreta.
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")
Quindi, sul test unitario:
- Implementare un finto ViewModel conforme allo stesso protocollo
- Passalo a UIViewController sotto test usando dependency injection, invece dell'istanza reale.
- Test!
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)
}
}
Utilizzo dei protocolli come tipi di prima classe
La programmazione orientata al protocollo può essere utilizzata come un modello di base di Swift.
Diversi tipi sono in grado di conformarsi allo stesso protocollo, i tipi di valore possono persino conformarsi a più protocolli e persino fornire l'implementazione del metodo di default.
Inizialmente vengono definiti protocolli che possono rappresentare proprietà e / o metodi comunemente utilizzati con tipi specifici o generici.
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)
}
È possibile creare un'implementazione predefinita per il metodo get, anche se i tipi conformi desiderati potrebbero sovrascrivere l'implementazione.
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 { }
Un tipo di valore conforme al protocollo ItemData, questo tipo di valore è anche in grado di conformarsi ad altri protocolli.
struct Item: ItemData {
let title: String
let description: String
let thumbnailURL: NSURL
let created: NSDate
let updated: NSDate
}
Qui la struttura dell'articolo viene estesa per conformarsi a un elemento di visualizzazione.
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())
}
}
Un sito di chiamata di esempio per l'utilizzo del metodo get statico.
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
})
}
Diversi casi d'uso richiederanno diverse implementazioni. L'idea principale qui è mostrare la conformità da tipi diversi in cui il protocollo è il punto principale del focus nel design. In questo esempio, forse i dati API vengono salvati condizionalmente in un'entità Core Data.
// 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
}
Qui la classe di supporto Core Data può anche essere conforme al protocollo 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)
}
}
}