Sunday, January 1, 2012

Interfaces in Go - Part 2: Aiding adaptable, evolutionary design


In discussing interfaces in the first post, I had started off with a familiar example from OOP - a Shaper interface and a couple of concrete shapes that implemented the interface. But as Rob pointed out in this this groups thread, "Go's interfaces aren't a variant on Java or C# interfaces, they're much more. They are a key to large-scale programming and adaptable, evolutionary design."

Let's first go over Java interfaces quickly and then compare it to how Go's interfaces work to find out how it could aid better extensibility and evolution. Our data structure will be a Bus which can be looked at in two ways via interfaces- a box which has a volume, and as a transport vehicle capable of carrying a certain number of passengers.

code for Java class
Bus.java
//OOP Step 1: design your interface and class hierarchy

//OOP Step 1.1: Pre-define what real-world abstractions could use the data we will define
interface PublicTransport {
    int PassengerCapacity();
}

//OOP Step - repeat for any other interfaces
interface Cuboid {
    int CubicVolume();
}

//OOP Step 2: Create data structures and implement all interfaces we have already defined in our class hierarchy
public class Bus implements 
PublicTransport, 
Cuboid {

    //OOP Step 2.1: Define data structures for class
    int l, b, h, rows, seatsPerRow;

    public Bus(int l, int b, int h, int rows, int seatsPerRow) {
        this.l = l; this.b = b; this.h = h; this.rows = rows; this.seatsPerRow = seatsPerRow;
    }

    //OOP Step 2.2: Define method implementation
    public int CubicVolume() { return l*b*h; }

    public int PassengerCapacity() { return rows * seatsPerRow; }

    //OOP Step 3: Use the classes and methods in main program
    public static void main() {
        Bus b = new Bus(10, 6, 3, 10, 5);
        System.out.Println(b.CubicVolume());
        System.out.Println(b.PassengerCapacity()); 
    }
}

Note: In the above Java example, you have to explicitly define class hierarchy ahead of implementation by using specific keywords like implements. Languages like C# list the interfaces after a ":" instead of the implements keyword. In such cases, any project requirement changes affecting these areas will require changes to these core modules.

Most large projects tend to start out somewhat in that sequence, going back and forth between the interface definitions and data structures, during a fairly long period of architecture and design, until all factors encompassing all the requirements have been considered in the design of the class hierarchy - quite a hairy task given the nature of changing project requirements. After that, the class hierarchy is quite firmly established and changes are usually not encouraged without considerable reasoning. In certain projects that I worked on, the architecture was provided by specialist architects, which then was unchangeable except through committee meetings. Which seemed fair at the time given the complexity of the systems and how changes to core elements like interfaces could cause multiple downstream changes, as all implementing classes would need to be updated. An alternative would be to create new classes and wrap existing classes around them with new interfaces. Beyond a certain size and complexity, these started getting out of hand. To manage complexities and to modularize changes, other popular workarounds emerged - like Design Patterns, which had predefined and standard class hierarchies which people were expected to at least understand while implementing the class methods. I must confess that I was quite in love with the intellectual complexity of Design Patterns and OOP design process. But looking back, that intellectual masochism did not always match our productivity. The design patterns themselves were quite a few in number and demanded a certain steep learning curve and a refreshment course each time we had to go back to the core design. Did it have to be that difficult or elitist? Can Go provide anything simpler to get results compared to spending too much time on that elusively perfect design?

Now let's look at the Go example on similar lines.

bus.go
package main

import "fmt"

//Go Step 1: Define your data structures
type Bus struct {
    l, b, h int
    rows, seatsPerRow int
}

//Go Step 2: Define a real world abstraction that could use the data we structure we have
type Cuboider interface {
    CubicVolume() int
}

//Go Step 3: Implement methods to work on data
func (bus Bus) CubicVolume() int {
    return bus.l *  bus.b * bus.h
}

//Go step - repeat 2 & 3 for any other interfaces
type PublicTransporter interface  {
    PassengerCapacity() int
}

func (bus Bus) PassengerCapacity() int {
    return bus.rows * bus.seatsPerRow
}

func main() {
    b := Bus{
             l:10, b:6, h:3,
             rows:10, seatsPerRow:5}

    fmt.Println("Cubic volume of bus:", b.CubicVolume())
    fmt.Println("Maximum number of passengers:", b.PassengerCapacity())
}

