In the following imaginary scenario, we are working for a start-up company that is building the software that's responsible for controlling fully automated warehouses. Autonomous robots equipped with gripping arms and lasers (what could possibly go wrong?) are busy moving around the warehouse floor locating and picking up order items from the shelves and placing them in cardboard boxes that are then shipped to the customers.
This is a tentative definition for a warehouse Robot:
package warehouse import "context" // Robot navigates the warehouse floor and fetches items for packing. type Robot struct { // various fields } // AcquireRobot blocks until a Robot becomes available or until the
// context expires. func AcquireRobot(ctx context.Context) *Robot { //... } // Pack instructs the robot to pick up an item from its shelf and place // it into a box that will be shipped to the customer. func (r *Robot) Pack(item *entity.Item, to *entity.Box) error { //... }
In the preceding code snippet, the Item and Box types live in an external package called entity. All goes well until one day when someone attempts to introduce a new helper method to the Box type, which, unfortunately, introduces an import cycle:
package entity // Box contains a list of items that are shipped to the customer. type Box struct { // various fields } // Pack qty items of type i into the box. func (b *Box) Pack(i *Item, qty int) error { robot := warehouse.Acquire() // compile error: import cycle detected // ... }
Technically speaking, this is a bad design decision: boxes and items should not really be aware of the robot's existence. However, for the sake of this argument, we will ignore this design flaw and try to work around this problem using Go's support for implicit interfaces. The first step would be to define a Packer interface within the entity package. Secondly, we would need to provide an abstraction for obtaining an instance of Packer, as shown in the following code snippet:
package entity import "context" // Packer is implemented by objects that can pack an Item into a Box. type Packer interface { Pack(*Item, *Box) error } // AcquirePacker returns a Packer instance. var AcquirePacker func(context.Context) Packer
With these two mechanisms in place, the helper method can work without the need to import the warehouse package:
// Pack qty items of type i into the box. func (b *Box) Pack(i *Item, qty int) error { p := AcquirePacker(context.Background()) for j := 0; j < qty; j++ { if err := p.Pack(i, b); err != nil { return err } } return nil }
The last bit of the puzzle that we need to address is how we are going to initialize AcquirePacker without importing the warehouse package. The only way we can do that is via a third package that imports the warehouse and entity packages:
package main import "github.com/achilleasa/logistics/entity" import "github.com/achilleasa/logistics/warehouse" func wireComponents() { entity.AcquirePacker = func(ctx context.Context) entity.Packer { return warehouse.AcquireRobot(ctx) } }
In the preceding code snippet, the wireComponents function ensures that the warehouse and entity packages are wired together without triggering any circular dependency errors.