tl;dr Build the layers with appropriate scaffolding, either top-down or bottom-up.
The goal is to understand an abstraction layer architecture. The teaching method is to have students build it layer by layer.
Here is a suggestion that you can adapt to quite a few situations, though it takes some work as well as some consideration of tradeoffs. The basic idea is that the students implement all of the levels of the abstraction stack in sequence starting either at the top or the bottom. I'll assume you are programming in Java. It translates pretty directly into similar languages, but with more work for dissimilar languages.
Envision, a three layer stack. In practice it could have any number of layers, but three is enough to show the strategy. I'll call the three layers
The Abstraction Layer Model
- The Attic
- The Main Floor
- The Basement
The Basement is the lowest level, most concrete layer and the Attic is the highest layer. The Attic might represent a true API intended for others to use (such as the Map interface) and the Basement is a low-level, even physical layer, perhaps. In practice, The Basement provides services to the layer above through a set of public methods. The Main Floor is written in terms of services provided by the Basement and itself provides services to the layer above via its own public methods.
You can work either top-down or bottom-up. However, before you can take advantage of this teaching strategy you need to build some scaffolding for the students to use. How much you have to do depends on whether you work top-down or bottom-up.
Advantages and Disadvantages of Top-Down
To work top-down, students build first the Attic with the Basement coming last.
If your students work from the top down it will be easier for them to understand the reasons for doing all this work, since the services that they are building are at a level that they might find useful in their work. The lower levels, though in reality more concrete, are more opaque since they aren't normally visible to the programmer directly.
On the other hand you actually need to do more work yourself for this as the normal Java tools don't give you as much support. I'll explain below.
Advantages and Disadvantages of Bottom-Up
To work bottom-up the students will build the Basement layer first and the Attic last.
The advantage of this is that normal unit testing tools (JUnit) make it easy for you to provide the required scaffolding.
However, the students may find this less satisfying as the things they will build at the beginning may not be as obviously useful. This is mitigated by showing them how the parts will eventually fit together before they start. They need some overview information.
To have the students build the Basement layer, simply decide on the interface between that layer and the Main Floor. These are the services that the Basement layer must implement. Use JUnit to write tests against this interface, simulating the Main Floor, sending known values to the Basement Services and making assertions about the values returned.
Students then build the Basement and make those tests pass. Make the test suite public so that they can write their own tests, however. Everything is open and available for inspection.
The Basement Layer may consist of several classes, some, perhaps, unrelated to others at the same level. For example, in a hash map, the actual Hasher might be at this level but the storage mechanism might be independent. You will need tests for each of the services, perhaps distributed over the classes.
Once the Basement is built, do the same for the next level up. Create the interface between the Attic and the Main Floor in JUnit and then have the students implement the Main Floor, providing those services, but using services of the Basement layer they already built.
To build the Attic, just repeat. The public API of the whole system is given a test suite and the students build the Attic using services of the Main Floor.
Building Top-Down requires more work since JUnit isn't available, though you may be able to find some Stub Testing tools.
The basic idea is that you first define the interface between one layer and the one below and then provide a Stub (set of) class(es) that implement that interface but accept known values and return known values when services are required of it.
To build the Attic, then, when the Main Floor doesn't yet exist, first develop the interface between the two. You then give them a Stub that they can use in writing the Attic. They will need to use the services of the Main Floor, but instead will be writing against the Stub's services with well known behavior.
When they are ready to write the Main Floor, give students a Basement Stub to that provides the services that the Main floor needs but using well determined "test" values.
Finally the Basement layer is written without additional stubbing.
However to avoid chaos when a stub is replaced by the actual layer code it will be essential that you also have a sufficient test suite in JUnit so that you know that what arrives at the outermost level actually makes sense.
Note that some development environments, such as Eclipse, will create a stub class for a given interface (or set of interfaces). The stub will have methods that ignore arguments and produce default values. You can tailor these, of course.
Caveats and Advice
Note that in both of these methods as described above, the instructor is the one who defines the interface between layers. This lessens the work of the student, of course, but is in some ways less satisfying. One can, instead, have group discussions in which the instructor leads the class to the development of the interfaces (actual Java interfaces) that form the boundaries between the layers. If you use this method, you will need to build your infrastructure after this discussion, but it may be little more than renaming things you built earlier in preparation. However, these class discussions also give you a way to talk about the layer separation.
And note that large software systems with such an architecture are often built this way with different teams working on the different layers, with someone like an architect defining the boundary APIs. And not again, that in some such situations, with many layers, efficiency dictates at some point that intermediate layers might be combined or eliminated. But, for teaching this initially don't permit that or your students may wind up building a Big Ball of Mud
CF: Test Stub
If you do the above on a sophisticated problem (building a hash map) it could take up a fair amount of class/exercise time. If you want to do it in about one class period, here is an example.
The Attic provides a service that produces the means of collections of random numbers with a given distribution; say Binomial. It calls "down" to get the collections.
The Main Floor produces the collections themselves when asked. The collection could be represented by, say, an Iterator.
The Basement produces a single value from the required distribution. It could even provide services for several distributions: Uniform, Binomial, ...
Other variations are possible. The attic, could, for example, provide the collection as a sorted list, rather than just a statistic about it. You can also add other intermediate layers, of course.
Note that each layer here is pretty trivial to build. Beware that you don't give the impression that all such uses have this property. Also note that what we really have here is akin to a Unix pipe, simply because it is so simple.