Chapter 7. Case Studies

Ever since I bought my first computer, a Commodore Vic-20, at the age of thirteen, I have been continuously learning new computer languages. I cannot count the number of computer languages that I have learned through the years on my fingers and toes. Even when I was working as a network engineer and security specialist, I learned languages such as Perl, Python, PHP, and Shell scripting to automate my administrative tasks. Learning all of those languages has taught me that the best way to learn a new programming language or a programming paradigm is to come up with several small projects and figure out how to implement them in the language or programming paradigm that I was trying to learn. With that in mind, in this chapter we will take three small projects and see how we would implement them using Swift and the protocol-oriented programming paradigm. I would recommend that you read the requirements for the project and then try to implement your own solution prior to reading our solution. Then you can compare your solution to ours. This will help you to begin thinking about application design using the protocol-oriented programming paradigm. I do have one hint for you: don't forget to use design patterns in your solutions for these projects.

In this chapter you will learn about the following topics:

Protocol-oriented programming isn't just about using protocols, protocol extensions, or value types. It is about how we code and architect our applications and frameworks. Protocol-oriented programming with design patterns focuses on ensuring that our code base can be easily expanded and maintained.

Focusing on how we will maintain and expand our applications and frameworks in the future may sometimes seem like a waste of time with the tight deadlines we are always on. However in the long term, the time spent on making sure our code base is easily expandable and maintainable always ends up saving us time because requirements will change and new features will be added. We need to ensure our code base is prepared for these changes.

In this chapter we will be looking at three real-world case studies to see how we can apply the protocol-oriented paradigm with design patterns to write solutions for real-world projects. These three case studies are:

In the first case study, we will look at how we would use the protocol-oriented programming paradigm to create a logging framework that can be easily maintained and expanded by our users. In the second case study we will see how we can use the protocol-oriented programming paradigm to add a SQLite database to our application using the SQLite.swift framework. The final case study will show us how we can use the protocol-oriented programming paradigm to create a very basic calculator that we can easily expand to add additional functionality.

Let's start by looking at how we can create a logging service for our application.

If I counted the lines of code that I have written and used in each language over the course of my life it would probably show that Java is the language that I have used the most. Java definitely has its good and bad points but one of the things that I really liked about developing applications in Java is all of the different logging frameworks that are available for it.

These logging frameworks make it incredibly easy to turn on log messages to make debugging very easy while we are developing our application. We can then turn those debugging messages off when it is time to build the production release of our application. To do this, these logging frameworks let us define how and where we wish to log messages for predefined log levels. We can also set them to ignore certain logging levels if we do not need them. The log levels ranged from info (used purely for debugging) all the way up to fatal (something really bad happened).

Some of the logging frameworks that I have used with Java are Log4j, Java Logging API, and Apache Commons Logging. For this project we will create a logging service similar to these but with the Swift language. The first thing we need to do is to define the requirements for our logging service.

We are going to break our design into two sections. The first section will be the logger profile section that will contain the types that do the actual logging of the messages to a storage medium or display them. The second section will be the logger section that will contain the types that our applications interface with. The logger types will determine the log level of the message and then pass the message to the appropriate logger profiles to log the message. Let's start off by looking at the logger profile section.

We will begin the logger profile design by creating a protocol named LoggerProfileProtocol. This protocol will define the interface that our logger profile will present and any type that conforms to this protocol can be used to log messages. Our framework, out of the box, will provide two types that conform to the LoggerProfileProtocol protocol. These types are the LoggerNull and LoggerConsole types.

By starting our design with the LoggerProfileProtocol protocol and using the interface exposed by the protocol to write log messages to the display/storage medium, we are implementing polymorphism. This will allow users of our framework to add additional logging profiles, as they need. To add these additional logging profiles all our uses need to do is to create additional types that conform to the LoggerProfileProtocol protocol. These types could provide a means to log messages to any display or storage medium that meets their requirements such as a database, file, or even a UILabel.

