This isn't how to go about unit testing. Test the door and lock
separately. Use a mock/fake for the lock when you're testing the
door's functionality and if the lock needs to know about the door to
do its thing you do the same there. You don't need to run the full
combination of every possible assignment of lock and door. That's the
whole point of unit testing actually.
Whole point of unit testing is to discover some of the defects earlier.
'Earlier' means before they get into repository and before they get into
build and before they start to cause more work to other people.
I am not sure why to split the tests at so low level? For example if design
say a template X<T> should not accept int as argument then why not to make
test that expects a static assert on X said:
Let the next levels, such as integration and functional testing, pick
up anything that results from some specific combination. When and if
such a bug is found it will have something to do with one or both of
the units in question and it will point out something you missed in
your unit test...add it.
Call it 'integration test', 'regression test' or whatever, like we saw
earlier the different abilities affect each other so depending on the
tested situation such test may still need actual or mocking or fake or
dummy code. Since it ensures that code matches with design I call it
'unit test' as well, for simplicity.
If to bring parallel with doors then test 'knock-ability', 'lock-ability',
'pass-ability' and 'open-ability' of whole doors too. In project with some
history such tests contain majority of test code.
It is quite normal for integration and
functional testing to take a while, which is another reason to keep
your unit testing small and fast...but complete for each unit.
Second level test for me after 'unit tests' is 'automatic tests', that
run on whole module and test it with real or generated scenarios.
Usually in environment of fake other modules. It is just to save the
human-testers time. May run constantly somewhere, i did not mean those.
I don't know why you're talking about refactoring here, I thought we
agreed the assumption was a new product design. I would not refactor
without some feature that can't be made the other way or was being
made more difficult, but I would not design it with inheritance from
the start for the reasons I've mentioned already...all of which ARE
objective reasons that I at least consider "needs" (such as future
maintainability).
I am talking about refactoring in the context of your words: "This can
be especially bad if they suddenly decide this 6 months down the road
and you've got all this code betting on the wrong kind of interface."
It happens with all projects that things turn 90 or even 180 degrees with
lot smaller intervals than 6 months. If something is difficult to refactor
then that is major flaw. I am not sure what you mean by wrong kind of
interface, so was just saying that the interface is pretty compatible.
Another issue with this conversation has been that you seem to be
taking a very simplistic, poorly thought out example to discuss
theoretical issues and used it specifically. While I've been
explaining ways in which to approach this example in terms that are
universal, you've been harping on this idea that you can make a
function to wrap up the cast. This is true here, but very often NOT
true with other, similar such constructs. Using inheritance as your
primary form of reuse is just plain wrong and leads to brittle,
unmaintainable code. Although this was how people thought OO would be
used when it first came out, it was realized fairly quickly that it
doesn't work that way. Inheritance should only be used for conceptual
organization--is-a relationships--and nothing else.
Is there a case of that "very often NOT true" where some "interface
getter" can not use cross-cast internally? It feels equal to forwarding a
reference to a member or other aggregate. Sure, lockable door is-a
lockable, what is wrong using inheritance there? Or ... when you see a
door what is wrong about asking "are you lockable?"? If it is wrong in
sense of some general principle of OOD then too bad for OOD. We maybe need
better example to discuss that side?
There are tons of ways how to reuse and combine functionality in C++
into real class. Some assortment of ways (far not all of them):
0) You do not need that functionality. Do not reuse it in any way. Lack
of any code is most simple.
1) Use template. Good template is hard thing to make and to test
sufficiently, but it is extremely efficient and reusable.
2) Forward functionality of non-public base class. If there are no
polymorphism then it is very efficient. Easier than (3) to use, bit
harder to understand.
3) Forward functionality to non-polymorphic data member. When the data
member is template then it is as efficient as (2) otherwise it carries
very little overhead. Easiest to understand.
4) Static polymorphism (CRTP). Very little overhead if any. Adds some
flexibility of (5), but is hardest to understand and to make since
it involves (1) with properties of (5).
5) Usual dynamic polymorphism. With its virtual functions it is bit less
efficient than others above. Somewhat overused, over-taught and
under-understood.
6) Forward functionality to polymorphic data member. That is both most
complex and flexible since polymorphic data member means that it may
change during object's life-time. More overhead than with (5).
Can not be that the SOLID OOD suggests to always use that (6) as very
first choice of functionality re-usage ... it seems least efficient and
not most simple.
The problem with using a simplistic example to discuss such a far
reaching concept is that there can always be these, "but I don't need
it in this case," type responses.
Ok, probably my fault. I did not want to base all considerations of
re-usage of functionality on one simplistic example.
Are we coding some game where doors are simple and they either lock or
don't?
No. I have explained else-thread. It was just used to avoid typical
class A, class B blah-blah too abstract example. I did not want to
introduce some problem with doors. I wanted to bring example of the usage
of cross cast.
As for doors ... yes let them rest ... Possibly the usage of vector of
polymorphic locks is the perfect choice for a door in some game-world.
It may also be that it is overkill and just a data member with fitting
key # is needed in some other game-world. That is why i have said that you
can let the doors to rest.
Anyway, someone somewhere asked how to do what they were doing without
RTTI and I've provided at least one good way now. Using RTTI isn't
necessarily bad, it's a smell that indicates something possibly bad
and/or that something could be done better. When you see RTTI being
used, look for violations of SOLID principles, which though are called
"OO Principles" extend quite readily into other paradigms like generic
programming.
Yes it was me. It was good discussion.
Your suggestion to go to next level and turn inheritance into component
can be correct choice. What i am still discussing is that you seem to
suggest to always design inheritance as component. I have seen it suggestedelsewhere and i do not see it at all as profitable on general case.
The way suggested by Scott and Leigh to combine all cases (to make all Doors
Lockable) was maybe most robust. It turns it into usual polymorphic usage.
The pre_opener of Goran (maybe also your, you mentioned visitor?) was to use
some double-dispatch to get rid of dynamic-cast. That raises complexity and
often feels like wheel-chair. It can work best with legacy code that should
not be modified.
Were there others? Sorry if i missed.
I myself still think that unless it burns (shows signals of under- or
over-engineering) somewhere else ... then that cross-cast there is fine.