I've been working through a revamp of the Views API (more on that later), and here are a few general thoughts I've had as I've scanned through uses of it in the codebase:
- The API is too big, there are too many vaguely (but not quite) identical ways of doing similar seeming things
- There is little control over the API upward (protected:) in the class hierarchy as well as outward (public:)
- Roles for specific types aren't clearly defined enough.
- There are some functions that are dangerous, misleading or just silly, and they're exposed on the public API and are then [mis]used by people who aren't aware of the real reason for their existence!
- Repetition is GOOD.
I'm trying to solve some of this. Specifically the lessons learned are:
It's important to think of how and why someone would subclass your class, when determining what is public, protected and private. The default should be for the least visibility.
For example, far too many methods on View are public, even though there is no reason for anyone to call them outside of the framework. Event handlers are a good example. I will be making all of these protected. Processing functions that exist only in the framework should be private. There are cases where people override some of these framework processing functions. In pretty much every case the people doing this have not understood the underlying framework and actually been doing something wrong/buggy.
I think it is preferable to have friendships or private interfaces with internal:: types than to make methods that only internal:: types call public and pollute the standard API.
For example, RootView is a type that not everyone needs to know about. It should be an implementation detail of the view hierarchy. Mostly, people just want to communicate with the containing Widget. RootView thus moves to the internal namespace, and knows how to communicate with the Views privately.
It is important to note the intended clients of particular functionality. It is best to assume that functionality is not generic if only one client needs it, especially if the implementation of that functionality is complex.
For example, View contains some complex logic intended to support clipping of native child views to the visible bounds of their ancestor views. The only legitimate client for this functionality I've found is NativeViewHost, another class in the Views framework. I am currently working to ensure it is the only one that can use this functionality.
It is a good idea for classes to do one thing and do it well.
RootView is currently the only cross platform per-Widget object. As a result it has a lot of routines on it that aren't really related to event propagation... they're just there because it was simpler to put them there than duplicate code across the various Widget implementations. In my redesign, RootView is focused on the internal details of event propagation, and there is a new cross platform Widget class that handles the rest.
Repetition, Repetition, Repetition! Do I need to say it again?
If you feel like you're off the beaten track when you're writing code, you're probably doing something wrong, or the API isn't well developed enough and you should try and fix it.
For example, in Views there are established patterns for View ownership, setup, hierarchy, etc. We have tried to make it really easy to achieve a certain set of functionality. We have not always made it easy enough. As one example, it's still too hard to port, and as another there are still some oddities with native widget focus that seem to leave us littered with hacks all over the place. It is worth understanding the underlying problem, what the framework is doing, and attempting to fix the framework generically than bandaiding in an increasing number of places.
When your code looks like other code, it's likely to raise fewer eyebrows and be easier to review.
-Ben