Let's look at a diagram that shows how we implement the LoggerProfileProtocol protocol design. The diagram shows that we extend the LoggerProfileProtocol protocol to add a method named getCurrentDateString(). This method will return a formatted string that contains the current date and time. The diagram also shows the LoggerNull and LoggerConsole types that conform to the LoggerProfileProtocol protocol:

The design

Let's look at how we implement this design. We will start off by looking at the LoggerProfileProtocol protocol and the LoggerProfileProtocol protocol extension:

The LoggerProfileProtocol protocol defines one property and one function. The property is named loggerProfileId, which is of the String type. This property is used to uniquely identify the logging profile. We will use this property in our framework to ensure that we do not add the logger profile to any log level more than once. The method defined by our protocol is named writeLog() and will be called to write the log message to the display or storage medium defined by the profile.

We created a protocol extension to the LoggerProfileProtocol protocol to add a method named getCurrentDateString(). This method returns a formatted date string of the current date and time. While types that conform to the LoggerProfileProtocol protocol can elect to not use the getCurrentDateString() method provided by the protocol extension, it is recommended that they do to ensure that all logger profile types provide a date and time string with a consistent format.

Now let's look at the LoggerNull and LoggerConsole types:

Both logger profiles have a unique id defined in the loggerProfileId constant. We use a reverse DNS notation as the format for this id. Reverse DNS notation is a naming convention that is commonly used to name components, packages, and other types. A reverse DNS notation string is usually based on a registered domain name but the names are in reverse order. For these examples I am using my name rather than a registered domain name.

In both types we also provide an implementation for the writeLog() method that is required by the LoggerProfileProtocol protocol. For the LoggerNull type the writeLog() method does not do anything because this type is designed to not actually write the log anywhere. The LoggerConsole type retrieves a string that represents the current date and time using the getCurrentDateString() method provided by the LoggerProfileProtocol protocol extension and then writes the log message to the console.

The next part of our logger service will be the logger type which will keep track of what logger profiles are assigned to the various log levels. Our applications will primarily use the interface provided by the logger type as the means to configure the logger service and also to log messages.

The logger types will conform to the LoggerProtocol protocol. While the initial design of our framework will only contain one logger type, we will still want to begin the design with the protocol to give us the ability to add additional logger types in the future. It will also give our users the ability to add additional logger types if they need to. It may not seem like that big a deal right now but, two or three years down the road, as requirements change and new features are added, we will be glad that we used the protocol. Trust me on this one.

The first thing we need to do is to define the log levels that our framework will offer. We will use an enumeration to define these levels since there is a finite number of log levels. The following enumeration defines the log levels for our logging framework:

The LogLevels enumeration defines five log levels. We also provide an array that contains all five levels. This array can be used to retrieve all values in the enumeration if needed. Now let's look at our LoggerProtocol protocol:

The LoggerProtocol protocol defines one property named loggers. This property is defined to be of the Dictionary type whose key is a log level that is defined in the LogLevels enumeration and whose value is an array of types that conform to the LoggerProfileProtocol protocol. We also define one method in our LoggerProtocol protocol named writeLog() . This method is called within our applications to write a message to the logs and it takes two arguments. The first argument is the log level to write the log message for and the second is the log message itself.

We define both the loggers property and the writeLog() method as static so we can access them without having to create an instance of the LoggerProtocol type. Properties and methods that are defined with the static keyword are known as type properties and type methods. Another reason we create these as static is that one of the requirements for our logger framework is to be able to configure the framework once and have the ability to keep that configuration throughout the life cycle of our application. By creating these as static there will be one (and only one) instance of them throughout the life cycle of our application thereby fulfilling this requirement. We could have used the singleton pattern to fulfill this requirement; however using type methods/properties would seem to work better in this situation. We will see the advantage of this when we look at how we will use our logging framework.

Now let's look at the methods that our LoggerProtocol protocol extension provides to types that conform to the LoggerProtocol protocol. Our protocol extension will provide six type (static) methods:

Now let's look at the code for the individual methods. We will start off by looking at the logLevelContainsProfile() method.

