This time I would like to talk a little bit about one more generating design pattern from the Gang of Four arsenal - the Builder . It turned out that in the course of getting my own (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. But in “iOS” projects, whether they are written in “Swift” or “Objective-C” , the template was rarely seen by me. Nevertheless, with all its simplicity, in appropriate cases, it can be quite convenient and, as is fashionable to say, powerful.
The template is used to replace the complex initialization process by constructing the desired object step by step, with a finalizing method call at the end. Steps can be optional and should not have a strict call sequence.
In cases when the necessary “URL” is not fixed, but constructed from the components (for example, the host address and relative path to the resource), you probably used the convenient URLComponents
mechanism from the “Foundation” library.
URLComponents
are, for the most part, just a class that combines a number of variables that store the values ​​of certain URL components, as well as the url
property, which returns the corresponding URL set to the current set of components. For example:
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 essence, the above example of use is the implementation of the “Builder” pattern. In this case, URLComponents
acts as the builder itself, assigning values ​​to its various properties ( scheme
, host
, etc.) is the initialization of the future object URLComponents
, and calling the url
property is a sort of finalizing method.
In the comments, there were heated battles about “RFC” documents describing the “URL” and “URI” , therefore, to be more precise, I suggest for example to assume that we are talking only about the “URL” of remote resources, and do not take into account such “URL” schemes, like, say, “file”.
It seems to be all right, if you use this code infrequently and know all its subtleties. But what if you forget something? For example, is anything important like a host address? What do you think will be the result of 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 url
returns an optional value, so maybe we get nil
? No, we get a fully-fledged URL
type object with a meaningless value - “https: / some / path”. Therefore, it occurred to me to practice writing my own “builder” on the basis of the “API” described above.
Despite the above, I consider URLComponents
good and convenient “API” for building a “URL” from components and, on the contrary, for “parsing” the components of the well-known “URL”. Therefore, based on it, we will now write our own type that collects the “URL” from parts and that has (suppose) the “API” we need at the moment.
First, I want to get rid of scattered initialization by assigning new values ​​to all the necessary properties. Instead, we implement the ability to create an instance of the builder and assign values ​​to all properties using the methods called along the chain. The chain ends with a finalizing method, the result of which will be called and the corresponding instance of the URL
. Perhaps you have encountered something like a “Java” StringBuilder
in your life path - we will strive for something like this “API” now.
To be able to call the methods-steps on the chain, each of them must return an instance of the current builder, within which the corresponding change will be stored. For this reason, as well as to get rid of multiple copying of objects and from dancing around mutating
methods, especially without thinking, we will declare our builder a class :
final class URLBuilder { }
Let's declare the methods that set the parameters of the future “URL”, taking into account the requirements listed above:
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 private properties of the class for further use by finalizing the method.
Another “API” tribute on which we base our class is the path
property, which, unlike all neighboring properties, is not optional, and in the absence of a relative path it stores an empty string as its value.
To write this, actually, finalizing method, you need to think about a few more things. First, the “URL” has some parts, without which it, as indicated at the beginning, ceases to have meaning — it is a scheme
and a host
. First, we "awarded" the default value, so forgetting about it, we still get the expected result.
With the second, things are a bit more complicated: you cannot assign a default value to it. In this case, we have two ways: if there is no value for this property, either return nil
or throw an error and let the client code decide what to do with it. The second option is more difficult, but it will allow unambiguously to point out a specific programmer's error. Perhaps, for example, on this path we will go.
Another interesting point is related to the user
and password
properties: they only make sense if they are used simultaneously. But what if the programmer forgets to assign one of these two values?
And, probably, the last thing that needs to be taken into account is that we want to have the value of the url
property URLComponents
as a URLComponents
, and this, in this case, is not by the way optional. Although with any combination of the given values ​​of the nil
properties we will not get. (The value will be missing only for an empty, newly created, instance of URLComponents
.) To overcome this circumstance, you can use !
- operator "forced unwrapping". But in general, we would not like to encourage its use, so in our example we abstract a little from the knowledge of the finer points of the 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 } }
Here, perhaps, that's all! Now componentwise creation of the “URL” from the example at the beginning may 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, using try
outside a do
- catch
or without an operator ?
when an error occurs, causes the program to crash. But we have provided the “client” with the ability to handle errors as he deems necessary.
Yes, and another useful feature of step-by-step design 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!
The pattern is extremely simple to understand, and everything simple is, as you know, brilliant. Or vice versa? Nevermind. The main thing is that I, without turning my head, can say that it (the template) has already happened, it helped me to solve 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 get acquainted with it. (Although, of course, since then much water has flowed under the bridge, and other programmers were also attached to this code.)
My other posts about design patterns:
And this is my Twitter to satisfy a hypothetical interest in my public professional activity.
Source: https://habr.com/ru/post/457086/
All Articles