Skip to content

Commit 7a8bb61

Browse files
committed
Finish Refactorable section
1 parent 5f871b1 commit 7a8bb61

File tree

2 files changed

+225
-3
lines changed

2 files changed

+225
-3
lines changed

README.md

Lines changed: 224 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -837,14 +837,236 @@ We will centralize our state management using Redux. If you haven't used Redux b
837837

838838
Let's see what this more refactorable code looks like:
839839

840+
```javascript
841+
// src/3-refactorable/good/containers/inventory.js
842+
import React from 'react';
843+
import { connect } from 'react-redux';
844+
import { addToCartAction } from '../actions';
845+
846+
import CurrencyConverter from '../lib/currency-converter';
847+
import Inventory from '../components/inventory';
848+
849+
const InventoryContainer = (
850+
{
851+
inventory,
852+
currencies,
853+
localCurrency,
854+
addToCart,
855+
},
856+
) => (
857+
<Inventory
858+
currencyConverter={new CurrencyConverter(currencies)}
859+
inventory={inventory}
860+
localCurrency={localCurrency}
861+
addToCart={productId => addToCart(productId)}
862+
/>
863+
);
864+
865+
InventoryContainer.propTypes = {
866+
currencies: React.PropTypes.object.isRequired,
867+
inventory: React.PropTypes.object.isRequired,
868+
localCurrency: React.PropTypes.string.isRequired,
869+
addToCart: React.PropTypes.func.isRequired,
870+
};
871+
872+
const mapStateToProps = state => ({
873+
inventory: state.inventory,
874+
currencies: state.currencies,
875+
localCurrency: state.localCurrency,
876+
});
877+
878+
export default connect(mapStateToProps, {
879+
addToCart: addToCartAction,
880+
})(InventoryContainer);
881+
882+
```
883+
884+
```javascript
885+
// src/3-refactorable/good/containers/cart.js
886+
import React from 'react';
887+
import { connect } from 'react-redux';
888+
889+
import CurrencyConverter from '../lib/currency-converter';
890+
import Cart from '../components/cart';
891+
892+
const CartContainer = ({ cart, inventory, currencies, localCurrency }) => (
893+
<Cart
894+
cart={cart}
895+
currencyConverter={new CurrencyConverter(currencies)}
896+
localCurrency={localCurrency}
897+
inventory={inventory}
898+
/>
899+
);
900+
901+
CartContainer.propTypes = {
902+
currencies: React.PropTypes.object.isRequired,
903+
cart: React.PropTypes.array.isRequired,
904+
inventory: React.PropTypes.object.isRequired,
905+
localCurrency: React.PropTypes.string.isRequired,
906+
};
907+
908+
const mapStateToProps = state => ({
909+
cart: state.cart,
910+
inventory: state.inventory,
911+
currencies: state.currencies,
912+
localCurrency: state.localCurrency,
913+
});
914+
915+
export default connect(mapStateToProps, {})(CartContainer);
916+
```
917+
918+
```javascript
919+
// src/3-refactorable/good/actions/index.js
920+
import * as types from '../constants/action-types';
921+
922+
export const addToCartAction = productId => ({
923+
type: types.ADD_TO_CART,
924+
productId,
925+
});
926+
```
927+
928+
```javascript
929+
// src/3-refactorable/good/reducers/cart.js
930+
import { ADD_TO_CART } from '../constants/action-types';
931+
932+
const initialState = [];
933+
934+
export default (state = initialState, action) => {
935+
switch (action.type) {
936+
case ADD_TO_CART:
937+
return [...state, parseInt(action.productId, 10)];
938+
default:
939+
return state;
940+
}
941+
};
942+
```
943+
944+
This improved code centralizes our side effects to an `action` function which takes a `productId` in and passes it to our `reducer` which creates an entirely brand-new cart with this product added to it. This new cart is placed in our `store`, and once that happens all of our components which derive their state from particular pieces of the `store` will be notified by `react-redux` of the new data, and they will update their state and React will intelligently re-render each updated component.
945+
946+
This flow makes it possible to be sure that the state of your application can only be updated in one way, and that's through the _action_ -> _reducer_ -> _store_ -> _component_ pipeline. There's no global state to modify, no messages to pass and keep track of, and no uncontrolled side effects that our modules can produce.
947+
948+
One caveat to note: now, you might not need Redux in this project's example application, but if we were to expand this code it would become easier to use Redux as the state management solution instead of putting everything in the top-level controller `index.js`. We could have isolated the state of our app there and passed the appropriate data-modifying action functions down through each module. The issue with that is that at scale, we would have a lot of actions to pass down and a lot of data that would live in one massive `index.js` controller. By committing to a proper centralization of state early, we won't need to change much as our application develops.
949+
950+
The last thing we need to look at is tests. Tests give us confidence that we can change a module and it will still do what it was intended to do. We will look at the tests for the Cart and Inventory components:
951+
952+
```javascript
953+
// src/test/cart.test.js
954+
import React from 'react';
955+
import { shallow } from 'enzyme';
956+
import Cart from '../src/3-refactorable/good/components/cart';
957+
958+
const props = {
959+
localCurrency: 'usd',
960+
cart: [1, 1],
961+
inventory: {
962+
1: {
963+
product: 'Flashlight',
964+
img: '/flashlight.jpg',
965+
desc: 'A really great flashlight',
966+
price: 100,
967+
currency: 'usd',
968+
},
969+
},
970+
currencyConverter: {
971+
convert: jest.fn(),
972+
},
973+
};
974+
975+
it('should render Cart without crashing', () => {
976+
const cartComponent = shallow(<Cart {...props} />);
977+
expect(cartComponent);
978+
});
979+
980+
it('should show all cart data in cart table', () => {
981+
props.currencyConverter.convert = function () {
982+
return `$${props.inventory[1].price}`;
983+
};
984+
985+
const cartComponent = shallow(<Cart {...props} />);
986+
let tr = cartComponent.find('tr');
987+
expect(tr.length).toEqual(3);
988+
989+
props.cart.forEach((item, idx) => {
990+
let td = cartComponent.find('td');
991+
let product = td.at(2 * idx);
992+
let price = td.at(2 * idx + 1);
993+
994+
expect(product.text()).toEqual(props.inventory[item].product);
995+
expect(price.text()).toEqual(props.currencyConverter.convert());
996+
});
997+
});
998+
```
999+
1000+
```javascript
1001+
// src/test/inventory.test.js
1002+
import React from 'react';
1003+
import { shallow } from 'enzyme';
1004+
import Inventory from '../src/3-refactorable/good/components/inventory';
1005+
1006+
const props = {
1007+
localCurrency: 'usd',
1008+
inventory: {
1009+
1: {
1010+
product: 'Flashlight',
1011+
img: '/flashlight.jpg',
1012+
desc: 'A really great flashlight',
1013+
price: 100,
1014+
currency: 'usd',
1015+
},
1016+
},
1017+
addToCart: jest.fn(),
1018+
changeCurrency: jest.fn(),
1019+
currencyConverter: {
1020+
convert: jest.fn(),
1021+
},
1022+
};
1023+
1024+
it('should render Inventory without crashing', () => {
1025+
const inventoryComponent = shallow(<Inventory {...props} />);
1026+
expect(inventoryComponent);
1027+
});
1028+
1029+
it('should show all inventory data in table', () => {
1030+
props.currencyConverter.convert = function () {
1031+
return `$${props.inventory[1].price}`;
1032+
};
1033+
1034+
const inventoryComponent = shallow(<Inventory {...props} />);
1035+
let tr = inventoryComponent.find('tr');
1036+
expect(tr.length).toEqual(2);
1037+
1038+
let td = inventoryComponent.find('td');
1039+
let product = td.at(0);
1040+
let image = td.at(1);
1041+
let desc = td.at(2);
1042+
let price = td.at(3);
1043+
1044+
expect(product.text()).toEqual('Flashlight');
1045+
expect(image.html()).toEqual('<td><img src="/flashlight.jpg" alt=""/></td>');
1046+
expect(desc.text()).toEqual('A really great flashlight');
1047+
expect(price.text()).toEqual('$100');
1048+
});
1049+
1050+
it('should have Add to Cart button work', () => {
1051+
const inventoryComponent = shallow(<Inventory {...props} />);
1052+
let addToCartBtn = inventoryComponent.find('button').first();
1053+
addToCartBtn.simulate('click');
1054+
expect(props.addToCart).toBeCalled();
1055+
});
1056+
```
8401057

