Skip to content
fwereade edited this page Dec 3, 2015 · 7 revisions

Facades

A facade is a collection of API methods organised for a specific purpose. It's a pretty fundamental concept: our versioning is at facade granularity, and a facade's most fundamental responsibility is to validate incoming API calls and ensure that they're sensible before taking action.

What exactly is a Facade?

Any type can be a facade; but if it doesn't have exported methods with suitable signatures, it won't be very interesting. Specifically, any exported method which (1) takes either 0 or 1 args; and (2) returns either (result) or (result, error): will be exposed over the websocket rpc connection.

What creates a Facade?

When an api request is received, the apiserver chooses a facade constructor, based on the request's rootName and version; then constructs the facade and calls the method corresponding to the request's methodName. (Many details elided.) Note that the facade is created for the use of, and persists only for the lifetime of, a single request.

Why would I want to write a Facade?

If any of the following conditions applies:

  • you're writing a new worker inside an agent
  • you're changing some existing worker, and it needs new capabilities
  • you're exposing some new domain of functionality to an external user
  • you're changing or extending some such exposed functionality

...you need to be writing and registering a new Facade. If it's a new worker or a new domain, it should definitely be in a new subpackage of apiserver; if it's a change to an existing one it still needs a new facade -- to be registered under a new version -- but it should go in the same package as its predecessors.

How do I write a Facade?

  • don't import state

  • really, please, don't import state

  • define or locate interfaces that expose the capabilities your facade requires

    • these might well be or want to be defined under core, if they're semi-mature
    • if it's a locally defined interface that's fine too
    • if it's defined somewhere else in the codebase, be suspicious: probably, either you're depending on unnecessary concretions, or that interface is defined in the wrong place.
  • write a simple constructor for your facade, most likely using the config-struct pattern, and making sure to include an apiserver/common.Authorizer:

    package whatever
    
    // Backend exposes capabilities required by the whatever facade.
    // It's an example of a locally defined interface that's fine.
    // Like all good interfaces, it should be tightly focused on its
    // actual purpose and prefer not to include methods that won't be
    // used.
    type Backend interface {
        DoSomething(to Something) error
        // more..?
    }
    
    // Config holds the dependencies and parameters needed for a whatever Facade.
    //
    // Keep in mind that this is *completely specific to a class of client*: the
    // internal `whatever` facade is intended for the exclusive use of the `whatever`
    // worker; and all external facades are for the exclusive use of, uh, external
    // users.
    //
    // That is to say: there probably won't be much to configure apart from the
    // direct dependencies. Anything else is part of the fundamental character of
    // the facade in play, and shouldn't be tweakable.
    type Config struct {
        Auth    common.Authorizer
        Backend Backend
        // more..? many facades will want to access several distinct backend capabilities,
        // consider carefully the granularity at which you expose them.
    }
    
    // Validate returns an error if any part of the Config is unsuitable.
    func (config Config) Validate() error {
    
        // This bit is significant! This is the moment at which you get to choose who can
        // use the facade at all. So a user-facing facade would want to AuthClient(), and
        // return false; an environment-management job would want to AuthEnvironManager();
        // or perhaps this is just for some worker that runs on all machine agents, in which
        // case we do:
        if !config.Auth.AuthMachineAgent() {
            return common.ErrPerm
        }
    
        // May as well check these too, it's better than panicking if someone somewhere
        // has messed up and passed us garbage.
        if config.Backend == nil {
            return errors.NotValidf("nil Backend")
        }
    }
    
    // NewFacade creates a Facade according to the supplied configuration, or
    // returns an error. Note that it returns a concrete exported type, which
    // is right and proper for a constructor.
    func NewFacade(config Config) (*Facade, error) {
        if err := config.Validate(); err != nil {
            return nil, errors.Trace(err)
        }
        return Facade{config}, nil
    }
    
    // Facade exposes methods for the use of the whatever worker. It lives only
    // for the duration of a single method call, and should usually be stateless
    // in itself.
    type Facade struct {
        config Config
    }
    
    // ...and now define your methods.
    

How do I test a Facade?

How do I register my Facade?

note this dependency on mutable global state is bad. If someone were to fix it I would be most grateful.

  • in your facade package
Clone this wiki locally