Concealment design pattern in Swift

This article introduces a design pattern that I call Concealment, and demonstrates how to use the pattern in Swift code.

Purpose

The Concealment pattern enables types to support novel, related functionality without adding nonessential members to their interface.

Motivation

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.

Example

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.


/// Represents an entire Morse encoded message.
struct EncodedMessage { let encodedTerms: [EncodedTerm] }
/// Represents a word or number consisting of Morse code symbols.
struct EncodedTerm { let symbols: [Symbol] }
/// Represents a character encoded with Morse code marks.
struct Symbol { let marks: [Mark] }
/// Represents an individual component of a Morse code symbol.
enum Mark: String { case Dot = ".", Dash = "-" }

view raw

Model.swift

hosted with ❤ by GitHub

The app converts the user’s text input into instances of this abstraction. It must then transform that abstraction into two consumable formats:

  1. A string filled with dots, dashes, and separators so that the user can view the message they typed in as Morse code.
  2. 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.


func createMorseCodeText(from encodedMessage: EncodedMessage) -> String {
let transformation = MorseTransformation(
dot: ".",
dash: "-",
markSeparator: "",
symbolSeparator: " ",
termSeparator: "\n")
let characters = transformation.apply(to: encodedMessage)
return characters.joinWithSeparator("")
}

Similarly, this is how the same EncodedMessage becomes a sequence of on/off states suitable for transmission (a.k.a. playback).


enum TransmissionState {
typealias RelativeDuration = Int
case On(RelativeDuration)
case Off(RelativeDuration)
static func createStates(from encodedMessage: EncodedMessage)
-> [TransmissionState] {
let transformation = MorseTransformation(
dot: TransmissionState.On(1),
dash: TransmissionState.On(3),
markSeparator: TransmissionState.Off(1),
symbolSeparator: TransmissionState.Off(3),
termSeparator: TransmissionState.Off(7))
return transformation.apply(to: encodedMessage)
}
}

The 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.


/// Converts an `EncodedMessage` to an alternate representation.
struct MorseTransformation<T> {
let dot, dash, markSeparator, symbolSeparator, termSeparator: T
func apply(to encodedMessage: EncodedMessage) -> [T] {
return encodedMessage.apply(self)
}
}
private extension EncodedMessage {
func apply<T>(transformation: MorseTransformation<T>) -> [T] {
return encodedTerms
.map { $0.apply(transformation) }
.joinWithSeparator([transformation.termSeparator])
.toArray()
}
}
private extension EncodedTerm {
func apply<T>(transformation: MorseTransformation<T>) -> [T] {
return symbols
.map { $0.apply(transformation) }
.joinWithSeparator([transformation.symbolSeparator])
.toArray()
}
}
private extension Symbol {
func apply<T>(transformation: MorseTransformation<T>) -> [T] {
return marks
.map { $0.apply(transformation) }
.joinWithSeparator([transformation.markSeparator])
.toArray()
}
}
private extension Mark {
func apply<T>(transformation: MorseTransformation<T>) -> [T] {
return [self == .Dot ? transformation.dot : transformation.dash]
}
}
private extension JoinSequence {
func toArray() -> [Base.Generator.Element.Generator.Element] {
return Array(self)
}
}

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.

This entry was posted in Swift and tagged . Bookmark the permalink.

1 Response to Concealment design pattern in Swift

  1. Pingback: Dew Drop - July 19, 2016 (#2290) - Morning Dew

Comments are closed.