All the nouns used for the interfaces and types are similar, but there are some differences. One of which is a lack of a keyword like implements which is used to define hierarchy in Java. Also, it appears to be data centric - define your data first and build your interface abstractions as you go along. Hierarchy here is kind of built 'along the way' without explicitly stating it - depending on the method signatures associated with the type, it is understood as implementing specific interfaces. At this point in this short example, this crucial difference might not seem to be much of a difference since we achieve the same functionality, but when the program evolves and we need to add other real world abstractions, the Go approach will prove useful.

Let us assume now that as time evolved, some of the project requirements for our Bus changed - there is now a new law that says that each passenger should at least have a certain minimum amount of cubic volume. Our Bus now now has to adhere to a new interface called PersonalSpaceLaw which is distinct from any of the other interfaces it already implements. In Java and many other OOP languages we would have to do something like the following to accommodate this change:

partial Java code
//new requirement that the Bus must be compatible with
interface PersonalSpaceLaw {
    boolean IsCompliantWithLaw();
}

class Bus implements 
PublicTransport, 
Cuboid, 
PersonalSpaceLaw {

    //... other existing code

    public IsCompliantWithLaw() 
    {
        return ( l * b * h ) / ( rows * seatsPerRow ) > 3;
    }
}


This functional change requirement called for a change to the class hierarchy which includes modifying our core classes, thus making it vulnerable to negative side effects across the board - a case for convening the 'architects committee'. Avoiding this and subclassing and wrapping classes is also another possibility, but still calls for 'architects committee' to make decisions regarding the class hierarchy. (To quote Jesse McNeils from the thread, "You have to convince whoever is in charge of "class C" to add this interface to the implements list.")

Let us see if it can be easier in Go.

partial code in Go
//new requirement that the Bus must be compatible with
type PersonalSpaceLaw interface {
    IsCompliantWithLaw() bool
}

func (b Bus) IsCompliantWithLaw() bool {
    return (b.l * b.b * b.h) / (b.rows * b.seatsPerRow) >= 3
}

As you may notice, the functionality has been extended without any change to the core classes or core hierarchies. This implementation is much cleaner, easily extensible, and can scale better with the changing needs of the project's requirements.

To summarize the advantage in this approach I'll quote John Asmuth from the thread about the productivity of interfaces in Go, "It's the fact that I don't have to spend time up front designing some sort of type hierarchy and then rearranging it two or three times before I finish. It's not even the fact that it's easy to do it right - it's the fact that I just don't have to worry about it and can get on with the actual algorithm."

Note: Here we are not compromising or discounting the value of good, thoughtful, clean design. However, the designers are less bound to have the design be past, present, and future perfect encompassing even vaguely possible, and even unknown, future needs.


Full code - bus.go
package main

import "fmt"

type Bus struct {
    l, b, h int
    rows, seatsPerRow int
}

type Cuboider interface {
    CubicVolume() int
}

func (b Bus) CubicVolume() int {
    return b.l * b.b * b.h
}

type PublicTransporter interface {
    PassengerCapacity() int
}

func (b Bus) PassengerCapacity() int {
    return b.rows * b.seatsPerRow
}

func main() {
    b := Bus{
             l:10, b:6, h:3,
             rows:10, seatsPerRow:5}

    fmt.Println("Cubic volume of b:", b.CubicVolume())
    fmt.Println("Maximum number of passengers:", b.PassengerCapacity())
    fmt.Println("Is compliant with law:", b.IsCompliantWithLaw())
}

type PersonalSpaceLaw interface {
    IsCompliantWithLaw() bool
}

func (b Bus) IsCompliantWithLaw() bool {
    return (b.l * b.b * b.h) / (b.rows * b.seatsPerRow) >= 3
}


