There's lots of questions about the open-closed principle here, but none that I found satisfied this particular problem. At $job
, I'm teaching my team Rust, but I believe the nature of this question extends beyond Rust. We have a training problem that we use to kick the tires on new ideas, because we can all imagine a few canonical solutions to it. One small aspect of this problem is that an embedded target needs to communicate with a host PC over a serial port. Naturally, I used Rust's famous enums to describe the protocol (domain terms have been erased from the example):
enum Message {
SetCharacteristic {
id: u8,
characteristic: Characteristic,
},
SetIsEnabled {
id: u8,
enabled: bool,
},
}
My mentor disapproved of this solution, because enums are closed to the addition of new variants under the open-closed principle (SO answer for reference), and we expect to receive new requirements in the future that require us to add new message types. While I absolutely agree that this is the case, refactoring this enum out to apply the OCP invariably produces a solution where message handlers are dynamically registered at runtime, so the call site goes from a match expression:
match message {
Message::SetCharacteristic { .. } => todo!(),
Message::SetIsEnabled { .. } => todo!(),
}
To something like this:
fn is_set_characteristic(message: &Message) -> bool { todo!() }
let mut router = Router::new();
router.add(is_set_characteristic, move |message| {
// Handle SetCharacteristic message...
});
router.handle(message);
I think the tradeoff here is twofold:
- Loss of some type safety:
match
expressions that don't handle all representable cases are compiler errors in Rust, whereas "forgetting" to register a message handler in the second case is a runtime error. - The second case asks more from the reader: it may be harder to understand.
I think the benefit of this refactor is only realized in cases where the software lifecycle of message types necessitates change independent of the protocol implementation (e.g. the Router
is authored and released by ACME Corp., while my organization sets the message types for our product), but my team is very dedicated to the SOLID Principles. In a situation like this where the application of the OCP invariably reduces type-safety, how do I choose? If I choose type safety, how do I justify my choice to experienced architects and engineers?