Swift: Mixins using Protocol Extensions

在继承、组合外添加了新的功能添加(组合)方式

Protocols can have default implementations for requirements specified in a protocol extension, allowing “mixin” or “trait” like patterns.

If you have experience with other languages such as Ruby or Python — or even PHP! — you may have seen mixins and traits before, but we haven’t had this on iOS or OS X yet. It allows for a slightly different way of programming that has benefits over traditional object-oriented programming.

https://machinethink.net/blog/mixins-and-traits-in-swift-2.0/

 

Protocol extensions are a great feature of Swift 2. With a protocol extension we can add a method that will apply to every types conforming to a protocol. Other than extending types conforming to a protocol, another interesting use is to provide a default implementation for methods defined in a protocol. This second use is what I’m going to be pushing a bit further here.

Let’s start with a generic protocol with a generic name: Node. A node has a name and may have child nodes. Not all node types will have children however. The Node protocol will look like this:

protocol Node: class {

    /// The parent node this node belongs to, or `nil` if this is a root node.
    var parent: Node? { get }
    /// The index of this node within the parent node. `nil` if no parent.
    var indexInParent: Int? { get }

    /// The name of this node, typically a file name.
    var name: String { get }

    /// Number of children of this node
    var count: Int { get }
    /// Get a children of this node by its index.
    subscript (index: Int) -> Node { get }
    /// Get a children of this node by name, or `nil` if there is none.
    subscript (name: String) -> Node? { get }
    /// Names for all children of this node.
    var childrenNames: [String] { get }
    /// The index of the child node with `name`.
    func indexForName(name: String) -> Int?

    /// Insert a new node at the given index.
    func insert(node: Node, atIndex index: Int) throws
    /// Remove the node at the given index.
    func removeAtIndex(index: Int) throws

}

enum NodeError: ErrorType {
    case UnsupportedContent
}

Now that we have a protocol, we’ll want to implement some types conforming to that protocol. Some of those types will represent leaf nodes — nodes that will never have children. A simple implementation would look like this:

class ImageNode: Node {
    // Basic node implementation
    weak var parent: Node? = nil
    var indexInParent: Int? {
        return parent?.indexForName(name)
    }
    var name: String = ""

    // Boilerplate for a node with no children:
    var count: Int {
        return 0
    }
    subscript (index: Int) -> Node {
        fatalError()
    }
    subscript (name: String) -> Node? {
        return nil
    }
    var childrenNames: [String] {
        return []
    }
    func indexForName(name: String) -> Int? {
        return nil
    }
    func insert(node: Node, atIndex index: Int) throws {
        throw NodeError.UnsupportedContent
    }
    func removeAtIndex(index: Int) throws {
        fatalError()
    }
}

Oh oh… so much boilerplate for implementating the part of the node dealing with children! Imagine we now add a few other leaf node types, how many time are we going to rewrite all this? Worse, if we change or add a method to Node for children, we’ll have to revisit all the leaf types and make the corresponding change. Wouldn’t it be nice if all those classes could share the same implementation for the “no children” aspect of a node?

One way we can do this is by having a common base class for nodes with no children. But this does not compose well if we have multiple aspects we want to be able to mix together. For instance, our model might be composed of many “root” nodes (nodes with no parent), in which case the boilerplate for parent and indexInParent would have to be repeated everywhere in the same way. Some node types might be root nodes and at the same time have no children, and in this case which base class do we choose?

Contrary to class inheritance, protocols can be combined. So instead of creating a base class, let’s make this clever empty protocol, then accompany it with a protocol extension that implements the aspect of the Node protocol dealing with children:

protocol NoChildrenNode: Node {
}
extension NoChildrenNode {
    var count: Int {
        return 0
    }
    subscript (index: Int) -> Node {
        fatalError()
    }
    subscript (name: String) -> Node? {
        return nil
    }
    var childrenNames: [String] {
        return []
    }
    func indexForName(name: String) -> Int? {
        return nil
    }
    func insert(node: Node, atIndex index: Int) throws {
        throw NodeError.UnsupportedContent
    }
    func removeAtIndex(index: Int) throws {
        fatalError()
    }
}

