Mixins / Modularization
During the making of ImReflect I had to build a bunch of implementations for specific C++ types. All of which needed their own customization. If we look at Booleans and enums, they both could be presented in 3 different ways:
Checkbox - Radio Buttons - Dropdown

Slider - Radio Buttons - Dropdown

Notice how the boolean and enum have 2 of the same input types. They both have the radio and dropdown option.
Because I was going for a Builder Pattern style customization, every builder needed to have a setter function.
as_checkbox()as_radio()as_dropdown()as_slider()
My first instinct was to just duplicate the code, for example:
enum class input_type {
Slider,
Radio,
Checkbox,
Dropdown
};
struct bool_settings {
input_type type;
as_checkbox() { type = input_type::Checkbox };
as_radio() { type = input_type::Radio };
as_dropdown() { type = input_type::Dropdown };
}
struct enum_settings {
input_type type;
as_slider() { type = input_type::Slider };
as_radio() { type = input_type::Radio };
as_dropdown() { type = input_type::Dropdown };
}
While this works, there are a couple of drawbacks to this approach.
- Repetition - Every time I make a new setting struct for a type, I will have to manually write the setters again.
- Fragile / Consistency - There is no check if the names are the same, I could write
as_Radio(Capital R) and it would be inconsistent with the others.
To solve this, I can use the Mixins Design Pattern. Mixins is a design pattern that allows you to re-use code for different implementations and makes the code more modular.
So instead of writing all the functions by hand every time. I do:
enum class input_type {
Slider,
Radio,
Checkbox,
Dropdown
};
/* Base */
struct input_base {
input_type type;
void set_input(const input_type v) { type = v; }
input_type_widget get_input_type() { return type; }
}
/* Mixins */
struct slider_mixin : virtual input_base {
as_slider() {set_input(input_type::Slider); }
};
struct radio_mixin : virtual input_base {
as_radio() {set_input(input_type::Radio); }
};
struct checkbox_mixin : virtual input_base {
as_checkbox() {set_input(input_type::Checkbox); }
};
struct dropdown_mixin : virtual input_base {
as_dropdown() {set_input(input_type::Dropdown); }
};
/* Settings */
struct bool_settings : radio_mixin, dropdown_mixin, checkbox_mixin {
};
// bool_settings.get_input_type() returns either radio, dropdown, or checkbox
struct enum_settings : radio_mixin, dropdown_mixin, slider_mixin {
};
// enum_settings.get_input_type() returns either radio, dropdown, or slider
While I admit, it is a bit more code. It solves the problems I had with the previous approach. Additionally, it makes things more explicit, and make it easier to add future input types that have overlapping input types.
So the actual implementation in my library look like this:
// booleans
template<typename T>
struct type_settings<T, Detail::enable_if_bool_t<T>> : ImRequired<T>,
ImReflect::Detail::radio_widget<T>,
ImReflect::Detail::dropdown_widget<T>,
ImReflect::Detail::checkbox_widget<T>,
ImReflect::Detail::button_widget<T>,
ImReflect::Detail::true_false_text<T> {
/* Default to checkbox */
type_settings() : ImReflect::Detail::input_type(ImReflect::Detail::input_type_widget::Checkbox) {}
};
// enums
template<typename E>
struct type_settings<E, std::enable_if_t<std::is_enum_v<E>, void>> : ImRequired<E>,
ImReflect::Detail::radio_widget<E>,
ImReflect::Detail::dropdown_widget<E>,
ImReflect::Detail::drag_widget<E>,
ImReflect::Detail::drag_speed<E>,
ImReflect::Detail::slider_widget<E> {
/* Default settings */
type_settings() : ImReflect::Detail::input_type(ImReflect::Detail::input_type_widget::Dropdown) { }
};
Both the enum and the boolean implementation use the radio_widget and dropdown_widget. It is guaranteed that both have the exact same function names, without a possibility of human error.
This makes the code just a bit more explicit, consistent, and avoids copying and pasting functions. See [ImReflect > ImReflect_primitives.hpp] for more mixin examples.
For those who pay attention, you might have noticed the mixins are virtual. I explain in “Diamond “problem” and virtual inheritance” why that is : )