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
- Decentralization
- Explicit sharing
-
Co-location
-
Decoupling
-
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
- Single feature is spread all over
- 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
- Needed in at least 2 places.
- Non-trivial logic.
- 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
- A feature should not depend on other features.
- A page should not depend on other pages.
- 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
- Header knows about loginStatus and its values
- Header knows about how to login and logout actions
- 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
- Decentralization
- Explicit sharing
-
Co-location
-
Isolation
-
Disposability
Work parallelisation
Controlling shared abstractions
Discoverability
Integration Tests
AB Tests
Refactoring
π
Feature-Driven Architecture
By Oleg Isonen
Feature-Driven Architecture
- 3,087