在编写有意义的代码时,我们都以很多种方式接受了协议。随着Swift的发展,编程的方向完全转向面向协议编程,以实现编写简洁代码的多重好处。本文将借助一些小例子来尝试列举这些好处。
可测试性
当您要测试需要访问网络或需要存储在数据库或磁盘中的内容时,可以使用协议。可以轻松地创建一个实现该协议并测试该协议的功能的模拟类。让我们通过一个简单的示例来了解这一点。
考虑到您需要从给定的URL中获取数据,并成功返回数据或失败返回错误。因此,我们定义协议和类如下:
public enum Error: Swift.Error {
case connectivity
case invalidData
}
protocol Service {
func fetchData(_ callback: @escaping ([Data]?, Error?) -> ())
}
class DataService: Service {
func fetchData(_ callback: @escaping ([Data]?, Error?) -> ()) {
// fetch data
}
}
现在,为了测试该类,我们肯定不会点击URL并等待响应并声明我们的结果。相反,我们将创建一个将实现该协议的模拟类。
class ServiceTest: XCTestCase {
func test_noData_returnsNoData() {
let sut = ServiceSpy(data: nil, error: Error.connectivity)
sut.fetchData { (data, error) in
XCTAssertNil(data)
XCTAssertNotNil(error)
}
}
func test_withData_returnsData() {
let sut = ServiceSpy(data: Data(base64Encoded: “test_data”), error: nil)
sut.fetchData { (data, error) in
XCTAssertNotNil(data)
XCTAssertNil(error)
})
}
class ServiceSpy: Service {
var data: Data?
var error: Error?
init(data: Data?, error: Error?) {
self.data = data
self.error = error
}
func fetchData(_ callback: @escaping ([Data]?, Error?) -> ()) {
if let data = data {
callback(data, nil)
}
if let error = error {
callback(nil, error)
}
}
}
}
这种方法增加了代码的覆盖范围,并有助于提高可测试性。
实现多重继承
众所周知,Swift不允许多类继承。这也不意味着我们将多个功能放在同一个类中,这违反了单一责任原则。使用协议,我们可以将多个功能隔离到不同的类中并与它们兼容。让我们看一个例子。
考虑一个具有说话和行走方法的Person类。我们在协议Talkable和Walkable中声明了这些方法,然后Person类实现协议。
protocol Talkable {
func talk()
}
extension Talkable {
func talk() {
print(“Person can Talk”)
}
}
protocol Walkable {
func walk()
}
extension Walkable {
func walk() {
print(“Person can Walk”)
}
}
class Person: Talkable, Walkable {
}
let person = Person()
person.talk()
person.walk()
值类型的继承
在上一个示例中,为简单起见,我们没有向Person类添加任何其他行为。使Person成为结构体而不是没有任何行为的繁重类是有意义的。但是由于结构体不允许继承,因此可能会犹豫是否这样做。使用协议,可以在结构体中实现所需的继承。
依赖注入
我们都知道依赖注入在干净的代码实践中的重要性以及它带来的好处,例如
- 单一责任原则
- 可测试性
- 避免单例实例的变化 还有很多。通过使参数符合协议,可以带来更多好处,例如
- 实现一种通用行为,其中可以注入符合给定协议的任何类型
- 使用模拟的注入参数测试类的能力 让我们看一个示例,以帮助您更好地理解这一点。考虑一个MVVM模式,其中有一个ViewModel和一个视图控制器。
protocol HelloViewModelable {
func fetchHello()
}
class HelloViewModel: HelloViewModelable {
weak var view: DayViewController?
func fetchHello() {
view?.updateView(hello: “Hello”)
}
}
class HelloViewController: UIViewController {
let viewModel: HelloViewModelable
@IBOutlet weak var helloLabel: UILabel!
init(viewModel: HelloViewModelable) {
self.viewModel = viewModel
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel.fetchHello()
}
func updateView(hello: String) {
DispatchQueue.main.async { [weak self] in
self?.helloLabel.text = “\(hello)”
}
}
}
在这里,视图控制器正在调用注入的viewModel的fetchHello函数。因为我们正在注入协议,所以我们可以注入符合协议的viewModel变体。再次,如在上面的示例中看到的,我们可以在测试视图控制器时模拟视图模型,反之亦然。
重用性
通过协议扩展,我们可以在实现相同功能的多个类中重用一段代码,而无需覆盖和创建继承链。
考虑一个示例,其中针对不同类型的动物(例如鱼,鸟和大象)有多个类别。他们都有一种共同的呼吸行为。因此,我们可以提取协议扩展的常见行为并重用呼吸行为。
protocol Breathable {
func breathe()
}
extension Breathable {
func breathe() {
print(“This creature can breathe.”)
}
}
struct Fish: Breathable {
}
struct Bird: Breathable {
}
struct Elephant: Breathable {
}
let fish = Fish()
let bird = Bird()
let elephant = Elephant()
fish.breathe()
bird.breathe()
elephant.breathe()
以上是POP提供的一些好处。我希望这将有助于更好地了解协议,并激发您提高您的代码质量。请随时在下面的评论中添加其他您能想到的好处,以帮助其他读者。另外,欢迎点赞,让更多人看到,它使我们都是更好的程序员!