Feature-Driven Architecture

CSS

in

JS

Hi, I am Oleg,

I write

v10 πŸŽ‰

πŸ˜‚
@ken_wheeler

Physical vs. Virtual

Software Engineering

has 2 constraints

  • Hardware

  • Humans

Goals

  • Discoverability
  • Work parallelisation

  • Controlling shared abstractions

  • Refactoring

  • AB Tests

  • Integration Tests

Feature-Driven Architecture

A set of principles that helps you

to define the boundaries.

Who is it for?

Code will be

maintained over a longer

period of time and

Β size will increase?

for you

not for you

yes

no

Principles

  1. Decentralization
  2. Explicit sharing
  3. Co-location

  4. Decoupling

  5. Disposability

Goals

1. Decentralization

How to avoid a death by Monolith

Is Monolith when

the entire codebase is in

one repo?

No

1. Decentralization

src/
β”œβ”€β”€ components/
|   └── Login.js
β”œβ”€β”€ containers/
|   └── Login.js
β”œβ”€β”€ actionCreators/
|   └── login.js
└── reducers/
    └── login.js
  1. Single feature is spread all over
  2. Everything is interconnected

1. Decentralization

src/
└── features/
    β”œβ”€β”€ featureA/
    β”œβ”€β”€ featureB/
    └── featureC/

Organise by featuresΒ instead of types.

1. Decentralization

Feature is a self-contained user facing reusable

complex building block.

1. Decentralization

Application
Pages / Screens

Features

Components

Utilities

1. Decentralization

Navigating code through UI

1. Decentralization

Understanding

the scope of impact

1. Decentralization

Common UI language

1. Decentralization

src/
└── features/
    β”œβ”€β”€ featureA/
    |   β”œβ”€β”€ privateModuleA.js
    |   β”œβ”€β”€ privateModuleB.js   
    |   β”œβ”€β”€ privateModuleC.js   
    |   └── index.js
    |
    β”œβ”€β”€ featureB/
    └── featureC/

Public interface & private exports

1. Decentralization

src/
β”œβ”€β”€ shared/
|   β”œβ”€β”€ components/
|   └── api/
└── features/
    β”œβ”€β”€ featureA/
    β”œβ”€β”€ featureB/
    └── featureC/

Enforces separation of shared code

The whole truth about

shared abstractions

2. Explicit Sharing

Changing shared code

with 100% unit test coverage

can still break something.

2. Explicit Sharing

const f = (value) => {
  if (typeof value === 'object') {
    return {...value, say: 'Hello'}
  }

  return value
} 



// How we think it should be used.
f({say: 'hi'}) // {say: 'Hello'}

// Reality
f(null) // {say: 'Hi'}

You can't know how your lib is used.

const f = (value) => {
  if (
    typeof value === 'object' && 
    value.say === 'hi'
  ) {
    return {...value, say: 'Hello'}
  }
  return value
} 

// How we think it should be used.
f({say: 'hi'}) // {say: 'Hello'}

// Reality
f(null) // Uncaught TypeError

2. Explicit Sharing

2. Explicit Sharing

const f = (fn) => {
  ... some code

  fn()

  ... some code

  return 1
}

You can't know how your lib is used.

2. Explicit Sharing

  1. Needed in at least 2 places.
  2. Non-trivial logic.
  3. Low frequency of change.

To share or not to share?

2. Explicit Sharing

  • Use pure functional utilities.

  • Writte moreΒ tests.

  • Carefully design the interface.

  • Do more detailed code reviews.

2. Explicit Sharing

3. Co-location

Navigating the Giant

3. Co-location

class JustAnotherCounter extends Component {
  state = {
    count: 0
  };

  setCount = () => {
    this.setState({ 
      count: this.state.count + 1 
    });
  };

  render() {
    return (
      <div>
        <h1>{this.state.count}</h1>

        <button onClick={this.setCount}>
          Count Up To The Moon
        </button>
      </div>
    );
  }
}
import React, { useState } from 'react';

function JustAnotherCounter() {
  const [count, setCount] = useState(0);
  const onClick = () => setCount(count + 1);

  return (
    <div>
      <h1>{count}</h1>
      <button 
        onClick={onClick}
      >
        Count Up To The Moon
      </button>
    </div>
  );
}

Code structure

3. Co-location

