Zoeken…


Opmerkingen

Zie de WWDC 2015 talk Protocol-Oriented Programming in Swift voor meer informatie over dit onderwerp.

Er is ook een geweldige schriftelijke gids over hetzelfde: introductie van protocol-georiënteerd programmeren in Swift 2 .

Gebruikmaken van protocol-georiënteerde programmering voor het testen van eenheden

Protocol Oriented Programming is een handig hulpmiddel om eenvoudig betere unit-tests voor onze code te schrijven.

Laten we zeggen dat we een UIViewController willen testen die afhankelijk is van een ViewModel-klasse.

De benodigde stappen voor de productiecode zijn:

  1. Definieer een protocol dat de openbare interface van de klasse ViewModel blootlegt, met alle eigenschappen en methoden die de UIViewController nodig heeft.
  2. Implementeer de echte ViewModel-klasse, conform dat protocol.
  3. Gebruik een afhankelijkheidsinjectietechniek om de viewcontroller de gewenste implementatie te laten gebruiken, en deze door te geven als het protocol en niet als de concrete instantie.
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")

Vervolgens, tijdens de unit-test:

  1. Implementeer een nep ViewModel dat voldoet aan hetzelfde protocol
  2. Geef het door aan de UIViewController die wordt getest met behulp van afhankelijkheidsinjectie, in plaats van de echte instantie.
  3. 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)
    }
}

Gebruik van protocollen als eerste klas types

Protocol-georiënteerd programmeren kan worden gebruikt als een centraal Swift-ontwerppatroon.

Verschillende typen kunnen zich aan hetzelfde protocol conformeren, waardetypes kunnen zelfs aan meerdere protocollen voldoen en bieden zelfs standaardmethode-implementatie.

Aanvankelijk zijn protocollen gedefinieerd die veelgebruikte eigenschappen en / of methoden met specifieke of generieke typen kunnen vertegenwoordigen.

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)
}

Een standaardimplementatie voor de methode get kan worden gemaakt, maar indien gewenst kunnen conforme typen de implementatie vervangen.

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 { }

Een waardetype dat voldoet aan het ItemData-protocol, dit waardetype kan ook voldoen aan andere protocollen.

struct Item: ItemData {
    
    let title: String
    let description: String
    let thumbnailURL: NSURL
    let created: NSDate
    let updated: NSDate
    
}

Hier wordt de artikelstructuur uitgebreid om te voldoen aan een weergave-item.

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())
    }
}

Een voorbeeld van een aanroepsite voor het gebruik van de statische methode 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
    })
    
}

Verschillende gebruikssituaties vereisen verschillende implementaties. Het belangrijkste idee hier is om conformiteit te tonen van verschillende typen waarbij het protocol het belangrijkste punt van de focus in het ontwerp is. In dit voorbeeld worden de API-gegevens misschien voorwaardelijk opgeslagen in een Core Data-entiteit.

// 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
}

Hier kan de door Core Data ondersteunde klasse ook voldoen aan het DisplayItem-protocol.

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)
        }
        
    }
}


Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow