Design Philosophy

Strongback uses several design patterns throughout its API. This section talks abstractly about the philosophy. You’ll see later on how these characteristics manifest themselves in different parts of the library.

Many of these philosophies are common practice in professional software. Together, they make code easy to read, understand, reason about, write, and test.

Functional interfaces

The Strongback API uses lots interfaces, and we tried to keep the interfaces as small and focused as possible. You’ll also see quite a few default methods on those interfaces — this is something that was added in Java 8, and it allows the interface to define a method and a default implementation. Implementing classes get this default method for free, although they can always override any of the methods in the interfaces. Interface methods that do not have a default implementation are referred to as "abstract methods".

Functional interfaces were also new in Java 8: a functional interface is any interface that has exactly one abstract method. Here’s an example of a functional interface used in Strongback:

public interface DistanceSensor {

    /**
     * Gets the current distance, in inches, relative to the zero-point.
     *
     * @return the distance in inches
     */
    public double getDistanceInInches();

    /**
     * Gets the current value of this {@link DistanceSensor} in feet.
     *
     * @return the value of this {@link DistanceSensor}
     * @see #getDistanceInInches()
     */
    default public double getDistanceInFeet() {
        return getDistanceInInches() / 12.0;
    }
}

This interface represents a sensor that can detect the distance from the sensor to some object placed in front of the sensor. It has one abstract method, getDistanceInInches() that returns the distance in inches, and one default method getDistanceInFeet() that returns the distance in feet and that is implemented in terms of the abstract method. This is a functional interface in Java 8, which means it can be implemented with only a single function that takes no parameters and returns a double. You can do this the traditional way by defining a class that extends the DistanceSensor interface, or you can use a lambda to define that single function.

Using lambdas is incredibly powerful. Let’s imagine a method that takes a DistanceSensor:

public double computeShootingAngle( DistanceSensor sensor ) {
    ...
}

If we have a DistanceSensor object, then we can pass it as a parameter to the method:

DistanceSensor mySensor = ...
double angle = computeShootingAngle(mySensor);

But what if the geometry of our robot and the field were such that we needed to always add 10 inches to our the distance measured by our sensor? We can very easily use a lambda to define a function that took no parameters and return a double:

DistanceSensor mySensor = ...
double angle = computeShootingAngle(()->mySensor.getDistanceInInches() + 10.0);

Or we might not even have a real distance sensor, but we instead calculate the distance based using vision. In that case, we probably have a method somewhere that returns the calculated distance:

public class VisionModel {
    ...
    public double estimateDistance() {...}
    ...
}

Our class doesn’t have to implement or contain a DistanceSensor, and its estimateDistance() method isn’t even named the same as DistanceSensor.getDistanceInInches(). Yet we still can have computeShootingAngle use that method by passing a lambda that calls our vision model:

VisionModel visionModel = ...
double angle = computeShootingAngle(()->visionModel.estimateDistance());

Or, since estimateDistance() takes no parameters and returns a double, we can alternatively pass a method reference rather than a lambda. The following code is identical to the previous snippet:

VisionModel visionModel = ...
double angle = computeShootingAngle(visionModel::estimateDistance);

Functional interfaces, lambdas, and method references take some getting used to, but they are a very powerful new addition to Java 8 and help to keep your code as small and simple as possible.

Immutable

Where possible Strongback tries to use immutable objects. Immutable objects appear to external observers as fixed and unchangeable. This means that you can pass around immutable objects without worrying that some other component might change them. And because immutable objects never change, concurrent access by multiple threads is trivial: no synchronization, no volatiles, no locking, and no race conditions to worry about.

Immutable classes also tend to be simpler, since there’s no need for setters and fields are often final.

All immutable Strongback classes are annotated with the @Immutable annotation.

Injection

When an object requires references to one or more other objects, we very often will require all of those references be passed into the constructor, and the class will then store those references using final fields. This is a form of injection, which is just a fancy way of saying that when a component is created it should be handed all the objects it needs.

For example, imagine a class that requires a Motor and DistanceSensor. The constructor should take references to these objects and use them throughout its lifetime. When used on the robot, the code can pass in the real motor and distance sensor into the component, while a test case can easily pass in a mock motor and mock distance sensor.

This is also true for more significant, resource-intensive objects. Classes should never create threads; instead, they should be handed an Executor or even a Supplier<Thread> which they can use to run code asynchronously.

The bottom line is many class should depend only upon objects injected via the constructor, and should never know how to find one. This helps keep these classes easily testable.

