API Design & QML: To Inherit or Not To Inherit?
The base class of your item affects a large amount of your public-facing API, since everything that base class offers will also become part of your public-facing API, and you'll need to ensure that
1) the public-facing API remains cohesive 2) your component is a logical subclass of the base class
Cohesion
What exactly is API cohesion, anyhow? Cohesion, as per the dictionary is “the action or fact of forming a united whole.” A cohesive API has two main requirements
1) all of the parts make up the complete functionality of your API 2) all of the parts make up only the complete functionality of your API 3) none of the parts conflict
Complete Functionality
When designing an API, you should first and foremost outline what the thing does. Make sure to account for all of the possible usecases you can think of ahead of time, and you can always revise your API to be more ergonomic or featureful after you release it. A cohesive API should preferably achieve as much as possible with as little as possible, even though a large API can be cohesive. Too big of an API and developers will have trouble getting acclimated to it. If your API fails to offer functionality that the user would expect from it, it loses cohesiveness as the developer has to cobble together something on their end in order to implement the functionality the developer expected.
Only The Complete Functionality
When implementing an API, you need to keep in mind that you should only expose what the API of that component should offer logically. If you inherit from a base class that exposes too large of an API surface, your component may have stuff that doesn't make sense. For example, your component may be clickable, so you decide to make it a subset of a Button in order to gain clickability-related things. This, however, is likely to introduce stuff into your API that doesn't make sense. For example, a control inheriting from Button and lacking room in its design for an icon, e.g. a clickable image, would have to deal with the icon
grouped property of Buttons. Since the icon
grouped property does not form part of the implemented API, yet is part of the API the end user gets, this makes the API not cohesive. Such a control would also have issues with the text
property of Buttons. This combined with other issues makes inheriting from a Button a poor choice for a “clickable image” component's API despite both buttons and clickable images being clickable UI components, since it causes the API design to not be cohesive.
No Conflicts
Blindly inheriting from a base class can cause two parts of the API to conflict: one that you provide in your subclass, and one coming from the superclass. For example, say you're making a Button-based control that can take multiple actions, e.g. a floating action button that reveals more buttons corresponding to actions when tapped. Whilst implementing this API, you add a primary
action and a list of secondary
actions. This introduces an API conflict due to not considering the base class's own offering—the action-based API of the expanding FAB conflicts with the button's own API: one action
and some signals to connect to. This is one of the worse cases of an incohesive API—due to there being many ways to perform the same action, developers will likely use something outside of what you designed for, and will run into places you didn't implement. Even if you accomodate for these conflicts, e.g. handling both action
and primary
in the FAB component, you'll still have multiple ways to achieve the same goal, which is still not a cohesive API, and makes it harder for multiple developers to come to a consensus on how to achieve something with your API—not good.
Logical Subclasses
Determining whether something or not is a logical base class of your component should be accounted for when implementing your API. For example, you may want to render something with a foreground and a background internally, so you opt to make your component a Control, since it has a foreground
and a background
property, and automatically lays them out in a desirable manner. However, your component may not necessarily make sense as a Control APIwise, even if you want some of the functionality to implement it—say you're just using the background and foreground for visual effect only, as in a UI element that renders an user avatar. A component like this has no use for most of the API added by a control—you're using the component for its visual look, not its functionality, so overriding the visuals whilst keeping functionality intact with a public background/foreground property doesn't make sense. Fonts, spacings, UI colour palettes, and other aspects of a Control do not make sense for this component, therefore the base class should not be a Control, but rather an Item containing a Control internally. When in doubt, an Item with implicitHeight/Width set and manually exposed properties is often a better choice APIwise than exposing all of the features of the class you're using to implement the API, but not the class whose functionality you want as part of the public API. In short, pick parent classes for the API you want to expose, not the implementation you want to make. Do not expose implementation details.