API Design

Ice breaker

export zord {

    assemble(parts: ZordPart[]): Megazord;

    dissemble(megazord: Megazord): ZordPart[];
}

<API client>

Megazord API

import * from "zord"

zordParts = ["T-Rex", "Mammoth", ... "Sabertooth"]

const megazord = zord.assemble(zordParts)

Megazord API

Megazord API

import * from "zord"

zordParts = ["T-Rex", "Mammoth", ... "Sabertooth"]

const megazord = zord.assemble(zordParts)

Megazord API

import * from "zord"

zordParts = ["T-Rex", "Cannons", ... "Sabertooth"]

const megazord = zord.assemble(zordParts)

Megazord API

  1. Concise
  2. Does one thing and does it well
  3. Methods clearly state input/output
  4. Hides from the user all the logic of (dis)assembling the pieces toghether

What's wrong here?

export zord {

    assemble(parts: ZordPart[], 
             assemblingOrder: number[]): Megazord;
}

Megazord API

export zord {

    assemble(parts: ZordPart[], 
             assemblingOrder: number[]): Megazord;
}

exposes implementation details

export zord {

    assemble(parts: ZordPart[], 
             name: string,
             specialMove: string): Megazord;
}

What's wrong here?

Megazord API

export zord {

    assemble(parts: ZordPart[], 
             name: string,
             specialMove: string): Megazord;
}

1. misplaceable

2. hardly extensible

Designing APIs is HARD

Autobot API

1. Throughout this tutorial, we will design and refine the autobot API

 

Activity #1: Write a client for our API

 

Activity #2: Define an interface for our API

 

Activity #3: Define types for our API

 

Activity #4: Think about extensibility

 

Instructions

1. Under the Github organization find the tutorial2 repo

2. Clone the repo

 

3. Open Autobot.spec.ts

 

4.Don't look at anything else or you will lose all the fun

https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/tutorial2.git

Autobot API - Overview

1. It should be used by course instructors and TAs

 

2. It should setup virtual environments to run tests against students' answers

 

3. Autobot contains courses and deliverables

 

4. Tests from a given deliverable must run against a given commit

< As designing APIs is hard. My API is far from perfect >

#1: Write a client for our API

1. In Autobot.spec.ts there are some variables

 

2. Using only features from your IDE, write a test that uses the autobot API to test "d0"

 

3. Don't look at the source code of Autobot!

 

4. I'll provide all method signatures of the API in the next minute

 

5. At the 5 mins mark, the signatures will be improved with some documentation

 

10~15 min

Autobot API

Autobot {


    test(csID: string, githubRepo: string, commitSHA: string, deliverable: string): Promise<number>
    

    getCourseID(githubRepo: string): string 


    getCourses(): string[] 


    exist(courseID: string): Promise<boolean> 


    register(courseID: string, instructor: string, tas: string[], students: string[]): Promise<boolean> 


    clone(githubRepo: string, commitSHA: string): Promise<boolean> 


    add(courseID: string, deliverable: string, testSuite: ITestSuite): Promise<boolean> 


    build(studentRepo: any): Promise<boolean> 


    testSuite(studentRepo: any, testSuite: any): Promise<number>


    setupPermission(instructor: string, tas: string[]): Promise<boolean>
}
Autobot {
    /** Test one commit from a user against a given deliverable. 
        Returns a grade */
    test(csID: string, githubRepo: string, commitSHA: string, deliverable: string): Promise<number>
    
    /** Obtains unique ID of a course based on a Github Repo. 
        Returns the course ID for that repo */
    getCourseID(githubRepo: string): string 

    /** List courses using Autobot */
    getCourses(): string[] 

    /** Check if a course exists 
        Returns true if course exists. false otherwise. */    
    exist(courseID: string): Promise<boolean> 

    /** Register a new course 
        Returns true if course was successfully registered. false otherwise */    
    register(courseID: string, instructor: string, tas: string[], students: string[]): Promise<boolean> 

    /** Clone a Github repo 
        Returns true if repo was successfully cloned. false otherwise */    
    clone(githubRepo: string, commitSHA: string): Promise<boolean> 

    /** Add a deliverable to a course 
        Returns true if deliverable was successfully added. false otherwise */    
    add(courseID: string, deliverable: string, testSuite: ITestSuite): Promise<boolean> 

    /** Build the student Github repo 
        Returns true if repo was successfully built. false otherwise */    
    build(studentRepo: any): Promise<boolean> 

    /** Run the test suite 
        Returns a grade */
    testSuite(studentRepo: any, testSuite: any): Promise<number>

    /** Setup permissions for course instructor and TAs */    
    setupPermission(instructor: string, tas: string[]): Promise<boolean>
}
describe("Autobot", () => {

    let autobot: Autobot;

    before(async () => {
        Log.info("before: setup a course project and its deliverables");
        autobot = new Autobot();
        const courseID = "cpsc310-2017w2";
        const instructor = "Reid Holmes";
        const tas = ["8d8", "4lom", "a290b"];
        const students = ["t1t0b", "r2d2", "c3p0", "t5t0b", "bb8", "t5t0b", "aat9", "a5f5t", "g2l5m"];

        await autobot.register(courseID, instructor, tas, students);
        await autobot.add(courseID, "d0", TestSuites.get("d0"));
        await autobot.add(courseID, "d1", TestSuites.get("d1"));
    });

    it("Should run all test for d0", async () => {
        Log.info("\n----------------------------------");
        const grade = await autobot.test(
            "t5t0b",
            "cpsc310-2017w2_team66",
            "37debc2b427003dcc4d12aea11b32ef0d6d9aa42",
            "d0");
        expect(grade).to.be.closeTo(97.00, 0.003);
    });
});