This method will return true if the log level contains the logger profile. This method is used by the setLogeLevel() and addLogProfileToAllLevels() method to ensure that we do not add a logger profile to a log level more than once. We begin this method by using optional binding to retrieve a list of logger profiles assigned to the log level. We then use the for-in statement with the where clause to loop through the list of logger profiles where the loggerProfileId property matches the loggerProfileId property of the profile we are looking for. If any item in the array matches the loggerProfileId property of the profile we are looking for we return true; otherwise we return false.


The next method we will look at is the setLogLevel() method.

The setLogLevel() method will use the logLevelContainsProfile() method to verify that the logger profile is not already assigned to the specified log level; if it isn't then it will add the logger profile to that level. We begin this method by using optional binding to retrieve the list of logger profiles assigned to the log level. We do this simply to verify that there is a valid array assigned to the log level. We then use the logLevelContainsProfile() method to verify that the logger profile is not already assigned to the level and, if not, we add it.

If the optional binding fails at the start of the method then we create a new array, add the logger profile to it, and assign the array to the log level within the loggers dictionary.

Now let's look at the addLogProfileToAllLevels() method.

The addLogProfileToAllLevels() method is used to add a logger profile to all log levels. This can be used to initialize the logger framework by adding a single profile to all levels. In this method we loop through each of the log levels and then call the setLogLevel() method to try to add the logger profile to each of the individual log levels.

The next method we will look at is the removeLogProfileFromLevel() method.

The removeLogProfileFromLevel() method will remove the logger profile from the specified log level. This method starts off by using optional binding to retrieve the list of logger profiles for the log level. We then use the indexOf() method to locate the index of the logger profile that matches the logger profile that we want to remove (it will return nil if it cannot find a match). If we find the profile we then remove it.

The next method is the removeLogProfileFromAllLevels() method.

The removeLogProfileFromAllLevels() method will attempt to remove a logger profile from all log levels. This method will loop through all of the log levels that we have defined and call the removeLogProfileFromLevel() method in an attempt to remove the logger profile from the level.

The final method in our LoggerProtocol protocol extension is the haveLoggersForLevel() method:

The haveLoggersForLevel() method returns true if the log level contains any logger profiles; otherwise, it returns false. This method uses optional binding with a guard statement to retrieve the list of logger profiles assigned to the log level. If the optional binding statement fails then a false value is returned; otherwise a true value is returned.

Now let's look at our Logger type, which will conform to the LoggerProtocol protocol.

The Logger type is the type that we use in our application for configuration of our log levels and also for logging our messages. The Logger type conforms to the LoggerProtocol protocol by implementing the requirements defined by the protocol.

The Logger type has one property named loggers that is a dictionary whose key is a log level defined in the LogLevels enumeration and whose value is an array of types that conform to the LoggerProfileProtocol protocol. The writeLog() method is used within our applications to write a message to the log and it takes two arguments. The first argument is the log level to write the log message for and the second is the log message itself.

The writeLog() method checks to see if we have any log profiles assigned to the log level and if so we loop through the list of profiles and call the writeLog() method on each profile, passing it the message we wish to log.

We could now use our logger framework like this:

In this sample code, we begin by adding the LoggerConsole logger profile to all of our log levels. This will cause all log messages, no matter what the log level is, to be logged to the console. We then write a log message ( Debug Message 1) with the log level set to Debug. This log message will be printed to the console.

In the next couple of lines we add the LoggerDatabase logger profile (this would be a user created profile) to both the Error and Fatal log levels. The LoggerDatabase logger profile would be a profile that would log messages to a database such as SQLite and will need to conform to the LoggerProfileProtocol protocol. We then write the log message ErrorMessage 1 with the log level set to Error. This will write the log message to both the console and database since both logger profiles are assigned to the Error log level.

In this example, we can see that we do not need to create an instance of the Logger type to configure it or to log messages. The reason for this is that we made the properties and methods static (type methods and type properties). When we are creating frameworks that should have a very small footprint, for instance our logger framework, using type methods and properties to avoid instantiation of our types can make our frameworks significantly easier to use.