Type Identity and its Impact on API Design


Looking for Side Income? Have you heard of Affiliate Marketing? You obviously have a working Computer and Wi-Fi to get started. Click here to Register for FREE Training from one of the Best Super Affiliates out there


Author profile picture

When designing an API, it’s easy to use public types that denote resource identity to allow actions on a resource. As an example in the snippet below, we use Int to denote the identifier of the Storable type that the user of the interface wants to use to retrieve the value.

Option 1

protocol StoredCollection {
	associatedtype Storable: Codable
	func fetch(id: Int) -> Storable
}

It allows the caller to ask for any random id: 5, 6, 99, 105. At first, this does not appear to be a problem, all we need to do is to make an edit to the interface to indicate that we will return an optional value in case the identifier does not exist.

protocol StoredCollection {
	associatedtype Storable: Codable
	func fetch(id: Int) -> Storable?
}

Now, let’s add creation semantics into this collection, which could lead to the possibilities mentioned below.

Option 2

//Option 1: Interface user specifies the id
protocol StoredCollection {
	associatedtype Storable: Codable
	func create(id: Int, value: Storable)
	func fetch(id: Int) -> Storable?
}

//Option 2: Interface user just provides the value
protocol StoredCollection {
	associatedtype Storable: Codable
	func create(value: Storable) -> Int
	func fetch(id: Int) -> Storable?
}

Option 2, in my opinion, is more preferable. It indicates that the responsibility of creating the identifier for the storable type belongs to the StoredCollection implementation, whereas Option 1 puts that responsibility on the user of StoredCollection.

But what happens to Option 2 when we update the interface to introduce update and remove semantics.

protocol StoredCollection {
	associatedtype Storable: Codable
	func create(value: Storable) -> Int
	func update(id: Int, value: Storable)
	func remove(id: Int)
	func fetch(id: Int) -> Storable?
}

Now, we are once again allowing integers to be passed in to update and remove and fetch. But the user may accidentally end up using a random int type which has got nothing to do with the StoredCollection when invoking the method.

One way to circumvent this problem is by introducing IDTypes that are public to the outside world with respect to their existence, but whose creation is private to the framework that’s implementing StoredCollection.

protocol StoredCollection {
	associatedtype Storable: Codable
	func create(value: Storable) -> StoreIdentifier<Storable>
	func update(id: StoreIdentifier<Storable>, value: Storable)
	func remove(id: StoreIdentifier<Storable>)
	func fetch(id: StoreIdentifier<Storable>) -> Storable?
}

public struct StoreIdentifier<Storable: Codable> {
	private let value: Int

	fileprivate init(value: Int) {
		self.value = value
	}
}

In the example above, we replaced Int with StoreIdentifier. StoreIdentifier could be a public type with init being internal to the system implementing StoredCollection. By making this change, we can send the type across the system boundaries, but are also able to avoid usage confusion related to the user passing in an unrelated type as the identifier when using update, remove, and fetch.

It also prevents unnecessary operations on identifier types. As an example if we had retained identifiers as Int, the user could create a sum by adding two identifiers, which does not offer any meaning.

Making identifiers opaque to the users of the interface in this manner introduces additional types, but it also offers the benefit of retaining identifier semantics. Another benefit is that the internal identifiers can be changed to String from Int, without having any impact on the users of the interface.

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.



Source link

Related Posts