Swift Language
Rozpoczęcie pracy z programowaniem zorientowanym na protokół
Szukaj…
Uwagi
Więcej informacji na ten temat można znaleźć w wykładzie WWDC 2015 Programowanie zorientowane na protokół w Swift .
Jest też świetny pisemny przewodnik na ten sam temat: Wprowadzenie do programowania zorientowanego na protokół w Swift 2 .
Wykorzystanie programowania zorientowanego na protokół do testowania jednostkowego
Programowanie zorientowane na protokół jest przydatnym narzędziem do łatwego pisania lepszych testów jednostkowych dla naszego kodu.
Powiedzmy, że chcemy przetestować UIViewController, który opiera się na klasie ViewModel.
Potrzebne kroki w kodzie produkcyjnym to:
- Zdefiniuj protokół, który udostępnia interfejs publiczny klasy ViewModel, ze wszystkimi właściwościami i metodami wymaganymi przez UIViewController.
- Zaimplementuj prawdziwą klasę ViewModel, zgodną z tym protokołem.
- Użyj techniki wstrzykiwania zależności, aby umożliwić kontrolerowi widoku wykorzystanie implementacji, którą chcemy, przekazując ją jako protokół, a nie konkretną instancję.
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")
Następnie w teście jednostkowym:
- Zaimplementuj próbny model ViewModel, który jest zgodny z tym samym protokołem
- Przekaż go do testowanego kontrolera UIViewController za pomocą wstrzykiwania zależności zamiast rzeczywistej instancji.
- 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)
}
}
Używanie protokołów jako typów pierwszej klasy
Programowanie zorientowane na protokół może być wykorzystane jako podstawowy wzór Swift.
Różne typy mogą być zgodne z tym samym protokołem, typy wartości mogą nawet być zgodne z wieloma protokołami, a nawet zapewniać domyślną implementację metody.
Początkowo zdefiniowane są protokoły, które mogą reprezentować powszechnie używane właściwości i / lub metody z konkretnymi lub ogólnymi typami.
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)
}
Można utworzyć domyślną implementację dla metody get, ale jeśli pożądane typy zgodne mogą zastąpić implementację.
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 { }
Typ wartości zgodny z protokołem ItemData, ten typ wartości może również być zgodny z innymi protokołami.
struct Item: ItemData {
let title: String
let description: String
let thumbnailURL: NSURL
let created: NSDate
let updated: NSDate
}
Tutaj struktura elementu jest rozszerzana, aby była zgodna z elementem wyświetlanym.
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())
}
}
Przykładowa strona wywoławcza do użycia metody get static.
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
})
}
Różne przypadki użycia będą wymagały różnych implementacji. Główną ideą tutaj jest wykazanie zgodności z różnymi typami, w których protokół jest głównym punktem zainteresowania w projekcie. W tym przykładzie być może dane API są warunkowo zapisane w encji 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
}
Tutaj klasa oparta na danych podstawowych może być również zgodna z protokołem 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)
}
}
}