Wednesday, July 8, 2009

Breaking up classes using interfaces

When I started writing F# programs whose code span over multiple files, I was surprised to see that the order of compilation of these files matters. If the code in file B.fs refers to types or functions in A.fs, then A.fs must be compiled before B.fs.

Now, what happens if A.fs and B.fs depend on each other? In this case, you have a problem. The easy fix is to merge the two files, but this solution does not scale well. Another solution is to break code into smaller pieces, building small "islands" of circular dependencies, which can each be moved into their own file. Easily said, but not so easily said.

Consider an application involving processes and links. Processes can be executed, and can communicate with each other using unidirectional links.

The code might look like this:

type Process(name, config, ...) =
// Construction
...

// Methods
// Connection to links
member x.AddInputLink(link : Link) = ...
member x.AddOutputLink(link : Link) = ...

// Control execution
member x.ExecuteInSomeWay(args) = ...
member x.ExecuteInSomeOtherWay(args) = ...
member x.CheckIsReady() = ...
member x.MarkAsReady() = ...

// Debugging, and other stuff...
member x.ToggleDebugging(b) = ...
member x.SetDebugOutput(o) = ...
...

and type Link(name, config, ...) =
// Construction
...

// Methods
// Connection to processes
member x.ConnectOutputTo(p : Process) = ...
member x.ConnectInputTo(p : Process) = ...

// Read/Write data
member x.Send(data) = ...
member x.Receive() = ...

// Debugging ...
member x.ToggleDebugging(b) = ...
member x.SetDebugOutput(o) = ...
...



As Process and Link are mutually dependent, their code must be all in one file. That may seem OK at first, but things will become harder to manage as more code is added over time.

A first solution is to introduce interfaces for Process and Link, simply duplicating all their method signatures, and replace all occurrences of classes Process and Link by their interfaces. The interfaces can be moved to a separate file, which should be shorter, as it contains no code. Classes Process and Link can now each live in separate files.

Interfaces.fs:
type IProcess =
// Connection to links
abstract AddInputLink : ILink -> ...
abstract AddOutputLink : ILink -> ...

// Control execution
abstract ExecuteInSomeWay : Args -> ...
...

// Debugging, and other stuff...
member x.ToggleDebugging : bool -> unit
...

and type ILink =
// Connection to processes
abstract ConnectOutputTo Process -> unit
...



Process.fs:
open Interfaces

type Process(name, config, ...) =
// Construction
...

// Interface implementations
interface IProcess with
member x.ExecuteInSomeWay(args) = ...
...


Link.fs:
open Interfaces

type Link(name, config, ...) =
// Construction
...

// Interface implementation
interface ILink with
member x.ConnectOutputTo(p : IProcess) = ...
...


Still, Interfaces.fs may be excessively long, and we may end up in a situation where all interfaces are defined in one large file. Not very pleasant.

The next step consists of breaking up the interfaces. The debugging-related methods obviously belong to a IDebugable interface, which need not be concerned with the details of processes and links.

A process which is connected to the input side of a link has no need to have access to the methods taking care and transferring data from the link to the process, which indicates that ILink could be split in e.g. IInputLink and IOutputLink.

There is another case of excessive method exposure. Links do not control the execution of processes, that's the task of the scheduler. The final decomposition may look as shown below.

IActivable.fs:
type IActivable =
abstract CheckIsReady : unit -> bool
abstract MarkAsReady : bool -> unit


IExecutable.fs:
type IExecutable =
abstract ExecuteInSomeWay : Args -> ...
abstract ExecuteInSomeOtherWay : Args -> ...


InputInterfaces.fs:
type IReceiver =
abstract AddInputLink : ILinkInput -> unit

and type ILinkInput =
abstract Receive : unit -> Data
abstract ConnectOutputTo : IReceiver -> unit


OutputInterfaces.fs:
type ISender =
abstract AddOutputLink : ILinkOutput

and type ILinkOutput =
abstract Send : Data -> unit
abstract ConnectInputTo : ISender -> unit


The techniques shown here are by no means specific to F#, and are part of the "proper programming" techniques every object-oriented programmer should know and use. Before using F#, I almost exclusively used interfaces for polymorphism, on "as-needed" basis. The fact is, interfaces are also useful for modular programming.

Decomposing programs into interfaces and implementation helps writing reusable, readable code. It does have a cost, though, as it requires more typing. As long as keyboards remain cheaper than brains, the benefits should outweigh the costs.

No comments: