Sometimes you want classes to have a common API.
But rather than adding a common super class or interface to define that common API, you just make sure your classes have the same methods, events, properties & whatever else you feel is necessary to make them have “the same API”.
I would suggest you rethink doing this.
If what you need is controls that are similar but have differences in them just making it so you have common methods names, properties, and events will make trying to write any common code that can manipulate these controls a pain in the read end to create, maintain, and update.
For instance, lets suppose we create two controls that do wildly different things. One is a canvas control subclass that draws itself in whatever color set with a blue frame. The other draws the frame in in red. (This example is exceeding simple just to illustrate the issues. Its worse with more complex classes & controls)
So we have
Class BlueCanvas Inherits Canvas property myfillColor as color Sub SetFill(newFillColor as Color) myfillcolor = newFillColor End Sub Function GetFill() as Color return myfillColor End Function Event Paint(g as graphics, areas() as g.forecolor = myfillColor g.fillrect 0,0, g.width, g.height g.forecolor = &c0000FF g.drawrect 0,0, g.width, g.height End event End Class Class RedCanvas Inherits Canvas property myfillColor as color Sub SetFill(newFillColor as Color) myfillcolor = newFillColor End Sub Function GetFill() as Color return myfillColor End Function Event Paint(g as graphics, areas() as g.forecolor = myfillColor g.fillrect 0,0, g.width, g.height g.forecolor = &cFF0000 g.drawrect 0,0, g.width, g.height End event End Class
So our two controls have “the same API”
Same events, same methods, same properties of the same type & name.
Lets see what happens when we try to write code that uses these two simple controls.
Suppose we’d like to write a single method that can change the fillcolor of either control
Sub AlterFillColor(ctrl as Variant) Dim c As Color If SelectColor(c,"select a color") Then If ctrl IsA BlueCanvas Then BlueCanvas(ctrl).myFillColor = c BlueCanvas(ctrl).Invalidate Elseif ctrl IsA RedCanvas Then RedCanvas(ctrl).myFillColor = c RedCanvas(ctrl).Invalidate End If End If
Note several things. The parameter passed in has to be something generic – but it cant be a BlueControl or a RedControl as there is nothing really common between them. We could pass a Canvas but that would not change the code in actual method.
We MUST check to see, in this case, exactly what type was passed in because we cant be sure an appropriate type was passed in. Even if we used a Canvas we’d have to check because a generic Canvas may not have the myFillColor property and trying to access that would cause compilation errors. If we use Variant anything can be passed in. If we make that parameter an Object any INSTANCE of ANY class, not JUST our labels, can be passed in.
We MUST cast, either one time and assign to a temporary we reuse , or on every line of code where we use the control.
And this is one VERY short method and it already looks messy.
How about adding a button to copy the value of myFillColor from one canvas to another.
If we add one button to copy from the bluecanvas to the redcanvas, and another to copy the other way we can copy directly from one to another using something like :
RedCanvas1.SetFill( BlueCanvas1.GetFill() )
BlueCanvas1.SetFill( RedCanvas1.GetFill() )
This isnt so bad. But, being the clever sort we are we decide to write a generic “copy from one to the other” method and reuse the heck out of it (and this is where your spidey sense should really go crazy !)
Public Sub CopyFill(fromCtrl as variant, toCtrl as Variant) Dim copycolor As Color If fromCtrl IsA BlueCanvas Then copycolor = bluecanvas(fromctrl).FillColor Elseif fromCtrl IsA redCanvas Then copycolor = redcanvas(fromctrl).FillColor End If If toCtrl IsA BlueCanvas Then bluecanvas(toCtrl).FillColor = copycolor Elseif toCtrl IsA redCanvas Then redcanvas(toCtrl).FillColor = copyColor End If End Sub
And from the outset we have the same issue as above
Except now to manipulate two items we need to do several checks to see what types the from and to controls are.
So despite these “having the same API” thats of no real value since you have to cast and check all the time. And your parameters to methods have to be some more generic type like variant or some common super that still leaves you needing to cast all the time.
Worst of all you can pass ANY parameter that is of the right type. The compiler cannot help you check that your code is correct since it cant do type checking.
Now what if we decide we want to add a … YellowCanvas !
You can go back and see for yourself how many places we have to adjust to handle this “new” type. Every place we have a cast is a potential place we need to add code to.
And every place we add code is a new place for new bugs to crop up.
So how can we solve this in our little example ? I’m sure many of you have already seen the answer. In this case an interface ISNT of use since we can’t define events and properties.
But a common base class, even one that CANNOT be created directly, is of use.
Class FramedCanvas Inherits Canvas property myfillColor as color Sub SetFill(newFillColor as Color) myfillcolor = newFillColor End Sub Function GetFill() as Color return myfillColor End Function Private Sub Constructor() // makes it so you cant place instances on a layout // or create them direcly End Sub End Class Class BlueCanvas Inherits FramedCanvas Event Paint(g as graphics, areas() as g.forecolor = myfillColor g.fillrect 0,0, g.width, g.height g.forecolor = &c0000FF g.drawrect 0,0, g.width, g.height End event End Class Class RedCanvas Inherits FramedCanvas Event Paint(g as graphics, areas() as g.forecolor = myfillColor g.fillrect 0,0, g.width, g.height g.forecolor = &cFF0000 g.drawrect 0,0, g.width, g.height End event End Class
Now our classes dont just have the same API because we happened to write them that way. They have the same API by definition. They cant NOT have the same API.
Now our method to set a fillColor looks like
Public Sub AlterFillColor(ctrl as FramedCanvas) Dim c As Color If SelectColor(c,"select a color") Then ctrl.FillColor = c ctrl.Invalidate End If End Sub
Note we CAN use the super class as the TYPE for the parameter. BlueCanvases are also FramedCanvases. As are RedCanvases. But a regular canvas control could NOT be passed in (unlike before) and we do not need to do any casting etc. The compiler CAN check for us what is passed in. So our reliability and likelihood of writing bug free code is improved.
And, note, we have LESS code than before. And even if we add YellowCanvases etc we do NOT need to alter this code. Thats a good thing.
Directly copying the fill color from one control to another still works about the same
BlueCanvas1.SetFill( RedCanvas1.GetFill() ) BlueCanvas1.Invalidate
RedCanvas1.SetFill( BlueCanvas1.GetFill() ) RedCanvas1.Invalidate
What about our method based copy that we want to just reuse like mad ?
Public Sub CopyFill(fromCtrl as FramedCanvas, toCtrl as FramedCanvas) toCtrl.FillColor = fromCtrl.fillColor End Sub
Its trivial. No casts. No need to alter it every time a new sub type of framedCanvas comes along. And unlikely to ever have a bug.
Why ? Because these controls have an EXPLICIT API that forces them to be the same. Its not just because I happened to write the same code in several of them but because BY DEFINITION they are the same.
Where’s this all leading ?
Well so far this has all been simple and in a desktop app. It could just as well have been in a web app, iOS or some other app type. But the PRINCIPLES hold true even if you wanted to extend this to be across all those app types.
Suppose the hierarchy defined was
Control - common control events, method & properties RectControl - common rect control events, method & properties (for controls bound by a bounding box) Button - common button events properties & methods DesktopButton - events properties & methods specific to the desktop WebButton - events properties & methods specific to the web iOSButton - events properties & methods specific to ios Canvas Listbox TabPanel PagePanel
and so on
Now its plausible to write a method that regardless of whether it was used in a desktop, ios, or web project that could locate a Control, Rectcontrol, or Button. Or any other control that is defined.
No casting required.
There are some compatibility details. The DesktopButton would have to have its flags set to only be in desktop projects, the web ones only in web projects, etc. That would solve trying to compile a desktop project with iOS or web controls & vice versa.
The same may be true for controls that only exist on the desktop, and have no counter parts elsewhere. But those flags can be set at any level in the hierarchy.
And the IDE could check those flags to see what things ARE compatible and not allow you to use ones that arent.
The other upside with a properly laid out hierarchy like this for EVERYTHING is that it BY DEFINITION it tells you what properties you could expect to carry over if you copied a desktop button and pasted it into a web project. Nothing that is desktop specific should, or could, make sense in the web one. But anything defined in BUTTON and higher up in the hierarchy would, or should, be able to be copied over to a web button when you paste.
Currently its hard for any of us to do this in a cross platform app that targets ios, web and desktop simply because the underlying items in the frameworks do not do this already.
And the IDE doesnt seem to sort things out right. Depending on what version of the IDE you use you might see in the example SameXPLATExplicitAPI that the Blue Canvas has its compat flags all off but thats really wrong since it SHOULD be compatible with desktop. It would be subclassed from the Desktop one. And using those modules on ios would be from the IOS one. And on web from the web one. But this doesnt work as I expect. In some versions they are all greyed out and things “run” but the paint events never get called 🙁
So trying to make a FramedCanvas, like above, that works in iOS, desktop, and web IS, at the moment, not possible.
Making it so things share as much of their API by DEFINITION rather than just by coincidence would make it so more code is, or can be, cross platform without needing to copy paste from project to project.
Imagine THAT world !