Minimal requirements

When using injection, be sure to the constructor uses the most minimal interface or class. For example, if a component uses the current speed of a motor and only uses the getSpeed() method. So the constructor should not take a Motor or LimitedMotor, but should instead take a SpeedSensor.

This helps minimize dependencies, and it documents the developer’s intent (in our example, that the component should only be reading the speed). It also makes testing easier, since tests need to mock fewer and less complicated objects.

Fluent APIs

It’s fairly common in Java libraries for methods like setters to simply return void. After all, the methods change the object and you already have a reference to the object (otherwise you wouldn’t be able to invoke the method).

Where possible, Strongback methods that normally would return void often return a reference to the target of the method. The sole purpose is to allow methods to be chained together. And, like many modern Java libraries, we’ve abandoned the outdated pattern of naming all setters with set* and instead carefully name our methods to be easily readable.

An API that uses these patterns is called a fluent API, and if properly designed these APIs make your code easy to write (by leveraging code completion features in your IDE) and easy to read.

Consider the AngleSensor class, which defines these two methods (among others):

public interface AngleSensor extends Zeroable {

    /**
     * Gets the angular displacement in continuous degrees.
     * @return the positive or negative angular displacement
     */
    public double getAngle();

    /**
     * Change the output so that the current angle is considered to be 0.
     * @return this object to allow chaining of methods; never null
     */
    @Override
    default public AngleSensor zero() {...}

    ...
}

This normally zero() might return void, but having the method return the same AngleSensor (e.g., this) allows us to chain multiple methods together:

AngleSensor sensor = Hardware.AngleSensors.potentiometer(3,28.8).zero();

Another more meaningful example is that Strongback can be easily configured using a chain of method calls:

Strongback.configure().recordNoEvents()
                      .recordCommands()
                      .useSystemLogger(Level.DEBUG)
                      .initialize();

A third example is a bit more involved, but except for the imports this is a complete, working example of a tank drive robot with 4 motors:

public class SimpleTankDriveRobot extends IterativeRobot {

    private static final int JOYSTICK_PORT = 1; // in driver station
    private static final int LF_MOTOR_PORT = 1;
    private static final int LR_MOTOR_PORT = 2;
    private static final int RF_MOTOR_PORT = 3;
    private static final int RR_MOTOR_PORT = 4;

    private TankDrive drive;
    private ContinuousRange driveSpeed;
    private ContinuousRange turnSpeed;

    @Override
    public void robotInit() {
        // Set up the robot hardware ...
        Motor left = Motor.compose(Hardware.Motors.talon(LF_MOTOR_PORT),
                                   Hardware.Motors.talon(LR_MOTOR_PORT));
        Motor right = Motor.compose(Hardware.Motors.talon(RF_MOTOR_PORT),
                                    Hardware.Motors.talon(RR_MOTOR_PORT)).invert();
        drive = new TankDrive(left, right);

        // Set up the human input controls for teleoperated mode.
        FlightStick joystick = Hardware.HumanInterfaceDevices.logitechAttack3D(JOYSTICK_PORT);

        // We want to use the Logitech Attack 3D's throttle as a "sensitivity"
        // input to scale the drive speed and throttle, so we'll map it
        // from it's native [-1,1] to a simple scale factor of [0,1] ...
        ContinuousRange sensitivity = joystick.getThrottle().map(t -> (t + 1.0) / 2.0);

        // scale the pitch ...
        driveSpeed = joystick.getPitch().scale(sensitivity::read);

        // scale and invert the roll ...
        turnSpeed = joystick.getRoll().scale(sensitivity::read).invert();

        // Set up the data recorder to capture the left & right motor speeds
        // (since both motors on the same side should be at the same speed,
        // we can just record the speed of the composed motors) and the sensitivity.
        // We have to do this before we start Strongback...
        Strongback.dataRecorder()
                  .register("Left motors", left)
                  .register("Right motors", right)
                  .register("Sensitivity", sensitivity.scaleAsInt(1000));
    }

    @Override
    public void teleopInit() {
        // Start Strongback functions ...
        Strongback.start();
    }

    @Override
    public void teleopPeriodic() {
        drive.arcade(driveSpeed.read(), turnSpeed.read());
    }

    @Override
    public void disabledInit() {
        // Tell Strongback that the robot is disabled so it can flush and kill commands.
        Strongback.disable();
    }
}

results matching ""

    No results matching ""