Static function encapsulation in Swift by passing Protocols.Type better than OO encapsulation and just as testable?

Given that I have a function that does not need to share and store state; should I use a static class/struct/enum to hold the function? I have read in many places that it is a bad design to use static functions to hold code, as static function do not adhere to the SOLID principles and are considered procedural code. Testability seems to be there as I can isolate the parent class with the injected static Enums by injecting mock static enums.

E.g. I can encapsulate and have polymorphism by using protocols for static functions:

Static Protocol Approach

enum StaticEnum: TestProtocol {
    static func staticMethod() {
        print("hello")
    }
}

enum StaticEnum2: TestProtocol {
    static func staticMethod() {
        print("hello2")
    }
}

protocol TestProtocol {
    static func staticMethod()
}

class TestClass {
    let staticTypes: [TestProtocol.Type]
    init (staticTypes: [TestProtocol.Type]) {
        self.staticTypes = staticTypes
    }
}

class TestFactory {
    func makeTestClass() -> TestClass {
        return TestClass(staticTypes: [StaticEnum.self, StaticEnum2.self])
    }
}

vs

Object Oriented Approach

class InstanceClass: TestProtocol {
    func instanceMethod() {
        print("hello")
    }
}

class InstanceClass2: TestProtocol {
    func instanceMethod() {
        print("hello2")
    }
}

protocol TestProtocol {
    func instanceMethod()
}

class TestClass {
    let instances: [TestProtocol]
    init (instances: [TestProtocol]) {
        self.instances = instances
    }
}

class TestFactory {
    func makeTestClass() -> TestClass {
        return TestClass(instances: [InstanceClass(), InstanceClass2()])
    }
}

The static version still allows for protocol polymorphism as you can have multiple enums adhere to the static protocols. Furthermore no initialisation is needed after the first dispatch call to create the static function. Is there any drawback in using the Static Protocol approach?

As there is no state, I would just go for the simpler method, the static-function method.

Whether true or not, according to Google: Leonardo Da Vinci - Simplicity is the best sophistication. :slight_smile:

1 Like

I would suggest using nothing - you can inject functions without wrapping them in anything

func method1() {
    print("hello")
}

func method2() {
    print("hello2")
}

class TestClass {
    let staticTypes: [() -> Void]
    init (staticTypes: [() -> Void]) {
        self.staticTypes = staticTypes
    }
}

class TestFactory {
    func makeTestClass() -> TestClass {
        return TestClass(staticTypes: [method1, method2])
    }
}

does this break any other programming principles for instance SOLID?

I actually thought about this. Would Protocols give you added polymorphism and protocol type safety instead of injecting generic closures

Additionally, I forgot to add that other languages like C++ could already pass static functions around as well. Yet I have never seen any book or framework advocate passing static functions this way. Would you happen to know why that is?

If this is for your tests, then I don't see a problem. Either the tests pass which means the thing you passed is a correct thing to pass, or they fail and you get a message about that. I cannot imagine a situation where you get incorrect behavior.

I have never seen any book or framework advocate passing static functions this way. Would you happen to know why that is

Probably because that's the default, the simplest solution. If you want to pass a function somewhere, you just pass a function somewhere, no additional steps required.

The opposite question must be asked: Why are the books recommending encapsulation? Do you agree with these reasons? Are the reasons still relevant in your code? (I have no idea what are the answers to these questions, I never read programming books because they don't fit my style of learning)

I am not familiar with SOLID, but your method is rock solid.

I agree with @cukr: Just pass the functions directly into the testing code. I fail to see the purpose of encapsulating the functions in types and defining a protocol. All of that additional boilerplate just makes your code unnecessarily complex.

Remember: Swift isn't like java; you can create global functions without defining a class/struct/enum.

1 Like

I would encourage not getting too hung up on "best practices". The main thing they're useful for is as an entry point to learning why and in what circumstances they're best, and then discarding the rule-based approach in favor of a more flexible understanding-based approach.

8 Likes

Here's how I would test pure functions:

func method1() {
    print("hello")
}

func method2() {
    print("hello2")
}


class TestClass: XCTestCase {
    
    func testMethod1() {
        method1()
    }
    
    func testMethod2() {
        method2()
    }
    
}

That's it. All of this abstract theorizing about the best design principles is making your life unnecessarily complex.

5 Likes

There's a more precise point to make here that I should've noticed earlier:

SOLID principles are a set of rules for how to write object-oriented code. They don't tell you that you should use object-oriented code, and pure functions aren't object-oriented code.

Therefore, to say that "[pure] functions do not adhere to the SOLID principles" is a simple category error: SOLID principles don't apply to non-object-oriented code.

I'm sorry for posting this so late, but this is important for you to understand so that you don't go around applying these principles to other non-object-oriented code. In particular, you should carefully consider whether you need to use object-oriented programming at all before you start thinking about the principles that tell you how to write it.