This article introduces a design pattern that I call Concealment, and demonstrates how to use the pattern in Swift code.
The Concealment pattern enables types to support novel, related functionality without adding nonessential members to their interface.
User-defined types can easily be overused in a codebase, giving more prominence to things than necessary. A common example is having a data model entity be involved with tangentially related concerns; such as validation checks, business rule processing, and display formatting. It is preferable to conceal the augmentation and usage of your types behind an API expressly built for a single responsibility, rather than expose those implementation details to all the types’ consumers.
Swift implementation advice
Create a new type that represents the task to be performed. In the new type’s file, add a private extension for each of your pre-existing types whose participation in the task is being concealed. Pass an instance of the new type into the private extension methods, as a form of context, if necessary.
Swift Tip: private type members can be accessed from anywhere in the same file.
This demo project is available on GitHub. It is an iOS app that converts text to Morse code and plays it out loud, inspired by a programming challenge described here.
The Morse code data model is simple, and I wanted it to stay this way.
The app converts the user’s text input into instances of this abstraction. It must then transform that abstraction into two consumable formats:
- A string filled with dots, dashes, and separators so that the user can view the message they typed in as Morse code.
- A sequence of on/off states with relative durations so that the Morse code message can be played to the user.
It can be tempting to add methods or properties to the data model types to support such transformations, but doing so would reduce the clarity and simplicity of the data model. Instead, let’s conceal the data model’s participation in these transformation tasks, treating their involvement in the task as a mere implementation detail.
Here is how an
EncodedMessage is transformed to a string filled with Morse code dots and dashes.
Similarly, this is how the same
EncodedMessage becomes a sequence of on/off states suitable for transmission (a.k.a. playback).
MorseTransformation<T> struct is responsible for projecting a Morse encoded message into another format. However, as seen below, it merely contains the parameters for a transformation and passes itself to the data model, which does the heavy lifting.
Since all of the data model extensions are private, the rest of the codebase does not know about or have access to the methods added to support transformations. Usage of the data model to support this novel functionality is an implementation detail.
The Concealment pattern is only applicable in languages that support private type extensions, or something equivalent. Thanks to Swift’s thoughtful design, it is easy for Swift developers to apply this pattern and keep their types simple.