In my current code I call validation method at the end of every CRUD method (if validation fails, an error is thrown and the change is reverted). I'm thinking if it's possible to remove these explicit validation calls. At first I thought it would be simple. Take the following code as an example, I just need to intercept the write access to value and do validation on the fly.
struct Name {
var value: String
}
This thread lists the typical approaches to intercept access to internal value. I'd use computed variable (or property observer) for simple type and dynamic member lookup for complex type. However, automatic validation requires throwing error (otherwise how do we get notiifcation? Set a flag? But that requires explicit check too). Unfortunately none of these mechanisms supports throwing error. It seems it's impossible to implement it?
Since the above type is a basic type in the app (a basic type is used to build more complex type), you may think why not call validation method manually in these types, then we don't need do explicit validation for complex type. But that's not true. For example, Employee is a complex type built by using basic types. Let's suppose it has a rule that addr1 and addr2 shouldn't be same. So again we need to implement validation manually at the complex type level.
struct Employee {
var name: Name
var addr1: Address
var addr2: Address
}
(PS: function body macro is another approach when it's available. But I'm still curious if it's possible to implement it with computed variable or dynamic lookup like features)
Two quick "manual" suggestions which might work for you:
make the fields "let" instead of "var", incorporate the validation logic in the constructor and make it failable (either throwing or returning an optional)
struct Employee {
let name: Name
let addr1: Address
let addr2: Address
init(name: Name, addr2: Address, addr2: Address) throws { ... }
init?(name: Name, addr2: Address, addr2: Address) { ... }
}
do the validation on the outer level:
var employee: Employee {
didSet {
if !employee.isValid {
self.employee = oldValue
}
}
}
the latter will temporarily invalidate the invariant, but as it is done "synchronously" it might not be a big deal, e.g. it will not cause a UI flicker through the incorrect value first.
Sorry that my incomplete example code mislead you. All types have CRUD methods. Your approach only works for immutable values.
My intuition is this approach doesn't work well in practice. For example, the code in didset has to be different if the outer struct has an array (or dictionary, or optional) of Employee. Not to mention it's error prone (easy to forget it) and hurts code readability a lot.
I'm going to use my current approach (doing validation in all CRUD methods that modify the value). Thanks.
No, what I use is like the following (a regular approach):
public struct Employee {
public var name: String
public var addr1: String
public var addr2: String
public init(name: String, addr1: String, addr2: String) { ... }
public func modify(name: String, addr1: String, addr2: String) { ... }
}
My question is not about ACL, but about if it's possible to make the code more concise (and robust) by removing explicit validation method call in init() and modify(). That said, I see your point and I should use private(set) (though that doesn't solve my original question).
And your "init" / "modify" are throwing?
You could share the validation logic between init and modify at least.
(and yeah, make the fields "private set" otherwise it would be too easy to bypass the validation).
struct Employee {
private (set) var name: String
private (set) var addr1: String
private (set) var addr2: String
var valid: Bool { fatalError("TODO") }
static func validating(name: String, addr1: String, addr2: String) throws -> Self {
let result = self.init(name: name, addr1: addr1, addr2: addr2)
if !result.valid { throw ... }
return result
}
mutating func modify(name: String, addr1: String, addr2: String) throws {
self = try Self.validating(name: name, addr1: addr1, addr2: addr2, validate: true)
}
}
As the code is only for yourself - always remember using the "validating" static method to make "employees" and not the initialiser. As you can see the boilerplate amount is minimal. Alternative to a static creation method is an initialiser with a different signature (e.g. init(validating: true, ...)) but that's the same difference.
That's a design to make sure it's impossible to bypass validation in the code. One nit: if you're doing it this way, the struct is immutable and I think all properties should be declared using "let". One reason I prefer to dynamic lookup (if it was feasible) is that it provides a explicit place in the code for handling value change, so I can do validation or send change notification in a single place. While the above design deals with validation perfectly, it doesn't provide direct support for handling value change (thought there is nothing to prevent me doing it in modify()). I'll think more about it and let you know if I use this approach in actual code. Thanks.
BTW, this can be guaranteed by declaring init() as private.
UPDATE: on a second thought, I think the above code can be improved by replacing the static method with a regular init() which has validation code? With that change, the design is essentially about implementing modify() by creating a new instance rather than performing in-place modification.
Those setXxx methods are same as my modify() method in this sense.
I think it's feasible. For example, Entity macro add Validatable conformance; the struct implements the requirement of Validatable; and MemberInit calls validate(). I'm using this approach in my code for other purpose.
I don't mean the one synthesized by compiler. I create init using macro, which is flexible.
// Default behavior: create a public init
@Init
struct Foo {
...
}
// Pass arguments to change its behavior
// The arguments are defined in macro interface module.
// Macro supports variadic arguments.
@Init(.private, .validating)
struct Bar {
...
}