Trabe

Trabe logo

Asís & David

https://trabe-teaching.github.io/strike-your-balance

Context

Motivation

Strike your balance

...or the search for the right abstraction

About the code examples

Javascript, mostly naive, can be unsafe,
no imports/exports

Abstraction in computer science is the process of removing elements of a code or program that aren't relevant or that distract from more important elements. Abstraction refers to looking at something to maintain the general form or meaning while reducing the presence of specific details. Google 🤷🏻‍♀️

An abstraction is defined
by its APIs

How do I use it. What do I get

Example

Read a file into a string, three languages, three levels of abstraction


// C
int file = open("example.txt", O_RDONLY);

struct stat file_info;
fstat(file, &file_info);

char *buffer = (char *)malloc(file_info.st_size + 1);

read(file, buffer, file_info.st_size);
buffer[file_info.st_size] = '\0';

close(file);
      

// js - node
const buffer = await fs.readFile("some.txt");
const text = buffer.toString();
      

# ruby
text = File.open("some.txt").read
      

You can't have everything!

Every abstraction has tradeoffs

This talk's mantra

Real world example

Dependency injection

Option 1: Explicit dependencies


function updateUserPreference({ key, value, repos }) {
  // Do stuff with the user

  // Save the updated user
  repos.userRepo.update(updatedUser);
}
      

const repos = {
  userRepo: userRepo(),
};

updateUserPreference({ key: "someKey", value: "someValue", repos });

// updateUserPreference({ key: "someKey", value: "somevalue", repos: { userRepo: userRepoDb() })
// updateUserPreference({ key: "someKey", value: "somevalue", repos: { userRepo: userRepoKS() })
// updateUserPreference({ key: "someKey", value: "somevalue", repos: { userRepo: logProxy(userRepo()) })

      

it("updates the user preference", () => {
  const mockUpdate = jest.fn();
  const mockRepo = { update: mockUpdate };
  const repos = { userRepo: mockRepo };

  updateUserPreference({ key: "someKey", value: "someValue", repos });

  expect(mockUpdate).toHaveBeenCalledWith(/* the updatedUser */);
});
      

Option 2: Implicit dependencies


function updateUserPreference({ key, value }) {
  // Do stuff with the user

  // Save the updated user
  const repo = userRepo();
  repo.update(updatedUser);
}
      

updateUserPreference({ key: "someKey", value: "someValue" });
      

const mockUpdate = jest.fn();

jest.mock("user-repo.js", () => () => ({
  update: mockUpdate;
}));

it("updates the user preference", () => {
  updateUserPreference({ key: "someKey", value: "someValue" });
  expect(mockUpdate).toHaveBeenCalledWith(/* the updatedUser */);
});
      

Option 3: Both


function defaultRepos() {
  return {
    userRepo: userRepo(),
  };
};

function updateUserPreference({ key, value, repos = defaultRepos() }) {
  // Do stuff with the user

  // Save the updated user
  repos.userRepo.update(updatedUser);
}
      

updateUserPreference({ key: "someKey", value: "someValue" });
      

it("updates the user preference", () => {
  const mockUpdate = jest.fn();
  const mockRepo = { update: mockUpdate };
  const repos = { userRepo: mockRepo };

  updateUserPreference({ key: "someKey", value: "someValue", repos });

  expect(mockUpdate).toHaveBeenCalledWith(/* the updatedUser */);
});
      

You can't have everything!

Every abstraction has tradeoffs

Abstraction budget

Real world example

A very common React abstraction


const Context = React.createContext(0);

function Component() {
  const value = React.useContext(Context);
  return <span>value is {value}</span>;
}

<Context.Provider value={5}>
  <Component />
</Context.Provider>

// Renders "value is 5"
      

const StorageContext = React.createContext();

function StorageProvider({ storage, children }) {
  return (
    <StorageContext.Provider value={storage}>
      {children}
    </StorageContext.Provider>
  );
}
      

function useStorage() {
  const storage = React.useContext(StorageContext);

  if (!storage) {
    throw new Error("Cannot use useStorage outside of an StorageProvider");
  }

  return storage;
}
      