1058+
These tests ensure that the Cart and Inventory components:
1059+
* Show the data they are supposed to
1060+
* Maintain a consistent API
1061+
* Can modify state by calling a given action function
8411062

8421063

843-
You might not need Redux in this specific case, but as we expand this application it will become easier to use Redux as the state management solution instead of linking everything to the parent controller `index.js`
1064+
## Final Thoughts
1065+
Software architecture is the stuff that's hard to change, so invest early in a readable, reusable, and refactorable foundation. It will be hard to get there later. By following the 3 Rs, your users will thank you, your developers will thank you, and you will thank yourself.
8441066

8451067
--------------------------------------------------------------------------------
8461068
## Development
847-
If you wish to expand on this project or contribute, run the following commands to install everything you need:
1069+
Thank you for reading this! If you wish to expand on this project or contribute, run the following commands to install everything you need:
8481070

8491071
```
8501072
npm install -g create-react-app

public/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<link href='http://fonts.googleapis.com/css?family=Raleway:400,300,600' rel='stylesheet' type='text/css'>
77
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" />
88
<a href="https://github.com/ryanmcdermott/3rs-of-software-architecture" class="github-corner" aria-label="View source on Github"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#FD6C6C; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
9-
<title>Software Architecture Pyramid</title>
9+
<title>3 Rs of Software Architecture</title>
1010
</head>
1111
<body>
1212
<div id="root"></div>

0 commit comments

Comments
 (0)