Now we can remove the boilerplate from ImageNode, we just have to add the conformance to the NoChildrenNode protocol:

class ImageNode2: Node, NoChildrenNode {
    // Basic node implementation
    weak var parent: Node? = nil
    var indexInParent: Int? {
        return parent?.indexForName(name)
    }
    var name: String = ""
}

And this can be freely combined with other protocols to implement other aspects of our node. Note how the sole reason we have the NoChildrenNode protocol is to mix its protocol extension into ImageNode2? That’s why I call this protocol a mixin.

Let’s continue by implementing the root node aspect mentioned earlier:

protocol RootNode: Node {
}
extension RootNode {
    var parent: Node? {
        return nil
    }
    var indexInParent: Int? {
        return nil
    }
}

Our root node class that also has no children now becomes very straightforward to implement:

class RootImageNode: Node, RootNode, NoChildrenNode {
    // Basic node implementation
    var name: String = ""
}

All this is very interesting, but protocols can’t create stored properties. That limits the usefullness if we can’t store any state. Is there way to work around this? Sort of: just require a variable in the protocol and make use of it.

protocol NodeWithChildren: Node {
    var children: [Node] { get set }
}
extension NodeWithChildren {
    var count: Int {
        return children.count
    }
    subscript (index: Int) -> Node {
        return children[index]
    }
    subscript (name: String) -> Node? {
        guard let index = indexForName(name) else { return nil }
        return children[index]
    }
    var childrenNames: [String] {
        return children.map { $0.name }
    }
    func indexForName(name: String) -> Int? {
        return children.indexOf { $0.name == name }
    }
    func insert(node: Node, atIndex index: Int) throws {
        children.insert(node, atIndex: index)
    }
    func removeAtIndex(index: Int) throws {
        children.removeAtIndex(index)
    }
}

This implementation is far from optimal performance-wise, feel free to make a better one. The important point is that now, it becomes very easy to define a node with children:

class FolderNode: Node, NodeWithChildren {
    weak var parent: Node? = nil
    var indexInParent: Int? {
        return parent?.indexForName(name)
    }
    var name: String = ""

    // Required by NodeWithChildren:
    var children: [Node] = []
}

All we need is to add the children property that NodeWithChildren expects, and all the relevant methods get mixed into our class.

And we can still combine it with other mixin protocols. Here we create a root node with children with very little code:

class RootFolderNode: Node, NodeWithChildren, RootNode {
    var name: String = ""
    // Required by NodeWithChildren:
    var children: [Node] = []
}

Unlike classes, protocols are composable and we can combine them together in the same type. By using protocol extensions we can plug together code that represents different aspects of a type. This make it easy to create various types that follow a specific pattern but that have to differ in some other ways.

What I’ve shown here with classes can also work for structs and even enums by making them conform to our mixin protocols. Remember that, with classes, overriding in subclasses won’t work unless the method is part of a protocol the class implements. Methods solely present in an extension are statically dispatched and overriding them won’t work.

I haven’t touched the subject here, but we can also use associated types in our mixin protocols. We could for instance implement something that checks that all inserted nodes are of a specific type.

At the moment there seem to be a problem with setting the visibility of those protocols. If the Node protocol is public, the NoChildrenNode protocol has to be public also. This prevents us from making the implementation private or internal. Filled in SR-697.

Last thing, but that’s an important one: make sure not to abuse this by making things more complex than they should. Decomposing things into too many little pieces is going to make things complicated, so use this with a bit of restraint.

 

https://michelf.ca/blog/2016/swift-mixin-protocol-extension/

posted @ 2022-05-18 15:17  zzfx  阅读(41)  评论(0编辑  收藏  举报