function useStoredState(key, initialValue) {
  const storage = useStorage();

  const [value, setValue] = React.useState(() => {
    return storage.get(key) ?? initialValue;
  });

  const setStoredValue = (newValue) => {
    setValue(newValue);
    storage.set(key, newValue);
  };

  return [ value, setStoredValue ];
}
      

function Counter() {
  const [count, setCount] = useStoredState("count", 0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}


<StorageProvider storage={window.localStorage}>
  <Counter />
</StorageProvider>

      

test("It works", () => {
  render(
    <StorageProvider storage={new Map()}>
      <Counter />
    </StorageProvider>
  );
});
      

test("Does not work", () => {
  render(<Counter/>);
});


// Error: "Cannot use useStorage outside of an StorageProvider"
      

And now, what?


function useStorage() {
  const storage = React.useContext(StorageContext);

  if (!storage) {
    console.error("Cannot use useStorage outside of an StorageProvider");
  }

  return storage;
}
      

const testStorage = new Map();

function useStorage() {
  const storage = React.useContext(StorageContext);

  if (!storage && process.env.NODE_ENV === "test") {
    return testStorage;
  }

  if (!storage) {
    throw new Error("Cannot use useStorage outside of an StorageProvider");
  }

  return storage;
}
      

function useStorage({ skipCheck }) {
  const storage = React.useContext(StorageContext);

  if (!storage && !skipCheck) {
    throw new Error("Cannot use useStorage outside of an StorageProvider");
  }

  return storage;
}
      

function useStoredState(key, initialValue) {
  const storage = useStorage({ skipCheck: true });

  // ...
      

const StorageContext = React.createContext(new Map());

function useStorage() {
  return React.useContext(StorageContext);
}
      

You can't have everything!

Every abstraction has tradeoffs

Real world example

To couple or not to couple


const log = logger("errorMiddleware");

function errorMiddleware(ctx, next) {
  try {
    await next();
  } catch(e) {
    log.error("Something went wrong", e.message);
    log.debug(e.stack);
    ctx.status = 500;
  }
}
      

loggers:
  base:
    level: INFO
    transports:
      - console

  errorMiddleware:
    level: DEBUG
      

And then, came the lambda function


async function azureFn(request, context) {
  const log = logger("azureFun"); // NOPE

  try {
    // Stuff
  } catch(e) {
    log.error("Something went wrong", e.message);
    log.debug(e.stack);
    return {
      status: 500,
    };
  }
}
      

async function azureFn(request, context) {
  const log = logger({
    level: process.env.LOG_LEVEL,
    transports: [ azureLogTransport(context) ],
  });

  // ...
      

loggers:
  base:
    level: INFO

  errorMiddleware:
    level: DEBUG
      

const log = logger({
  ...config.loggers.base,
  ...config.loggers.errorMiddleware,
  transports: [ consoleTransport() ],
});
      

loggers:
  base: &base
    level: INFO

  errorMiddleware:
    <<: *base
    level: DEBUG
      

const log = logger({
  ...config.loggers.errorMiddleware,
  transports: [ consoleTransport() ],
});
      

But you've lost the option to configure the transport in the YAML?

Have we really?


const transportFactories = {
  console: () => consoleTransport(),
};

function coupledLogger(key) {
  const loggerConfig = {
    ...config.loggers.base,
    ...config.loggers[key],
  };

  const transports = loggerConfig.transports.map((t) => transportFactories[t]());

  return logger({ ...loggerConfig, transports });
}
      

const log = coupledLogger("errorMiddleware");
      

You can't have everything!

Every abstraction has tradeoffs

Summing up

You can't have everything!

Duplication is far cheaper than the wrong abstraction Sandi Metz
The biggest risk of tool development is accidentally convincing yourself that incidental complexity your abstraction created is essential complexity of the problem space.
And then you think workarounds for the incidental complexity are features you’re proud of. Ryan Florence - Remix fame

Every abstraction has tradeoffs

This is the last time!

Strike your balance

Use the right abstraction

Questions?

Shameless plug

rrhh@trabe.io

@trabe