Swift Language
Начало работы с программно-ориентированным программированием
Поиск…
замечания
Дополнительную информацию по этой теме см. В разделе « Протокольное программирование WWDC 2015» в Swift .
Существует также большое письменное руководство по тому же: Введение в протокол-ориентированное программирование в Swift 2 .
Использование программно-ориентированного программирования для модульного тестирования
Программно-ориентированное программирование - полезный инструмент, позволяющий легко писать лучшие модульные тесты для нашего кода.
Предположим, мы хотим протестировать UIViewController, который полагается на класс ViewModel.
Необходимые шаги для производственного кода:
- Определите протокол, который предоставляет открытый интерфейс класса ViewModel со всеми свойствами и методами, необходимыми для UIViewController.
- Реализовать реальный класс ViewModel, соответствующий этому протоколу.
- Используйте метод инъекции зависимостей, чтобы позволить контроллеру представления использовать нужную нам реализацию, передавая его как протокол, а не конкретный экземпляр.
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)
}
}
Использование протоколов в качестве типов первого класса
Программно ориентированное по протоколу программное обеспечение может использоваться в качестве основного шаблона проектирования Swift.
Различные типы могут соответствовать одному протоколу, типы значений могут даже соответствовать нескольким протоколам и даже обеспечивать реализацию метода по умолчанию.
Первоначально определены протоколы, которые могут представлять собой обычно используемые свойства и / или методы с конкретными или генерическими типами.
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
}
Здесь элемент struct расширяется, чтобы соответствовать элементу отображения.
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())
}
}
Пример сайта вызова для использования метода статического 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 условно сохраняются в объекте 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
}
Здесь класс поддержки Core Data также может соответствовать протоколу 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)
}
}
}