Architectural pattern "Builder" in the universe of "Swift" and "iOS" / "macOS"

    This time I would like to talk a little about another generative design pattern from the Gang of Four arsenal - “Builder” . It turned out that in the process of getting my (albeit not too extensive) experience, I quite often saw that the pattern was used in the "Java" code in general and in the "Android" applications in particular. In "iOS" projects, whether they were written in "Swift" or "Objective-C" , the pattern was quite rare for me. Nevertheless, with all its simplicity, in suitable cases, it can turn out to be quite convenient and, as it is fashionable to say, powerful.


    image


    The template is used to replace the complex initialization process by constructing the desired object step by step, with the finalizing method called at the end. The steps may be optional and should not have a strict call sequence.


    image


    Foundation example


    In cases where the desired "URL" is not fixed, but is constructed from components (for example, the host address and the relative path to the resource), you probably used the convenient mechanism URLComponentsfrom the "Foundation" library .


    URLComponents- this is, for the most part, just a class that combines many variables that store the values ​​of certain URL components, as well as a property urlthat returns the appropriate URL for the current set of components. For instance:


    var urlComponents = URLComponents()
    urlComponents.scheme = "https"
    urlComponents.user = "admin"
    urlComponents.password = "qwerty"
    urlComponents.host = "somehost.com"
    urlComponents.port = 80
    urlComponents.path = "/some/path"
    urlComponents.queryItems = [URLQueryItem(name: "page", value: "0")]
    _ = urlComponents.url
    // https://admin:qwerty@somehost.com:80/some/path?page=0

    In fact, the above use case is the implementation of the Builder pattern. URLComponentsin this case, it plays the role of the builder proper, assigning values ​​to its various properties ( scheme, hostetc.) is the initialization of the future object in steps, and calling the property urlis like a finalizing method.


    In the comments, heated battles unfolded about the “RFC” documents describing the “URL” and “URI” , therefore, to be more precise, I suggest, for example, that we are talking only about the “URL” of remote resources, and do not take into account such "URL" schemes, like, say, "file".


    Everything seems to be fine if you use this code infrequently and know all its subtleties. But what if you forget something? For example, anything important like a host address? What do you think will be the result of executing the following code?


    var urlComponents = URLComponents()
    urlComponents.scheme = "https"
    urlComponents.path = "/some/path"
    _ = urlComponents.url

    We work with properties, not methods, and no errors will be "thrown out" for sure. The “finalizing” property urlreturns an optional value, so maybe we get it nil? No, we get a fully-fledged type object URLwith a meaningless value - “https: / some / path”. Therefore, it occurred to me to practice writing my own “builder” based on the “API” described above .


    (There should have been an "emoji" "bicycle", but "Habr" does not display it)


    Despite the above, I consider it a URLComponentsgood and convenient "API" for assembling a "URL" from the constituent parts and, conversely, "parsing" the constituent elements of the famous "URL". Therefore, based on it, we now write our own type that collects the "URL" from the parts and possesses (suppose) the "API" we need at the moment.


    Firstly, I want to get rid of disparate initialization by assigning new values ​​to all the necessary properties. Instead, we implement the possibility of creating an instance of the builder and assigning values ​​to all properties using methods called by the chain. The chain ends with a finalizing method, the result of the call of which will be the corresponding instance URL. Perhaps you have encountered something like StringBuilder“Java” on your life journey - we will strive for something like that “API” now.


    In order to be able to call methods-steps along the chain, each of them must return an instance of the current builder, inside which the corresponding change will be stored. For this reason, and also to get rid of multiple copying of objects and from dancing around the mutatingmethods, especially without thinking, we will declare our builder a class :


    final class URLBuilder { }

    We’ll declare methods that specify the parameters of the future “URL”, taking into account the above requirements:


    final class URLBuilder {
        private var scheme = "https"
        private var user: String?
        private var password: String?
        private var host: String?
        private var port: Int?
        private var path = ""
        private var queryItems: [String : String]?
        func with(scheme: String) -> URLBuilder {
            self.scheme = scheme
            return self
        }
        func with(user: String) -> URLBuilder {
            self.user = user
            return self
        }
        func with(password: String) -> URLBuilder {
            self.password = password
            return self
        }
        func with(host: String) -> URLBuilder {
            self.host = host
            return self
        }
        func with(port: Int) -> URLBuilder {
            self.port = port
            return self
        }
        func with(path: String) -> URLBuilder {
            self.path = path
            return self
        }
        func with(queryItems: [String : String]) -> URLBuilder {
            self.queryItems = queryItems
            return self
        }
    }

    We save the specified parameters in the private properties of the class for future use by the finalizing method.


    Another tribute to the “API” on which we base our class is a property paththat, unlike all neighboring properties, is not optional, and if there is no relative path, it stores an empty string as its value.


    To write this, in fact, finalizing method, you need to think about a few more things. Firstly, «URL» has several parts, without which it is, as has been indicated in the beginning, it ceases to make sense - it's schemeand host. We “awarded” the first one with the default value, therefore, having forgotten about it, we will still receive, most likely, the expected result.


    With the second, things are a little more complicated: it cannot be assigned some default value. In this case, we have two ways: in the absence of a value for this property, either return nilor throw an error and let the client code decide for itself what to do with it. The second option is more complicated, but it will allow you to explicitly indicate a specific programmer error. Perhaps, for example, we will go along this path.


    Another interesting point related to the properties userand passwordthey make sense only if they are used simultaneously. But what if a programmer forgets to assign one of these two values?


    And, probably, the last thing to consider is that the result of the finalizing method we want to have the value of the property urlURLComponents, and this, in this case, is not very appropriate is optional. Although with any combination of property values nilwe won’t get. (The value will be absent only in the empty, just created, instance URLComponents.) To overcome this circumstance, you can use !the “forced unwrapping” operator. But in general, I would not want to encourage its use, therefore, in our example, we briefly abstract from the knowledge of the subtleties of “Foundation” and consider the situation under discussion as a system error, the occurrence of which does not depend on our code.


    So:


    extension URLBuilder {
        func build() throws -> URL {
            guard let host = host else {
                throw URLBuilderError.emptyHost
            }
            if user != nil {
                guard password != nil else {
                    throw URLBuilderError.inconsistentCredentials
                }
            }
            if password != nil {
                guard user != nil else {
                    throw URLBuilderError.inconsistentCredentials
                }
            }
            var urlComponents = URLComponents()
            urlComponents.scheme = scheme
            urlComponents.user = user
            urlComponents.password = password
            urlComponents.host = host
            urlComponents.port = port
            urlComponents.path = path
            urlComponents.queryItems = queryItems?.map {
                URLQueryItem(name: $0, value: $1)
            }
            guard let url = urlComponents.url else {
                throw URLBuilderError.systemError // Impossible?
            }
            return url
        }
        enum URLBuilderError: Error {
            case emptyHost
            case inconsistentCredentials
            case systemError
        }
    }

    That's all, perhaps! Now an exploded creation of the “URL” from the example at the beginning might look like this:


    _ = try URLBuilder()
        .with(user: "admin")
        .with(password: "Qwerty")
        .with(host: "somehost.com")
        .with(port: 80)
        .with(path: "/some/path")
        .with(queryItems: ["page": "0"])
        .build()
    // https://admin:Qwerty@somehost.com:80/some/path?page=0

    Of course, use tryoutside the block docatchor without an operator ?when an error occurs will cause the program to crash. But we provided the “client” with the opportunity to handle errors as he sees fit.


    Yes, and another useful feature of step-by-step construction using this template is the ability to place steps in different parts of the code. Not the most frequent case, but nonetheless. Thanks akryukov for the reminder!


    Conclusion


    The template is extremely easy to understand, and everything simple is, as you know, ingenious. Or vice versa? Nevermind. The main thing is that I, without a twitch of my soul, I can say that it (the template), already happened, helped me out in solving problems of creating large and complex initialization processes. For example, the process of preparing a session of communication with the server in the library, which I wrote for one service almost two years ago. By the way, the code is “open source” and, if desired, it is quite possible to familiarize yourself with it . (Although, of course, since then a lot of water has flowed, and other programmers have applied to this code.)


    My other posts on design patterns:



    And this is my “Twitter” to satisfy a hypothetical interest in my public-professional activity.


    Also popular now: