API design: initializer overloads vs static factory functions

Hello everyone! :wave:

I'm wondering what your preferred strategy would be when it comes to writing logic responsible for collecting a number of values and transforming those into a new type.

Would you reach for an initializer overload or a static factory function? Examples below.

Given the following types, how would you go about transforming a InventoryItemServerResponse into an InventoryItem?

struct InventoryItem { 
	let name: String
	let amount: Int
	let currencyCode: String
}

struct InventoryItemServerResponse { 
	let name: String
	let usdAmount: String
}

Option 1: Static factory function

extension InventoryItem { 
	static func make(
		from result: Result<InventoryItemServerResponse, Error>,
		locale: Locale
	) -> InventoryItem? { 
		switch result {
			case .failure: return nil

			case .success(let serverResponse):
				let currencyCode = locale.currencyCode
				let amount = MoneyHelper.localizeAmount(
					usdAmount: serverResponse.amount,
					currencyCode: currencyCode
				)

				return InventoryItem(
					name: serverResponse.name,
					amount: amount,
					currencyCode: currencyCode
				)
		}
	}
}

// Call site:
let inventoryItem = InventoryItem.make(from: result, locale: Locale.current)

Option 2: Initializer overload

extension InventoryItem { 
	init?(
		result: Result<InventoryItemServerResponse, Error>,
		locale: Locale
	) { 
		switch result {
			case .failure: return nil

			case .success(let serverResponse):
				let currencyCode = locale.currencyCode
				let amount = MoneyHelper.localizeAmount(
					usdAmount: serverResponse.amount,
					currencyCode: currencyCode
				)

				self.name = serverResponse.name
				self.amount = amount
				self.currencyCode = currencyCode
		}
	}
}

// Call site:
let inventoryItem = InventoryItem(result: result, locale: Locale.current)

Would love to hear your opinions on this!

  • Option 1: Static factory function
  • Option 2: Initializer overload

0 voters

Thanks :slightly_smiling_face:

Neither, really. I use initializers, but I put the transform in my network logic so I always get the value I want out of the network call and can throw errors for transform failures. Often this includes a "raw" version that defined exactly as the JSON is returned, including bad property names and overall structure (they're auto generated from response JSON) and then use an initializer to create the actual value I want. That way parsing and transforming are still two separate steps, but the initializer doesn't need to care about any errors, as it's only called if the original value was successfully retrieved.

Agreed. The example I posted above was related to networking logic, but I didn't mean to relegate this only to networking concerns. I was more so asking about a general approach for where do y'all put your "transforming glue" logic.

At a language level, Swift typically prefers initializers for those sorts of transforms. You can see this in practice in the standard library where we have initializers but no string.toInt() methods or properties.

1 Like

I sort of thought the same at first, but then I thought that the standard library only used initializers in those cases where they perform value preserving type conversions.

And that's why we have things like Int64(someUInt32), or String(Int).

But for transformations where it's not just a different representation of the same unit of data, but a combination of different pieces of data into a new type, we have things like Array.makeIterator()

I'm not sure if I'm understanding this convention correctly or not though.

i use one or another depending upon situation, there's no hard rule which one. slight preference to init version. for the init version i prefer to reference existing synthesised memberwise initialiser (so would be

self.init(name: serverResponse.name, amount: amount, currencyCode: currencyCode)

instead of explicit assignments above. there's some annoyance here that it is not possible to request having default memberwise initialiser when you put another initialiser in the struct body, here compiler is forcing you to use extension, and in those cases whereas i prefer to keep the logic in the struct body i have to use the static method approach.

also consider alternative chaining approach:

inventoryItemServerResponse.success?.inventoryItem

Value-preserving comes into play with the label in the initializer. If it's value preserving, then no label. If it's converting, changing, etc. then you'll want a label.

The reason you see some factory methods, such as the Array.makeIterator() like you provided is that the type of the iterator is intrinsically tied to Array - it's an associatedtype as part of the Sequence(Collection?) protocol so it knows and is responsible for providing an instance of that type to you.

In this case, I would definitely go towards an initializer. You're creating a new type value from a previous, and in the future it might change what pieces it needs/uses.

Factory is more for when you're abstracting away the creation of the returned object by allowing a subset of data to be provided, with defaults for others.

4 Likes