Common Model Antipattern
November 19, 2015
In the vast majority of native client side software, it is common to create client-side model objects from a server representation (most commonly JSON).
This is the flow that see the most:
Here are the reasons I believe this to be a problem:
This now means you must have a dictionary (with a certain key/value type) to create a
While this is a pretty simple example, and the parsing logic is relatively trivial - many of the cases in our applications are much more complicated.
In order to start to solve this coupling, the first thing we are going to do is remove the current initializer, change the current properties to not be optional, and add an initializer that consumes the data to fill in the properties:
Now our model does no parsing, and is far more reusable. Now we must implement the parsing, for this example I will simply add an extension to
In conclusion, now our
This is the flow that see the most:
- Request an API endpoint and download JSON response
- Convert JSON to a
Dictionary
- Initialize/update a model directly with the dictionary itself
Here are the reasons I believe this to be a problem:
- Ties creation of your model to a specific dictionary format, and at some level the API endpoint (and clients have to know this fact AND the format)
- Creates ambiguity if the dictionary is not formatted correctly - for example, it could lead to certain parts of your model being valid and others that are not
- Introduces parsing logic into your models, which ideally should just store data, and contain as little of this kind of logic as possible
struct User {
let name: String?
let id: Int?
init (dictionary: [String: Any]) {
self.name = dictionary["name"] as? String
self.id = dictionary["userID"] as? Int
}
}
This now means you must have a dictionary (with a certain key/value type) to create a
User
. You also must make the properties optional unless you make the initializer failable (which comes with it's own issues) or force unwrap and force cast. All of this makes it harder to tell if your whole model is valid or not, like I mentioned before. While this is a pretty simple example, and the parsing logic is relatively trivial - many of the cases in our applications are much more complicated.
In order to start to solve this coupling, the first thing we are going to do is remove the current initializer, change the current properties to not be optional, and add an initializer that consumes the data to fill in the properties:
struct User {
let name: String
let id: Int
init (name: String, id: Int) {
self.name = name
self.id = id
}
}
Now our model does no parsing, and is far more reusable. Now we must implement the parsing, for this example I will simply add an extension to
User
, with a factory method. extension User {
static func userWithDictionary(dictionary: [String: Any]) -> User? {
if let name = dictionary["name"] as? String, id = dictionary["userID"] as? Int {
return User(name: name, id: id)
}
return nil
}
}
In conclusion, now our
User
model is totally decoupled from any dictionary format (and at some level, the API), it is not possible to have a half-configured model, and our models have no parsing logic internally - but instead is isolated in the extension.