src/
β”œβ”€β”€ shared/
|   β”œβ”€β”€ components/
|   └── api/
└── features/
    └── {feature}/
        β”œβ”€β”€ containers/
        β”œβ”€β”€ actionCreators/
        β”œβ”€β”€ actionTypes/
        β”œβ”€β”€ renderers/
        β”œβ”€β”€ selectors/
        └── reducer

Directory structure

3. Co-location

  • CSS
  • Images
  • Tests
  • Anything else

4. Decoupling & isolation

useCommonSense(🚘)

4. Decoupling & isolation

  1. A feature should not depend on other features.
  2. A page should not depend on other pages.
  3. A shared abstraction should not depend on either.

Never Break Those Rules.

import {Header} from 'features/header';
import {LoginForm} from 'features/login';

const PageA = () => (
  <Fragment>
    <Header />
    <LoginForm />
  </Fragment>
);

4. Decoupling & isolation

import {Header} from 'features/header';
import {LoginForm} from 'features/login';

const PageA = () => {
  const [loginStatus, setLoginStatus] = useState('loggedOut');
  const onLogin = () => setLoginStatus('loggedIn');
  const onLogout = () => setLoginStatus('loggedOut');

  return (
    <Fragment>
      <Header loginStatus={loginStatus} onLogin={onLogin} ... />
      <LoginForm onLogin={onLogin} />
    </Fragment>
  );
};

4. Decoupling & isolation

Lets add login button to the header.

const Header = ({loginStatus, onLogin, onLogout}) => {
  return (
    <Fragment>
      <A />
      <B />
      {loginStatus === 'loggedOut' && 
          <button onClick={onLogin}>Login<button>}
      {loginStatus === 'loggedIn' && 
        <button onClick={onLogout}>Logout<button>}
    </Fragment>
  );
};

What are the problems here?

4. Decoupling & isolation

Coupled

  1. Header knows about loginStatus and its values
  2. Header knows about how to login and logout actions
  3. Header knows how to render and style the button

4. Decoupling & isolation

4. Decoupling & isolation

  • Render prop
  • Element as prop
  • Component as prop
const Header = ({loginLogoutButton}) => {
  return (
    <Fragment>
      <A />
      <B />
      {loginLogoutButton}
    </Fragment>
  );
};

4. Decoupling & isolation

import {Header} from 'features/header';
import {LoginForm, LoginLogoutButton} from 'features/login';

const PageA = () => (
  <Fragment>
    <Header loginLogoutButton={<LoginLogoutButton />} />
    <LoginForm />
  </Fragment>
);

4. Decoupling & isolation

How does LoginLogoutButton know what to do?

const LoginContext = React.createContext('login');

const LoginProvider = ({children}) => {
  const [status, setLoginStatus] = useState('loggedOut');
  const onLogin = data => api.login(data).then(setLoginStatus);
  const onLogout = data => api.logout(data).then(setLoginStatus);

  return (
    <LoginContext.Provider value={{status, onLogin, onLogout}}>
     {children}
    </LoginContext.Provider>
  );
};

4. Decoupling & isolation

const LoginLogoutButton = () => (
  <LoginContext.Consumer>
    {({status, onLogin, onLogout}) =>
      status === 'loggedIn' ? (
        <button onClick={onLogout}>logout</button>
      ) : (
        <button onClick={onLogin}>login</button>
      )
    }
  </LoginContext.Consumer>
);

4. Decoupling & isolation

import {Header} from 'features/header';
import {
  LoginForm, 
  LoginLogoutButton,
  LoginProvider
} from 'features/login';

const PageA = () => (
  <LoginProvider>
    <Header loginLogoutButton={<LoginLogoutButton />} />
    <LoginForm />
  </LoginProvider>
);

4. Decoupling & isolation

We can do the same with Redux!

Scoped Action Types

Scoped Action Creators

Scoped State

5. Disposability

πŸ—‘

Β 

5. Disposability

Do Not optimize for Modification!

Unless you can

predict the future.

Optimize for ease of removal!

Based on what you

already know.

5. Disposability

At some point everything is going to be a Mess.

5. Disposability

Refactor everything

vs.

Refactor isolated parts.

5. Disposability

Principles

  1. Decentralization
  2. Explicit sharing
  3. Co-location

  4. Isolation

  5. Disposability

Work parallelisation

Controlling shared abstractions

Discoverability

Integration Tests

AB Tests

Refactoring

πŸŽ‰

Feature-Driven Architecture

By Oleg Isonen

Feature-Driven Architecture

  • 3,046