Interfaces for Implementations

0 views
Skip to first unread message

Troy Howard

unread,
Nov 22, 2010, 9:16:32 PM11/22/10
to lucer...@googlegroups.com
All,

To respond in more detail to the question "Why are we creating
interfaces for variances of concrete types, even when the type has no
additional public members to add to the API?"...

I should say that in this application, interfaces aren't meant for
purposes of type inheritance but are rather part of a system for code
verification, interoperability and surface area definition.

The basis for this comes from a few ideas. First and most importantly,
is Contract Driven Development (aka Design by Contract).

Contract Driven Development
-----------------------------------------

When we create an interface, we create an operational contract that
both the client and the supplier can base their expectations on, in
terms of how communication happens between the two parts of the code.
The supplier (aka implementing class) says "I need these parameters
for this method and I will return you something of this type". Each
method is a individual operational contract, collected into larger
sets via the concept of an interface.

Unit tests are the next level of contract. The unit tests state the
ranges of valid values and the expected results that a given member
will provide, based on some controlled input.

Through the process of defining an interface and writing unit tests
against that interface, we describe a very detailed operational
contract without any specifics for implementation.


Code Verification
-------------------------

The next logical step towards implementation are mocks and they are
essential for code verification. Mocks are implementations that
contain static behaviour and data and have no side effects. They are
meant to be used to provide a simulated operation environment for real
implementations so that you may isolate the behaviour of the real
implementation you are testing. For each interface, we should be able
to write a mock class which passes the unit tests for that interface
and which operate correctly when used with other components.

The next stage involves creating the concrete implementations of the
interface which are written such that the unit tests may pass and such
that any given external component is interacted with via the
interfaces defined in the first stage, and could be replaced with
mocks or other real implementations and continue to operate correctly.


Surface Area
-------------------

The concept of surface area refers to the publicly exposed "surface"
of a application/library/service/etc... In our case, the collection of
public interfaces is our surface area. A surface area represents a
contract as well. It exposes a collection of entities, services and
definitions that a consumer of our library can base their expectations
on.


You may think: "Ok, I understand that.. Why do I need to make an
interface for every type again?"...

Each *public* variant type in our library represents surface area. Our
entire surface area should be abstracted to interfaces, have unit
tests, and have mocks. For example... Lets take the case of the Lucene
Lock class. There's a abstract base class called LockFactory and an
implementation of that base class called FSLockFactory (FS=File
System). It's clear from the naming that the FSLockFactory uses the
file system to implement a lock factory. There's another
implementation called NoLockFactory which, it is also clear from the
name, is a lock factory that doesn't actually do any locking.

To create interfaces for this, it's obvious that the LockFactory base
class defines methods that any LockFactory should implement and so we
can create a ILockFactory that describes that contract, and that
contract should apply to any inheritors of LockFactory.

So.. taking a look at a our concrete implementations -- we have two
implementations that are vastly different, deriving from the same base
class and having the same operational contract of ILockFactory.

But what has this done for our surface area? The public consumer of
the API expects that there are two different lock classes with two
very different behaviours. They expect there to be one implementation
that creates real locks and specifically expect that it uses the file
system, and they expect a different implementation that basically
doesn't do anything.

These expectations need to be represented both in our surface area
contracts and in our unit tests. Otherwise, we end up with a leaky
abstraction in our interface layer where there is behaviour and
expectations that are not clearly expressed.

If a full implementation of Lucene MUST include a file system based
variant of LockFactory to be complete, then the library's contracts
must include a definition of IFSLockFactory to indicate that. If the
library MUST include a variant of LockFactory that does nothing to be
complete, then the library's contracts must include a definition of
INoLockFactory.

The interfaces will not include any additional members for operational
contract, but they create a domain-level contract that says "this
domain must have an implementation of IFSLockFactory in it".

Our surface area (the API defined by our public interface) is a
contract just as the interfaces are, and as such, must completely
describe all structural elements of the domain, which includes all the
variant types that are needed.

---

I hope that makes sense to everyone. Feel free to ask any questions or
challenge these assertions. These are my personal opinions based on my
experiences, personal priorities and design inclinations. We don't
have to follow my coding process just because I started the project. I
may have gotten all these ideas mixed up and might be doing it wrong.
I've been successful with this method in the past, but it might have
been dumb luck for all I know.

That said, I think this process and strategy will be the best way to
proceed to guarantee the success of the project. Feel free to let me
know of a better way.

Thanks,
Troy

Reply all
Reply to author
Forward
0 new messages