SA#1

< If you used more than that,
the logic of the API is leaking to the client >

#2: Define an interface for our API

1. Take a look at the source code of Autobot.ts

 

2. Consider which methods should be exposed and define them in IAutobot.ts

 

3. Change everythin else to private

 

10 min

export interface IAutobot {

    addCourse(courseID: string, instructor: string, tas: string[], students: string[]): Promise<boolean>;

    addDeliverable(courseID: string, deliverable: string, testSuite: ITestSuite): Promise<boolean>;

    testDeliverable(csID: string, githubRepo: string, commitSHA: string, deliverable: string): Promise<number>;
}

SA#2

< Favor private classes, fields & methods >

#3: Define types for our API

1. Imagine how you can encapsulate the parameters of test

 

2. Define types for the input/output of test

15 min

export interface IAutobot {

    addCourse(course: ICourse): Promise<IAutobotResponse>; 

    addDeliverable(deliverable: IDeliverable): Promise<IAutobotResponse>;

    testDeliverable(testRequest: IAutobotTestRequest): Promise<IAutobotResponse>;
}

export interface IAutobotTestRequest {
    userID: string;
    githubRepo: string;
    commitSHA: string;
    deliverable: string;
}

/** Similar to IInsightFacade */
export interface IAutobotResponse {
    code: number;
    body: IAutobotSuccessBody | IAutobotErrorBody;
}

SA#3

< Extensible. You can easily add new fields  to IAutobotTestRequest>

You would probably favor a
smaller name, e.g. ITestRequest

#4: Think about extensibility

  1. We need Autobot to run both in a desktop machine, or on the cloud.
     
  2. Remember that most of the workflow is still the same, but testDeliverable has to either clone, build, and run the test suite locally or remotely.
     
  3. How would you do that?

 

15 min

#4: Think about extensibility

  • Option 1: Create two autobot classes DesktopAutobot, and CloudAutobot that inherit from Autobot.

     
  • Option 2: Create a new interface IAutobotRunner and change Autobot constructor to receive a runner parameter.    

     
  • Option 3: Create an enumeration IExecutionType and change Autobot constructor to receive a executionType parameter.

 

 

15 min

  1. Write your solution.

 

#4: Think about extensibility

  • Option 3: Create an enumeration IExecutionType and change Autobot constructor to receive a executionType parameter.

 

 

<API owners>

OK. As a client, now I want to run on a datacenter.
Give me yet another version of Autobot!

#4: Think about extensibility

  • Option 1: Create two autobot classes DesktopAutobot, and CloudAutobot that inherit from Autobot

 

<API owners>

OK. As a client, now I want to run on a datacenter.
Give me yet another version of Autobot!

#4: Think about extensibility

  • Option 2: Create a new interface IAutobotRunner and change Autobot constructor to receive a runner parameter.  

 

 

<Clients>

OK. As a client, now I have to know about all these details!
I'll find a better Autobot

export interface IRunner {

    testDeliverable(testRequest: IAutobotTestRequest): Promise<IAutobotTestBody>;

    cloneRepo(githubRepo: string, commitSHA: string): Promise<boolean>;

    buildRepo(studentRepo: any): Promise<boolean>;

    testSuite(studentRepo: any, testSuite: ITestSuite): Promise<IAutobotTestBody>;
}

SA#4

< Extensible. Yet challenging.
Now I've a compromise not only with IAutobot but also IRunner>

export default class Autobot implements IAutobot {

    private runner: IRunner;

    constructor(runner: IRunner = new DesktopRunner()) {
        this.courses = {};
        this.runner = runner;
    }
    ...
}

API changes

API changes

export default class Autobot implements IAutobot {

    private runner: IRunner;

    constructor(runner: IRunner = new DesktopRunner()) {
        this.courses = {};
        this.runner = runner;
    }
    ...
}

we are enforcing backwards compatibility with the default value

< Always strive for backwards compatibility>

Designing APIs is HARD

#5: If time allows us

  1. Improve error handling
     
  2. Document your API
     
  3. Find SOLID principles in sa4

 

~min

Made with Slides.com