Creational patterns

The final type of design patterns we will discuss is called creational patterns. These patterns relate to the initialization of new objects. At first, the initialization of an object probably seems simple and not a very important place to have design patterns. After all, we already have initializers. However, in certain circumstances, creational patterns can be extremely helpful.

Singleton/shared instance

The first patterns we will discuss are the singleton and shared instance patterns. We are discussing them together because they are extremely similar. First we will discuss shared instance, because it is the less strict form of the singleton pattern.

The idea of the shared instance pattern is that you provide an instance of your class to be used by other parts of your code. Let's look at a quick example of this in Swift:

class AddressBook {
    static let sharedInstance = AddressBook()

    func logContacts() {
        // ...
    }
}

Here, we have a simple address book class but we are providing a static constant called sharedInstance that any other code can use without having to create its own instance. This is a very convenient way to allow otherwise separate code to collaborate. Instead of having to pass around a reference to the same instance all over your code, any code can refer the shared instance right through the class itself:

AddressBook.sharedInstance.logContacts()

Now, the different thing about the singleton pattern is that you would write your code in such a way that it is not even possible to create a second instance of your class. Even though our preceding address book class provides a shared instance, there is nothing to stop someone from creating their own instance using the normal initializers. We could pretty easily change our address book class to a singleton instead of a shared instance, as shown:

class AddressBook {
    static let singleton = AddressBook()

    private init() {}
    
    func logContacts() {
        // ...
    }
}

AddressBook.singelton.logContacts()

Besides changing the name of the static constant, the only difference with this code is that we declared the initializers as private. This makes it so that no code outside of this file can use the initializer and therefore, no code outside of this file can create a new instance.

The singleton pattern is great for when multiple instances of the same class are going to cause a problem. This is especially important for classes that represent a finite physical resource but it can also be a way to simplify a class that would be more difficult and unnecessary to implement in a way that would allow multiple instances. For example, there isn't actually much of a reason to ensure there is only ever one address book in an application. Perhaps the user will want to have two address books: one for business and one for personal. They should be able to operate independently as long as they are working from a different file, but maybe in your application you know that there will only ever be a single address book and it always has to be driven by a single file. Instead of requiring your code to create an address book with a specific file path, and instead of dealing with the danger of having multiple instances reading and writing to the same file, you can use the singleton version above and have the file path be fixed.

In fact, the singleton and shared instance patterns are so convenient that many developers over use them. So let's discuss some of the drawbacks of these patterns. It is nice to be able to access an instance from anywhere, but when it is easy to do so, it is also easy to create a very complex web of dependencies on that object. That goes against the principle of low coupling that we are trying to achieve. Imagine trying to change a singleton class when you have 20 different pieces of code all using it directly.

Using these patterns can also create hidden dependencies. Usually, it is pretty clear what dependencies an instance has based on what it must be initialized with, but a singleton or shared instance does not get passed into the initializer, so it can often go unnoticed as a dependency. Even though there is some initial extra overhead to passing an object into an initializer, it will often reduce the coupling and maintain a clearer picture of how your types interact. The bottom line is, like with any other pattern, think carefully about each use of the singleton and shared instance patterns and be sure it is the best tool for the job.

Abstract factory

The final pattern we will discuss here is called abstract factory. It is based on a simpler pattern called factory. The idea of a factory pattern is that you implement an object for creating other objects, much like you would create a factory for assembling cars. The factory pattern is great when the initializing of a type is very complex or you want to create a bunch of similar objects. Let's take a look at the second scenario. What if we were creating a two-player ping-pong game and we had some scenario in the game where we would add additional balls that a specific player needed to keep in play? The ball class might look something like this:

struct Ball {
    let color: String
    let owningPlayer: Int
}

Every time we needed a new ball we could assign a new color and owning player to it. Or, we could create a separate ball factory for each player:

struct BallFactory {
    let color: String
    let owningPlayer: Int

    func createNewBall() -> Ball {
        return Ball(
            color: self.color,
            owningPlayer: self.owningPlayer
        )
    }
}

let player1Factory = BallFactory(
    color: "Red", owningPlayer: 1
)
let player2Factory = BallFactory(
    color: "Green", owningPlayer: 1
)

let ball1 = player1Factory.createNewBall()

Now, we could pass this factory into whatever object is responsible for handling the ball creation event and that object is no longer responsible for determining the color of the ball or any other properties we might want. This is great for reducing the number of responsibilities that object has and also keeps the code very flexible to add additional ball properties in the future without having to change the ball creation event object.

An abstract factory is a special form of factory where the instances the factory creates may be one of many subclasses of a single other class. A great example of this would be an image creation factory. As we discussed in Chapter 3, One Piece at a Time – Types, Scopes, and Projects, computers have an enormous number of ways to represent images. In that chapter we hypothesized having a superclass called just "Image" that would have a subclass for each type of image. This would help us write classes to handle any type of image very easily by always having them work with the image superclass. Similarly, we could create an image factory that would virtually eliminate any need for an external type to know anything about the different types of images. We could design an abstract factory that takes the path to any image, loads the image into the appropriate subclass, and returns it simply as the image superclass. Now, neither the code that loads an image, nor the code that uses the image, needs to know what type of image they are dealing with. All of the complexity of different image representations is abstracted away inside the factory and the image class hierarchy. This is a huge win for making our code easier to understand and more maintainable.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset