Recherche…


Remarques

Pour plus d'informations sur ce sujet, reportez-vous à la section consacrée à la programmation orientée protocole WWDC 2015 de Swift .

Il existe également un excellent guide écrit sur le même sujet: Introduction à la programmation orientée protocole dans Swift 2 .

Utilisation de la programmation orientée protocole pour les tests unitaires

La programmation orientée protocole est un outil utile pour écrire facilement de meilleurs tests unitaires pour notre code.

Disons que nous voulons tester un UIViewController qui repose sur une classe ViewModel.

Les étapes nécessaires sur le code de production sont les suivantes:

  1. Définissez un protocole qui expose l'interface publique de la classe ViewModel, avec toutes les propriétés et méthodes requises par UIViewController.
  2. Implémentez la vraie classe ViewModel, conforme à ce protocole.
  3. Utilisez une technique d'injection de dépendance pour permettre au contrôleur de vue d'utiliser l'implémentation souhaitée, en le transmettant en tant que protocole et non en tant qu'instance concrète.
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")

Ensuite, sur test unitaire:

  1. Implémenter un modèle ViewModel simulé conforme au même protocole
  2. Transmettez-le à UIViewController sous test en utilisant l'injection de dépendance, au lieu de l'instance réelle.
  3. Tester!
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)
    }
}

Utilisation de protocoles comme types de première classe

La programmation orientée protocole peut être utilisée comme modèle de conception Swift.

Différents types peuvent se conformer au même protocole, les types de valeur peuvent même se conformer à plusieurs protocoles et même fournir une implémentation de méthode par défaut.

Initialement, des protocoles peuvent être définis pour représenter des propriétés et / ou des méthodes couramment utilisées avec des types spécifiques ou génériques.

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

Une implémentation par défaut de la méthode get peut être créée, mais si vous le souhaitez, les types conformes peuvent remplacer l'implémentation.

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 type de valeur conforme au protocole ItemData, ce type de valeur peut également se conformer à d'autres protocoles.

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

Ici, la structure d'élément est étendue pour se conformer à un élément d'affichage.

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 exemple de site d'appel pour l'utilisation de la méthode get statique.

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

Différents cas d'utilisation nécessiteront des implémentations différentes. L'idée principale ici est de montrer la conformité de différents types lorsque le protocole est le point principal de la conception. Dans cet exemple, les données de l'API sont peut-être sauvegardées sous condition dans une 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
}

Ici, la classe sauvegardée Core Data peut également être conforme au protocole 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)
        }
        
    }
}


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow