4
\$\begingroup\$

For the purposes of an app I work on I have created a separate framework that abstracts away the mechanism of making atomic changes to the complex object structures.

Each class that has to participate in transactional changes of state is supposed to implement Transactable protocol where it has transaction context helper-object and several call-back functions to implement.

My question: is there a way to make it better? Or did I miss some corner-cases? (I tried to cover it well with unit-tests but still I could miss something).


Transactable

public protocol Transactable : class {

    var transactionContext: TransactionContext { get }

    func onBegin(transaction: Transaction)
    func onValidateCommit() throws
    func onCommit(transaction: Transaction)
    func onRollback(transaction: Transaction)

}

public extension Transactable {

    public var transactionIsActive: Bool { return transactionContext.transactionIsActive }
    public var activeTransaction: Transaction? { return transactionContext.activeTransaction }

    @discardableResult public func beginTransaction() throws -> Transaction {
        return try transactionContext.beginTransaction()
    }

    public func commitTransaction(_ transaction: Transaction = Transaction.any) throws {
        try transactionContext.commitTransaction(transaction)
    }

    public func rollbackTransaction(_ transaction: Transaction = Transaction.any) throws {
        try transactionContext.rollbackTransaction(transaction)
    }

    public func transaction<Result>(_ executeTransaction: (Void) throws -> Result) throws -> Result {
        if transactionContext.transactionIsActive {
            return try executeTransaction()
        } else {
            try transactionContext.beginTransaction()

            let result: Result

            do {
                result = try executeTransaction()
                try transactionContext.commitTransaction()
            }
            catch {
                try transactionContext.rollbackTransaction()
                throw error
            }

            return result
        }
    }

}

Transaction

public struct Transaction : Equatable {

    public static let any = Transaction(id: UUID.zero)

    public let id: UUID

    private init(id: UUID) {
        self.id = id
    }

    init() {
        id = {
            var id = UUID()
            while id == .zero { id = UUID() }
            return id
        }()
    }

    public static func == (lhs: Transaction, rhs: Transaction) -> Bool {
        return lhs.id == rhs.id
    }

}

TransactionError

public enum TransactionError : Error {

    case anotherTransactionIsActive
    case transactionIsNotActive
    case wrongTransactionDescriptor

}

TransactionContext

public class TransactionContext {

    final public var transactionIsActive: Bool { return activeTransaction != nil }
    public var activeTransaction: Transaction? { mustOverride() }

    typealias WrappedContextNode = WeakWrapper<TransactionContextNode>

    unowned let owner: Transactable
    var root: TransactionContextRoot { mustOverride() }
    private var children: Set<WrappedContextNode> = []

    init(owner: Transactable) {
        self.owner = owner
    }

    final public class func createRoot(owner: Transactable) -> TransactionContext {
        return TransactionContextRoot(owner: owner)
    }

    final public class func createNode(owner: Transactable, parent: Transactable) -> TransactionContext {
        return TransactionContextNode(owner: owner, parent: parent)
    }

    @discardableResult public func beginTransaction() throws -> Transaction {
        mustOverride()
    }

    public func commitTransaction(_ transaction: Transaction = Transaction.any) throws {
        mustOverride()
    }

    public func rollbackTransaction(_ transaction: Transaction = Transaction.any) throws {
        mustOverride()
    }

    final func validateCommit() throws {
        try owner.onValidateCommit()

        try children.forEach({
            try $0.object?.validateCommit()
        })
    }

    final func register(node: TransactionContextNode) {
        let wrappedNode = WrappedContextNode(node)

        assert(!children.contains(wrappedNode))

        children.insert(wrappedNode)
    }

    final func unregister(node: TransactionContextNode) {
        let wrappedNode = WrappedContextNode(node)

        assert(children.contains(wrappedNode))

        children.remove(wrappedNode)
    }

    final func propagateTransactionBegin() {
        owner.onBegin(transaction: activeTransaction!)

        for wrappedNode in children {
            guard let node = wrappedNode.object else {
                children.remove(wrappedNode)
                continue
            }

            node.propagateTransactionBegin()
        }
    }

    final func propagateTransactionCommit() {
        for wrappedNode in children {
            guard let node = wrappedNode.object else {
                children.remove(wrappedNode)
                continue
            }

            node.propagateTransactionCommit()
        }

        owner.onCommit(transaction: activeTransaction!)
    }

    final func propagateTransactionRollback() {
        for wrappedNode in children {
            guard let node = wrappedNode.object else {
                children.remove(wrappedNode)
                continue
            }

            node.propagateTransactionRollback()
        }

        owner.onRollback(transaction: activeTransaction!)
    }

}

TransactionContextRoot

final class TransactionContextRoot : TransactionContext {

    private var _transaction: Transaction? = nil
    override var activeTransaction: Transaction? { return _transaction }
    override var root: TransactionContextRoot { return self }

    override func beginTransaction() throws -> Transaction {
        guard !transactionIsActive else {
            throw TransactionError.anotherTransactionIsActive
        }

        let newTransaction = Transaction()

        _transaction = newTransaction

        propagateTransactionBegin()

        return newTransaction
    }

    override func commitTransaction(_ transaction: Transaction) throws {
        guard transaction == Transaction.any || transaction == _transaction else {
            throw TransactionError.wrongTransactionDescriptor
        }

        guard transactionIsActive else {
            throw TransactionError.transactionIsNotActive
        }

        try validateCommit()

        propagateTransactionCommit()

        _transaction = nil
    }

    override func rollbackTransaction(_ transaction: Transaction) throws {
        guard transaction == Transaction.any || transaction == _transaction else {
            throw TransactionError.wrongTransactionDescriptor
        }

        guard transactionIsActive else {
            throw TransactionError.transactionIsNotActive
        }

        propagateTransactionRollback()

        _transaction = nil
    }

}

TransactionContextNode

final class TransactionContextNode : TransactionContext {

    override var activeTransaction: Transaction? { return root.activeTransaction }
    private unowned let _root: TransactionContextRoot
    override var root: TransactionContextRoot { return _root }

    init(owner: Transactable, parent: Transactable) {
        let parentTransactionContext = parent.transactionContext

        _root = parentTransactionContext.root

        super.init(owner: owner)

        parentTransactionContext.register(node: self)

        if parentTransactionContext.transactionIsActive {
            propagateTransactionBegin()
        }
    }

    override func beginTransaction() throws -> Transaction {
        return try root.beginTransaction()
    }

    override func commitTransaction(_ transaction: Transaction) throws {
        try root.commitTransaction(transaction)
    }

    override func rollbackTransaction(_ transaction: Transaction) throws {
        try root.rollbackTransaction(transaction)
    }

}

WeakWrapper

struct WeakWrapper<Object: AnyObject> : Equatable, Hashable {

    let hashValue: Int

    private (set) weak var object: Object? = nil

    init(_ object: Object) {
        self.object = object
        self.hashValue = ObjectIdentifier(object).hashValue
    }

    static func == (lhs: WeakWrapper, rhs: WeakWrapper) -> Bool {
        return lhs.object === rhs.object
    }

}

UUID

extension UUID {

    static let zero = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!

}

mustOverride

func mustOverride() -> Never {
    fatalError("Must override this method or computed property")
}

Note: The repository of the framework (with documentation) is available here.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.