14 comments:

  1. Is there a reason why you did not call CubicVolume or PassengerCapacity in IsCompliantWithLaw in your full code?

    ReplyDelete
  2. Why is it you insist on defining the interfaces all the time? Couldn't you simply get away with just defining the bus-struct and then the methods that uses it?

    ReplyDelete
    Replies
    1. From my understanding, (maybe it is slightly different in Go but) Interface is like a C/C++ header file or a list of functions available in a binary Dynamic library. I mean it is for other programmers to see the list of functions available in your Program or Library or Module or whatever (you name it) etc. Imagine it is a closed source project. You only have to give other programmers a binary library and an Interface. As long as the input and the output are correct other programmers don't need to worry about how the function is written and makes it cleaner for them to read. More importantly it is easier to version control.

      Delete
  3. "Is there a reason why you did not call CubicVolume or PassengerCapacity in IsCompliantWithLaw in your full code?"

    No there isn't, you could use the existing functions and get the same result.

    ReplyDelete
  4. "Why is it you insist on defining the interfaces all the time? Couldn't you simply get away with just defining the bus-struct and then the methods that uses it?"

    Hello Paddie, what you are saying, though possible, would not illustrate what I intend to about the special feature of Go interfaces.
    Yet this is not a forced example. In this design, our core data structure is not changing - which is what I would prefer. A vehicle law would be applicable to all vehicles, a Bus being one of them, and would be best defined as a separate interface and 'applied' to all vehicles as the example shows.

    ReplyDelete
  5. To address Paddie's question further: there is no _actual_ need to specify the interface, until we need to give a type to an argument,etc. Even if not, they are useful documentation.

    It's an entertaining fact that (for instance) os.Error and fmt.Stringer are exactly compatible: they are just interfaces with a String() string method. A function that needs a Stringer will also be totally happy with an os.Error; that's the beauty of the system.

    steve d.

    ReplyDelete
  6. RE: "You have to convince whoever is in charge of "class C" to add this interface to the implements list."

    Don't you still have to convince whoever is in charge of the package containing type Bus to add func (b Bus) IsCompliantWithLaw() bool {...} to the package?

    ReplyDelete
  7. Hello Chris, you are correct as far as one "cannot define new methods on non-local type", and one needs to make updates to the package. But this is different from changing the core classes. In larger projects a change to a central class has large ripple effects - developers will be nervous to ever touch something that has worked fairly ok until then :-). With Go one needs to update the package, true, but because it is more loosely connected and less explicit, the side effects in making that change are independent without side effects on other parts of the code that derive or use the said class.

    ReplyDelete
  8. Embedding lets clients create augmented versions of things they don't own:

    type Bus {
    their.Bus
    }

    func (b Bus) blah() { ... }

    ReplyDelete
  9. So in these examples you don't really need the interface declarations. Can you give a basic example of when you do need them, or where you can use them to achieve something that is more complex without an interface? This tutorial still doesn't tell me why an interface is useful.

    ReplyDelete
  10. Hello Peter, I could direct you to my other first level Go Interfaces tutorial here: http://golangtutorials.blogspot.com/2011/06/interfaces-in-go.html, but that also assumes that you already know a little OOP. So based on your question, it looks like you are looking for a basic "need for interfaces" kind of topic. You could search for "OOP interface" and you'll find quite a few you can start on; after that you might find this write up more useful.

    ReplyDelete
  11. I have to agree that defining an interface here besides good practice is a bit pointless. I would like to see how you use a design like this but instead of instantiating a bus Object and use it directly do polymorphism. E.g. pass the object to a function as a parameter (we don't care what object that is) and it will work as long as the object adheres to both Cuboider and PublicTransporter. This probably requires a third interface and managing this in Java becomes a pain.

    A example of dependency injection like this more likely to make its way into real code.

    ReplyDelete
  12. In Java, interfaces are used due to the absence of multiple inheritance. Since Go has multiple inheritance anyway, what's the point of interfaces?

    ReplyDelete
  13. IMHO the benefits of interfaces are shown when the program grows more complex, in the example above there's no immediate benefit that *bus* implements the interface *PublicTransporter*. But if you have a tank also, for example it would implement Cuboid but not PublicTransporter as it's not qualified for that. And for example if you need to print the passenger capacity of a vehicle, a function would accept as input the interface PublicTransporter, and that makes sure that only the bus gets in and not the tank as it does not have the method to count passengers that PublicTransporter interface demands. This way you make sure that your function only deals with public transporters, and trying to pass tank would result in a compilation error since this is statically checked.

    ReplyDelete

If you think others also will find these tutorials useful, kindly "+1" it above and mention the link in your own blogs, responses, and entries on the net so that others also may reach here. Thank you.