layout | title | description | categories | seriesId | seriesOrder | |
---|---|---|---|---|---|---|
post |
Refactoring to remove cyclic dependencies |
Cyclic dependencies: Part 2 |
|
Dependency cycles |
2 |
In the previous post, we looked at the concept of dependency cycles, and why they are bad.
In this post, we'll look at some techniques for eliminating them from your code. Having to do this may seem annoying at first, but really, you'll come to appreciate that in the long run, "it's not a bug, it's a feature!"
Let's classify the kinds of dependencies you're likely to run into. I'll look at three common situations, and for each one, demonstrate some techniques for dealing with them.
First, there is what I will call a "method dependency".
- Type A stores a value of type B in a property
- Type B references type A in a method signature, but doesn't store a value of type A
Second, there is what I will call a "structural dependency".
- Type A stores a value of type B in a property
- Type B stores a value of type A in a property
Finally, there is what I will call an "inheritance dependency".
- Type A stores a value of type B in a property
- Type B inherits from type A
There are, of course, other variants. But if you know how to deal with these, you can use the same techniques to deal with the others as well.
Before we get started, here are three useful tips which apply generally when trying to untangle dependencies.
Tip 1: Treat F# like F#.
Recognize that F# is not C#. If you are willing to work with F# using its native idioms, then it is normally very straightforward to avoid circular dependencies by using a different style of code organization.
Tip 2: Separate types from behavior.
Since most types in F# are immutable, it is acceptable for them to be "exposed" and "anemic", even. So in a functional design it is common to separate the types themselves from the functions that act on them. This approach will often help to clean up dependencies, as we'll see below.
Tip 3: Parameterize, parameterize, parameterize.
Dependencies can only happen when a specific type is referenced. If you use generic types, you cannot have a dependency!
And rather than hard coding behavior for a type, why not parameterize it by passing in functions instead? The List
module is a great example of this approach, and I'll show some examples below as well.
We'll start with the simplest kind of dependency -- what I will call a "method dependency".
Here is an example.
module MethodDependencyExample =
type Customer(name, observer:CustomerObserver) =
let mutable name = name
member this.Name
with get() = name
and set(value) =
name <- value
observer.OnNameChanged(this)
and CustomerObserver() =
member this.OnNameChanged(c:Customer) =
printfn "Customer name changed to '%s' " c.Name
// test
let observer = new CustomerObserver()
let customer = Customer("Alice",observer)
customer.Name <- "Bob"
The Customer
class has a property/field of type CustomerObserver
, but the CustomerObserver
class has a method which takes a Customer
as a parameter, causing a mutual dependency.
One straightforward way to get the types to compile is to use the and
keyword, as I did above.
The and
keyword is designed for just this situation -- it allows you to have two or more types that refer to each other.
To use it, just replace the second type
keyword with and
. Note that using and type
, as shown below, is incorrect. Just the single and
is all you need.
type Something
and type SomethingElse // wrong
type Something
and SomethingElse // correct
But and
has a number of problems, and using it is generally discouraged except as a last resort.
First, it only works for types declared in the same module. You can't use it across module boundaries.
Second, it should really only be used for tiny types. If you have 500 lines of code between the type
and the and
, then you are doing something very wrong.
type Something
// 500 lines of code
and SomethingElse
// 500 more lines of code
The code snippet shown above is an example of how not to do it.
In other words, don't treat and
as a panacea. Overusing it is a symptom that you have not refactored your code properly.
So, instead of using and
, let's see what we can do using parameterization, as mentioned in the third tip.
If we think about the example code, do we really need a special CustomerObserver
class? Why have we restricted it to Customer
only? Can't we have a more generic observer class?
So why don't we create a INameObserver<'T>
interface instead, with the same OnNameChanged
method, but the method (and interface) parameterized to accept any class?
Here's what I mean:
module MethodDependency_ParameterizedInterface =
type INameObserver<'T> =
abstract OnNameChanged : 'T -> unit
type Customer(name, observer:INameObserver<Customer>) =
let mutable name = name
member this.Name
with get() = name
and set(value) =
name <- value
observer.OnNameChanged(this)
type CustomerObserver() =
interface INameObserver<Customer> with
member this.OnNameChanged c =
printfn "Customer name changed to '%s' " c.Name
// test
let observer = new CustomerObserver()
let customer = Customer("Alice", observer)
customer.Name <- "Bob"
In this revised version, the dependency has been broken! No and
is needed at all. In fact, you could even put the types in different projects or assemblies now!
The code is almost identical to the first version, except that the Customer
constructor accepts a interface, and CustomerObserver
now implements the same interface. In fact, I would argue that introducing the interface has actually made the code better than before.
But we don't have to stop there. Now that we have an interface, do we really need to create a whole class just to implement it? F# has a great feature called object expressions which allows you to instantiate an interface directly.
Here is the same code again, but this time the CustomerObserver
class has been eliminated completely and the INameObserver
created directly.
module MethodDependency_ParameterizedInterface =
// code as above
// test
let observer2 = {
new INameObserver<Customer> with
member this.OnNameChanged c =
printfn "Customer name changed to '%s' " c.Name
}
let customer2 = Customer("Alice", observer2)
customer2.Name <- "Bob"
This technique will obviously work for more complex interfaces as well, such as that shown below, where there are two methods:
module MethodDependency_ParameterizedInterface2 =
type ICustomerObserver<'T> =
abstract OnNameChanged : 'T -> unit
abstract OnEmailChanged : 'T -> unit
type Customer(name, email, observer:ICustomerObserver<Customer>) =
let mutable name = name
let mutable email = email
member this.Name
with get() = name
and set(value) =
name <- value
observer.OnNameChanged(this)
member this.Email
with get() = email
and set(value) =
email <- value
observer.OnEmailChanged(this)
// test
let observer2 = {
new ICustomerObserver<Customer> with
member this.OnNameChanged c =
printfn "Customer name changed to '%s' " c.Name
member this.OnEmailChanged c =
printfn "Customer email changed to '%s' " c.Email
}
let customer2 = Customer("Alice", "[email protected]",observer2)
customer2.Name <- "Bob"
customer2.Email <- "[email protected]"
In many cases, we can go even further and eliminate the interface class as well. Why not just pass in a simple function that is called when the name changes, like this:
module MethodDependency_ParameterizedClasses_HOF =
type Customer(name, observer) =
let mutable name = name
member this.Name
with get() = name
and set(value) =
name <- value
observer this
// test
let observer(c:Customer) =
printfn "Customer name changed to '%s' " c.Name
let customer = Customer("Alice", observer)
customer.Name <- "Bob"
I think you'll agree that this snippet is "lower ceremony" than either of the previous versions. The observer is now defined inline as needed, very simply:
let observer(c:Customer) =
printfn "Customer name changed to '%s' " c.Name
True, it only works when the interface being replaced is simple, but even so, this approach can be used more often than you might think.
As I mentioned above, a more "functional design" would be to separate the types themselves from the functions that act on those types. Let's see how this might be done in this case.
Here is a first pass:
module MethodDependencyExample_SeparateTypes =
module DomainTypes =
type Customer = { name:string; observer:NameChangedObserver }
and NameChangedObserver = Customer -> unit
module Customer =
open DomainTypes
let changeName customer newName =
let newCustomer = {customer with name=newName}
customer.observer newCustomer
newCustomer // return the new customer
module Observer =
open DomainTypes
let printNameChanged customer =
printfn "Customer name changed to '%s' " customer.name
// test
module Test =
open DomainTypes
let observer = Observer.printNameChanged
let customer = {name="Alice"; observer=observer}
Customer.changeName customer "Bob"
In the example above, we now have three modules: one for the types, and one each for the functions. Obviously, in a real application, there will be a lot more Customer related functions in the Customer
module than just this one!
In this code, though, we still have the mutual dependency between Customer
and CustomerObserver
. The type definitions are more compact, so it is not such a problem, but even so, can we eliminate the and
?
Yes, of course. We can use the same trick as in the previous approach, eliminating the observer type and embedding a function directly in the Customer
data structure, like this:
module MethodDependency_SeparateTypes2 =
module DomainTypes =
type Customer = { name:string; observer:Customer -> unit}
module Customer =
open DomainTypes
let changeName customer newName =
let newCustomer = {customer with name=newName}
customer.observer newCustomer
newCustomer // return the new customer
module Observer =
open DomainTypes
let printNameChanged customer =
printfn "Customer name changed to '%s' " customer.name
module Test =
open DomainTypes
let observer = Observer.printNameChanged
let customer = {name="Alice"; observer=observer}
Customer.changeName customer "Bob"
The Customer
type still has some behavior embedded in it. In many cases, there is no need for this. A more functional approach would be to pass a function only when you need it.
So let's remove the observer
from the customer type, and pass it as an extra parameter to the changeName
function, like this:
let changeName observer customer newName =
let newCustomer = {customer with name=newName}
observer newCustomer // call the observer with the new customer
newCustomer // return the new customer
Here's the complete code:
module MethodDependency_SeparateTypes3 =
module DomainTypes =
type Customer = {name:string}
module Customer =
open DomainTypes
let changeName observer customer newName =
let newCustomer = {customer with name=newName}
observer newCustomer // call the observer with the new customer
newCustomer // return the new customer
module Observer =
open DomainTypes
let printNameChanged customer =
printfn "Customer name changed to '%s' " customer.name
module Test =
open DomainTypes
let observer = Observer.printNameChanged
let customer = {name="Alice"}
Customer.changeName observer customer "Bob"
You might be thinking that I have made things more complicated now -- I have to specify the observer
function everywhere I call changeName
in my code. Surely this is worse than before? At least in the OO version, the observer was part of the customer object and I didn't have to keep passing it in.
Ah, but, you're forgetting the magic of partial application! You can set up a function with the observer "baked in", and then use that function everywhere, without needing to pass in an observer every time you use it. Clever!
module MethodDependency_SeparateTypes3 =
// code as above
module TestWithPartialApplication =
open DomainTypes
let observer = Observer.printNameChanged
// set up this partial application only once (at the top of your module, say)
let changeName = Customer.changeName observer
// then call changeName without needing an observer
let customer = {name="Alice"}
changeName customer "Bob"
Let's look at the changeName
function again:
let changeName observer customer newName =
let newCustomer = {customer with name=newName}
observer newCustomer // call the observer with the new customer
newCustomer // return the new customer
It has the following steps:
- do something to make a result value
- call the observer with the result value
- return the result value
This is completely generic logic -- it has nothing to do with customers at all. So we can rewrite it as a completely generic library function. Our new function will allow any observer function to "hook into" into the result of any other function, so let's call it hook
for now.
let hook2 observer f param1 param2 =
let y = f param1 param2 // do something to make a result value
observer y // call the observer with the result value
y // return the result value
Actually, I called it hook2
because the function f
being "hooked into" has two parameters. I could make another version for functions that have one parameter, like this:
let hook observer f param1 =
let y = f param1 // do something to make a result value
observer y // call the observer with the result value
y // return the result value
If you have read the railway oriented programming post, you might notice that this is quite similar to what I called a "dead-end" function. I won't go into more details here, but this is indeed a common pattern.
Ok, back to the code -- how do we use this generic hook
function?
Customer.changeName
is the function being hooked into, and it has two parameters, so we usehook2
.- The observer function is just as before
So, again, we create a partially applied changeName
function, but this time we create it by passing the observer and the hooked function to hook2
, like this:
let observer = Observer.printNameChanged
let changeName = hook2 observer Customer.changeName
Note that the resulting changeName
has exactly the same signature as the original Customer.changeName
function, so it can be used interchangably with it anywhere.
let customer = {name="Alice"}
changeName customer "Bob"
Here's the complete code:
module MethodDependency_SeparateTypes_WithHookFunction =
[<AutoOpen>]
module MyFunctionLibrary =
let hook observer f param1 =
let y = f param1 // do something to make a result value
observer y // call the observer with the result value
y // return the result value
let hook2 observer f param1 param2 =
let y = f param1 param2 // do something to make a result value
observer y // call the observer with the result value
y // return the result value
module DomainTypes =
type Customer = { name:string}
module Customer =
open DomainTypes
let changeName customer newName =
{customer with name=newName}
module Observer =
open DomainTypes
let printNameChanged customer =
printfn "Customer name changed to '%s' " customer.name
module TestWithPartialApplication =
open DomainTypes
// set up this partial application only once (at the top of your module, say)
let observer = Observer.printNameChanged
let changeName = hook2 observer Customer.changeName
// then call changeName without needing an observer
let customer = {name="Alice"}
changeName customer "Bob"
Creating a hook
function like this might seem to add extra complication initially, but it has eliminated yet more code from the main application, and once you have built up a library of functions like this, you will find uses for them everywhere.
By the way, if it helps you to use OO design terminology, you can think of this approach as a "Decorator" or "Proxy" pattern.
The second of our classifications is what I am calling a "structural dependency", where each type stores a value of the other type.
- Type A stores a value of type B in a property
- Type B stores a value of type A in a property
For this set of examples, consider an Employee
who works at a Location
. The Employee
contains the Location
they work at, and the Location
stores a list of Employees
who work there.
Voila -- mutual dependency!
Here is the example in code:
module StructuralDependencyExample =
type Employee(name, location:Location) =
member this.Name = name
member this.Location = location
and Location(name, employees: Employee list) =
member this.Name = name
member this.Employees = employees
Before we get on to refactoring, let's consider how awkward this design is. How can we initialize an Employee
value without having a Location
value, and vice versa.
Here's one attempt. We create a location with an empty list of employees, and then create other employees using that location:
module StructuralDependencyExample =
// code as above
module Test =
let location = new Location("CA",[])
let alice = new Employee("Alice",location)
let bob = new Employee("Bob",location)
location.Employees // empty!
|> List.iter (fun employee ->
printfn "employee %s works at %s" employee.Name employee.Location.Name)
But this code doesn't work as we want. We have to set the list of employees for location
as empty because we can't forward reference the alice
and bob
values..
F# will sometimes allow you to use the and
keyword in these situation too, for recursive "lets". Just as with "type", the "and" keyword replaces the "let" keyword. Unlike "type", the first "let" has to be marked as recursive with let rec
.
Let's try it. We will give location
a list of alice
and bob
even though they are not declared yet.
module UncompilableTest =
let rec location = new Location("NY",[alice;bob])
and alice = new Employee("Alice",location )
and bob = new Employee("Bob",location )
But no, the compiler is not happy about the infinite recursion that we have created. In some cases, and
does indeed work for let
definitions, but this is not one of them!
And anyway, just as for types, having to use and
for "let" definitions is a clue that you might need to refactor.
So, really, the only sensible solution is to use mutable structures, and to fix up the location object after the individual employees have been created, like this:
module StructuralDependencyExample_Mutable =
type Employee(name, location:Location) =
member this.Name = name
member this.Location = location
and Location(name, employees: Employee list) =
let mutable employees = employees
member this.Name = name
member this.Employees = employees
member this.SetEmployees es =
employees <- es
module TestWithMutableData =
let location = new Location("CA",[])
let alice = new Employee("Alice",location)
let bob = new Employee("Bob",location)
// fixup after creation
location.SetEmployees [alice;bob]
location.Employees
|> List.iter (fun employee ->
printfn "employee %s works at %s" employee.Name employee.Location.Name)
So, a lot of trouble just to create some values. This is another reason why mutual dependencies are a bad idea!
To break the dependency, we can use the parameterization trick again. We can just create a parameterized vesion of Employee
.
module StructuralDependencyExample_ParameterizedClasses =
type ParameterizedEmployee<'Location>(name, location:'Location) =
member this.Name = name
member this.Location = location
type Location(name, employees: ParameterizedEmployee<Location> list) =
let mutable employees = employees
member this.Name = name
member this.Employees = employees
member this.SetEmployees es =
employees <- es
type Employee = ParameterizedEmployee<Location>
module Test =
let location = new Location("CA",[])
let alice = new Employee("Alice",location)
let bob = new Employee("Bob",location)
location.SetEmployees [alice;bob]
location.Employees // non-empty!
|> List.iter (fun employee ->
printfn "employee %s works at %s" employee.Name employee.Location.Name)
Note that we create a type alias for Employee
, like this:
type Employee = ParameterizedEmployee<Location>
One nice thing about creating an alias like that is that the original code for creating employees will continue to work unchanged.
let alice = new Employee("Alice",location)
The code above assumes that the particular class being parameterized over is not important. But what if there are dependencies on particular properties of the type?
For example, let's say that the Employee
class expects a Name
property, and the Location
class expects an Age
property, like this:
module StructuralDependency_WithAge =
type Employee(name, age:float, location:Location) =
member this.Name = name
member this.Age = age
member this.Location = location
// expects Name property
member this.LocationName = location.Name
and Location(name, employees: Employee list) =
let mutable employees = employees
member this.Name = name
member this.Employees = employees
member this.SetEmployees es =
employees <- es
// expects Age property
member this.AverageAge =
employees |> List.averageBy (fun e -> e.Age)
module Test =
let location = new Location("CA",[])
let alice = new Employee("Alice",20.0,location)
let bob = new Employee("Bob",30.0,location)
location.SetEmployees [alice;bob]
printfn "Average age is %g" location.AverageAge
How can we possibly parameterize this?
Well, let's try using the same approach as before:
module StructuralDependencyWithAge_ParameterizedError =
type ParameterizedEmployee<'Location>(name, age:float, location:'Location) =
member this.Name = name
member this.Age = age
member this.Location = location
member this.LocationName = location.Name // error
type Location(name, employees: ParameterizedEmployee<Location> list) =
let mutable employees = employees
member this.Name = name
member this.Employees = employees
member this.SetEmployees es =
employees <- es
member this.AverageAge =
employees |> List.averageBy (fun e -> e.Age)
The Location
is happy with ParameterizedEmployee.Age
, but location.Name
fails to compile. obviously, because the type parameter is too generic.
One way would be to fix this by creating interfaces such as ILocation
and IEmployee
, and that might often be the most sensible approach.
But another way is to let the Location parameter be generic and pass in an additional function that knows how to handle it. In this case a getLocationName
function.
module StructuralDependencyWithAge_ParameterizedCorrect =
type ParameterizedEmployee<'Location>(name, age:float, location:'Location, getLocationName) =
member this.Name = name
member this.Age = age
member this.Location = location
member this.LocationName = getLocationName location // ok
type Location(name, employees: ParameterizedEmployee<Location> list) =
let mutable employees = employees
member this.Name = name
member this.Employees = employees
member this.SetEmployees es =
employees <- es
member this.AverageAge =
employees |> List.averageBy (fun e -> e.Age)
One way of thinking about this is that we are providing the behavior externally, rather than as part of the type.
To use this then, we need to pass in a function along with the type parameter. This would be annoying to do all the time, so naturally we will wrap it in a function, like this:
module StructuralDependencyWithAge_ParameterizedCorrect =
// same code as above
// create a helper function to construct Employees
let Employee(name, age, location) =
let getLocationName (l:Location) = l.Name
new ParameterizedEmployee<Location>(name, age, location, getLocationName)
With this in place, the original test code continues to work, almost unchanged (we have to change new Employee
to just Employee
).
module StructuralDependencyWithAge_ParameterizedCorrect =
// same code as above
module Test =
let location = new Location("CA",[])
let alice = Employee("Alice",20.0,location)
let bob = Employee("Bob",30.0,location)
location.SetEmployees [alice;bob]
location.Employees // non-empty!
|> List.iter (fun employee ->
printfn "employee %s works at %s" employee.Name employee.LocationName)
Now let's apply the functional design approach to this problem, just as we did before.
Again, we'll separate the types themselves from the functions that act on those types.
module StructuralDependencyExample_SeparateTypes =
module DomainTypes =
type Employee = {name:string; age:float; location:Location}
and Location = {name:string; mutable employees: Employee list}
module Employee =
open DomainTypes
let Name (employee:Employee) = employee.name
let Age (employee:Employee) = employee.age
let Location (employee:Employee) = employee.location
let LocationName (employee:Employee) = employee.location.name
module Location =
open DomainTypes
let Name (location:Location) = location.name
let Employees (location:Location) = location.employees
let AverageAge (location:Location) =
location.employees |> List.averageBy (fun e -> e.age)
module Test =
open DomainTypes
let location = { name="NY"; employees= [] }
let alice = {name="Alice"; age=20.0; location=location }
let bob = {name="Bob"; age=30.0; location=location }
location.employees <- [alice;bob]
Location.Employees location
|> List.iter (fun e ->
printfn "employee %s works at %s" (Employee.Name e) (Employee.LocationName e) )
Before we go any further, let's remove some unneeded code. One nice thing about using a record type is that you don't need to define "getters", so the only functions you need in the modules
are functions that manipulate the data, such as AverageAge
.
module StructuralDependencyExample_SeparateTypes2 =
module DomainTypes =
type Employee = {name:string; age:float; location:Location}
and Location = {name:string; mutable employees: Employee list}
module Employee =
open DomainTypes
let LocationName employee = employee.location.name
module Location =
open DomainTypes
let AverageAge location =
location.employees |> List.averageBy (fun e -> e.age)
Once again, we can remove the dependency by creating a parameterized version of the types.
Let's step back and think about the "location" concept. Why does a location have to only contain Employees? If we make it a bit more generic, we could consider a location as being a "place" plus "a list of things at that place".
For example, if the things are products, then a place full of products might be a warehouse. If the things are books, then a place full of books might be a library.
Here are these concepts expressed in code:
module LocationOfThings =
type Location<'Thing> = {name:string; mutable things: 'Thing list}
type Employee = {name:string; age:float; location:Location<Employee> }
type WorkLocation = Location<Employee>
type Product = {SKU:string; price:float }
type Warehouse = Location<Product>
type Book = {title:string; author:string}
type Library = Location<Book>
Of course, these locations are not exactly the same, but there might be something in common that you can extract into a generic design, especially as there is no behavior requirement attached to the things they contain.
So, using the "location of things" design, here is our dependency rewritten to use parameterized types.
module StructuralDependencyExample_SeparateTypes_Parameterized =
module DomainTypes =
type Location<'Thing> = {name:string; mutable things: 'Thing list}
type Employee = {name:string; age:float; location:Location<Employee> }
module Employee =
open DomainTypes
let LocationName employee = employee.location.name
module Test =
open DomainTypes
let location = { name="NY"; things = [] }
let alice = {name="Alice"; age=20.0; location=location }
let bob = {name="Bob"; age=30.0; location=location }
location.things <- [alice;bob]
let employees = location.things
employees
|> List.iter (fun e ->
printfn "employee %s works at %s" (e.name) (Employee.LocationName e) )
let averageAge =
employees
|> List.averageBy (fun e -> e.age)
In this revised design you will see that the AverageAge
function has been completely removed from the Location
module. There is really no need for it, because we can do these
kinds of calculations quite well "inline" without needing the overhead of special functions.
And if you think about it, if we did need to have such a function pre-defined, it would probably be more appropriate to put in the Employee
module rather than the Location
module.
After all, the functionality is much more related to how employees work than how locations work.
Here's what I mean:
module Employee =
let AverageAgeAtLocation location =
location.things |> List.averageBy (fun e -> e.age)
This is one advantage of modules over classes; you can mix and match functions with different types, as long as they are all related to the underlying use cases.
In the examples so far, the "list of things" field in location has had to be mutable. How can we work with immutable types and still support relationships?
Well one way not to do it is to have the kind of mutual dependency we have seen. In that design, synchronization (or lack of) is a terrible problem
For example, I could change Alice's location without telling the location she points to, resulting in an inconsistency. But if I tried to change the contents of the location as well, then I would also need to update the value of Bob as well. And so on, ad infinitum. A nightmare, basically.
The correct way to do this with immutable data is steal a leaf from database design, and extract the relationship into a separate "table" or type in our case. The current relationships are held in a single master list, and so when changes are made, no synchronization is needed.
Here is a very crude example, using a simple list of Relationship
s.
module StructuralDependencyExample_Normalized =
module DomainTypes =
type Relationship<'Left,'Right> = 'Left * 'Right
type Location= {name:string}
type Employee = {name:string; age:float }
module Employee =
open DomainTypes
let EmployeesAtLocation location relations =
relations
|> List.filter (fun (loc,empl) -> loc = location)
|> List.map (fun (loc,empl) -> empl)
let AverageAgeAtLocation location relations =
EmployeesAtLocation location relations
|> List.averageBy (fun e -> e.age)
module Test =
open DomainTypes
let location = { Location.name="NY"}
let alice = {name="Alice"; age=20.0; }
let bob = {name="Bob"; age=30.0; }
let relations = [
(location,alice)
(location,bob)
]
relations
|> List.iter (fun (loc,empl) ->
printfn "employee %s works at %s" (empl.name) (loc.name) )
Or course, a more efficient design would use dictionaries/maps, or special in-memory structures designed for this kind of thing.
Finally, let's look at an "inheritance dependency".
- Type A stores a value of type B in a property
- Type B inherits from type A
We'll consider a UI control hierarchy, where every control belongs to a top-level "Form", and the Form itself is a Control.
Here's a first pass at an implementation:
module InheritanceDependencyExample =
type Control(name, form:Form) =
member this.Name = name
abstract Form : Form
default this.Form = form
and Form(name) as self =
inherit Control(name, self)
// test
let form = new Form("form") // NullReferenceException!
let button = new Control("button",form)
The thing to note here is that the Form passes itself in as the form
value for the Control constructor.
This code will compile, but will cause a NullReferenceException
error at runtime. This kind of technique will work in C#, but not in F#, because the class initialization logic is done differently.
Anyway, this is a terrible design. The form shouldn't have to pass itself in to a constructor.
A better design, which also fixes the constructor error, is to make Control
an abstract class instead, and distinguish between non-form child classes (which do take a form in their constructor)
and the Form
class itself, which doesn't.
Here's some sample code:
module InheritanceDependencyExample2 =
[<AbstractClass>]
type Control(name) =
member this.Name = name
abstract Form : Form
and Form(name) =
inherit Control(name)
override this.Form = this
and Button(name,form) =
inherit Control(name)
override this.Form = form
// test
let form = new Form("form")
let button = new Button("button",form)
To remove the circular dependency, we can parameterize the classes in the usual way, as shown below.
module InheritanceDependencyExample_ParameterizedClasses =
[<AbstractClass>]
type Control<'Form>(name) =
member this.Name = name
abstract Form : 'Form
type Form(name) =
inherit Control<Form>(name)
override this.Form = this
type Button(name,form) =
inherit Control<Form>(name)
override this.Form = form
// test
let form = new Form("form")
let button = new Button("button",form)
I will leave a functional design as an exercise for you to do yourself.
If we were going for truly functional design, we probably would not be using inheritance at all. Instead, we would use composition in conjunction with parameterization.
But that's a big topic, so I'll save it for another day.
I hope that this post has given you some useful tips on removing dependency cycles. With these various approaches in hand, any problems with module organization should be able to be resolved easily.
In the next post in this series, I'll look at dependency cycles "in the wild", by comparing some real world C# and F# projects.
As we have seen, F# is a very opinionated language! It wants us to use modules instead of classes and it prohibits dependency cycles. Are these just annoyances, or do they really make a difference to the way that code is organized? Read on and find out!