I recently fixed a defect that I felt the compiler could have caught. Some initialization code was not being run as I was calling a protected function instead of the correct public one. With some simple additional access modifiers it would be easy to prevent this type of error.
The problem
My code had a fairly common structure: a public function which wraps an internal protected one. In my case it was part of Fuse’s layout code in the Element
class:
1 2 3 4 5 6 |
public float2 ArrangeMarginBox(float2 position, float2 availableSize, SizeFlags availSet) { // ...common setup code... var sz = OnArrangeMarginBox(position, availableSize, availSet); // ...final layout code... return sz; } |
Where:
1 |
protected abstract float2 OnArrangeMarginBox(float2 position, float2 availableSize, SizeFlags availSet); |
The problem lies in the protected
modifier on the OnArrangeMarginBox
. This is needed so the base class can call the derived implementations, but it also means all those implementations can call this function on other instances.
The problem surfaces for me since I have a container class including another element. During measuring it needs to measure the other element and I did this by accident:
1 2 3 |
protected override float2 OnArrangeMarginBox(float2 position, float2 availableSize, SizeFlags availSet) { return MyContent.OnArrangeMarginBox(poistion, availableSize, availSet); } |
I called OnArrangeMarginBox
instead of ArrangeMarginBox
, thus getting invalid results!
The simple solution
When I marked the OnArrangeMarginBox
function as protected
I really wanted to say that only the current instance can call that function. Another instance of the same type shouldn’t be able to call it. It’s actually a very common desire for protected
functions and properties; they are meant to be available only to the current instance.
1 |
protected(instance) abstract float2 OnArrangeMarginBox(float2 position, float2 availableSize, SizeFlags availSet); |
This would have caught my error quickly, since MyContent.OnArrangeMarginBox
is no longer allowed; MyContent
is not this
instance thus I can’t call instance protected methods on it.
I’ve looked at a lot of my code in several projects and I’m starting to wonder if maybe instance protected isn’t the more reasonable default. Other than a few comparators it’s rare that I actually want my protected fields accessible by other instances. I’m sure I’ve seen at least one language where this is the case, but I can’t remember which.
The complex solution
I’m tempted to go one step further here. OnArrangeMarginBox
should actually not be called by anything other than the ArrangeMarginBox
function. It’s not really a standalone function: it’s a component of the public function.
I’d like to say:
1 |
callable_by(ArrangeMarginBox) abstract float2 OnArrangeMarginBox(float2 position, float2 availableSize, SizeFlags availSet); |
Where callable_by(ArrangeMarginBox)
specifies that only a single function may call this one.
This modifier would have prevented several defects I’ve had over the years. It’d block another situation I had with layout code as well. ArrangeMarginBox
has a counterpart ArrangePaddingBox
, along with it’s OnArrangePaddingBox
partner. There are a lot of types that implement layout, and it’s an easy mistake to call OnArrangePaddingBox
when we actually meant OnArrangeMarginBox
. Markers like callable_by
can help prevent this type of error.
Strictness over convention
A lot of projects use conventions to indicate extended modifiers, such as using a trailing underscore, different casing, or other naming patterns. I would much prefer that my compiler can validiate my code rather than me manually tracking such conventions. The compiler is just better at these fine details.
There’s always a balance that has to be reached with adding more modifiers. We want to write safe code, but we don’t want to lose the ease of programming new code. I think OOP is missing a lot of flow control modifiers though, making it confusing to write type hierarchies. I’d welcome the addition of modifiers that help me better manage my virtual functions.