È questo codice spaghetti dell'applicazione web nodejs?
Recentemente ho sostenuto un colloquio per un ruolo di sviluppatore back-end presso una startup coinvolta in prodotti finanziari, come laureato nel 2020. Il compito da portare a casa che mi hanno fatto inviare aveva alcuni obiettivi di base e hanno consigliato lo stack Nodejs / MongoDB. Ho ricevuto il seguente feedback sulla mia richiesta:
1. La visibilità dell'architettura REST è inferiore.
2. La strutturazione del codice avrebbe potuto essere migliore (sono d'accordo con questo).
3. Se la lingua scelta è Nodejs, almeno l'uso della sintassi di base dovrebbe essere corretto. È stato osservato il contrario.
Le mie domande su questo feedback sono:
1. La struttura di un'API REST non è altamente soggettiva? Come posso rendere la mia applicazione più conforme agli obiettivi REST?
2. Che cos'è l '"utilizzo della sintassi non corretto?" Se la mia sintassi non fosse corretta, il progetto si comporterebbe male o non funzionerebbe, no? Ho pubblicato questa domanda sul subreddit r / codereview e ho ricevuto pochi feedback utili a parte un commento che diceva "spaghetti". Mi piacerebbe se potessi darmi indicazioni su come migliorare il mio codice.
Mi piacerebbe imparare da questo esercizio e sono aperto a tutti i feedback / critiche. Il mio codice risiede su github.com/utkarshpant/portfolio-api insieme alla documentazione. Ho testato l'applicazione utilizzando Postman e, come esercizio, sto scrivendo test di unità / integrazione per esso.
Sto riproducendo di seguito una porzione del routes/portfolio_v2.jsfile, dove ho implementato la maggior parte degli endpoint:
const dbDebugger = require('debug')('app:db');
const apiDebugger = require('debug')('app:api');
const express = require('express');
const router = express.Router();
const Joi = require('joi');
const customError = require('http-errors');
const errorHandlerMiddleware = require('../middleware/errorHandlerMiddleware');
// importing models;
const Portfolio = require('../models/portfolioModel');
// import validations;
const validations = require('../validations/validateRequest');
// get returns on portfolio
router.get('/getReturns/:portfolioName', errorHandlerMiddleware((req, res) => {
const portfolioName = req.params.portfolioName;
(async () => {
const portfolio = await Portfolio.findOne({ "name": portfolioName }).catch(err => res.send(error));
if (!portfolio) {
return res.status(404).send("No portfolio found");
}
const currentPrice = 100;
let returns = 0;
portfolio.securities.forEach(security => {
apiDebugger(`The returns on ${security.ticker} are ${((currentPrice - security.avgBuyPrice) * security.shares)}`);
returns += ((currentPrice - security.avgBuyPrice) * security.shares)
})
console.log("Returns:\t", returns);
res.send({
portfolio: portfolio.name,
cumulativeReturns: returns
});
})();
}));
// place a buy trade
router.post('/buy/:portfolioName', errorHandlerMiddleware((req, res) => {
/*
Request body includes:
Trade object, including ticker, type, quantity, price
TODO:
- validations for
- ticker match
- trade type == sell,
- shares - quantity > 0 always
- resultant shares > 0 always
*/
// validating request body;
const { error } = validations.validateTradeRequest(req);
if (error) {
throw customError(400, "Bad Request; re-check the request body.");
} else {
// mismatch of type;
if (req.body.type != "buy") {
throw customError(400, "Bad request; type must be 'buy'.")
}
const portfolioName = req.params.portfolioName;
const trade = req.body;
(async () => {
// retrieve portfolio and find relevant security;
const portfolio = await Portfolio.findOne({ "name": portfolioName }).catch(err => res.send(err));
if (!portfolio) {
return res.status(404).send("No portfolio found");
}
const security = portfolio.securities.find(security => security.ticker == trade.ticker);
// if the ticker does not exist, return a 404;
if (!security) {
return res.status(404).send("Invalid ticker.");
}
// register sell trade and update shares;
security.trades.push(trade);
let oldShares = security.shares;
security.shares += trade.quantity;
security.avgBuyPrice = (((security.avgBuyPrice) * (oldShares)) + ((trade.price) * (trade.quantity))) / (security.shares);
apiDebugger(`(security.avgBuyPrice): ${security.avgBuyPrice},\nsecurity.shares: ${security.shares},\ntrade.price: ${trade.price},\ntrade.quantity: ${trade.quantity}\n`);
// save portfolio
try {
await portfolio.save().then(res.status(200).send(trade));
} catch (err) {
let errorMessages = [];
ex.errors.forEach(property => errorMessages.push(property));
res.status(500).send("An error occured in saving the transaction.")
}
})();
}
}));
// place a sell trade
router.post('/sell/:portfolioName', errorHandlerMiddleware((req, res) => {
/*
Request body includes:
Trade object, including ticker, type, quantity
TODO:
- validations for
- ticker match
- trade type == sell,
- shares - quantity > 0 always
- resultant shares > 0 always
*/
// validating request body;
const { error } = validations.validateTradeRequest(req);
if (error) {
throw customError(400, "Bad Request; re-check the request body.");
} else {
if (req.body.type != "sell") {
throw customError(400, "Bad Request; type must be 'sell'.");
}
const portfolioName = req.params.portfolioName;
const trade = req.body;
(async () => {
// retrieve portfolio and find relevant security;
const portfolio = await Portfolio.findOne({ "name": portfolioName }).catch(err => res.send(err));
if (!portfolio) {
return res.status(404).send("No portfolio found");
}
const security = await portfolio.securities.find(security => security.ticker == trade.ticker);
// check that resultant share count > 0;
if ((security.shares - trade.quantity) < 0) {
// throw customError(422, `The given Trade will result in ${security.shares - trade.quantity} shares and cannot be processed.`); return res.status(422).send(`Request cannot be serviced. Results in (${security.shares - trade.quantity}) shares.`);
}
// register sell trade and update shares;
security.trades.push({ "ticker": trade.ticker, "type": "sell", quantity: trade.quantity });
security.shares -= trade.quantity;
// save portfolio
try {
await portfolio.save().then(res.status(200).send(trade)).catch();
} catch (err) {
let errorMessages = [];
ex.errors.forEach(property => errorMessages.push(property));
res.status(500).send("An error occured in saving the transaction.")
}
})();
}
}));
function validateRequest(request) {
const tradeRequestSchema = Joi.object({
ticker: Joi.string().required().trim(),
type: Joi.string().required().valid("buy", "sell").trim(),
quantity: Joi.number().required().min(1).positive(),
price: Joi.number().min(1).positive()
})
return tradeRequestSchema.validate(request.body);
}
module.exports = router;
Grazie!
Risposte
Da una recensione media;
La gestione delle eccezioni solleva sicuramente le sopracciglia
exnon è definito, quindiex.errors.forEach(property => errorMessages.push(property));falliràerrorMessagesè un mistero, sembra che tu non ci faccia niente?Questo
let errorMessages = []; ex.errors.forEach(property => errorMessages.push(property));dovrebbe essere
const errorMessages = err.errors;Allo stesso modo,
.catch(err => res.send(error));non funzionerà
Usa uno strumento di suggerimento come https://jshint.com/
async () =>crea una funzione anymous, è necessario utilizzare funzioni denominate per motivi di traccia dello stackAvvolgi
console.login una sorta di funzione di filtraggio del livello di verbosità, non scrivere maiconsole.logdirettamenteIl calcolo di
returnssarebbe perfetto per mettersi in mostrareduce()Il grande commento di
router.post('/buy/:portfolioName'dovrebbe essere sopra la funzionePer me
:portfolioName/buyè molto più REST come'/buy/:portfolioName', guardo i grandi giocatori e tendono ad andare molto più per nome / verbo che per verbo / nomeSe dovessi registrare gli errori per un'app di trading, lo farei
- Crea per ogni errore un GUID e scarica quel GUID nei log del server
- Als fornire quel GUID nel messaggio 500,
"An error occured in saving the transaction."è così privo di informazioni, potrebbe farti licenziare
Quando vedo questo;
if (error) { throw customError(400, "Bad Request; re-check the request body."); } else {- Mi rabbrividisco, perché
elsedopo unthrow? - perchè non lo fai
elsedopo il prossimothrow?
- Mi rabbrividisco, perché
Era il loro progetto da controllare
typeanche se il tipo fa parte dell'URL? Questo è solo un cattivo designAvresti dovuto avvolgere
validateTradeRequestin una funzione that- Chiamate
validateTradeRequest - Controlla il tipo
- Controlla il tipo di portafoglio
- Genera un errore se qualcos'altro va storto
- Riutilizzata questa funzione in entrambi
sellebuy
- Chiamate
return res.status(404).send("Invalid ticker.");è una scelta sfortunata di codice di errore http e messaggio. REST saggio, 404 significa che il portfolio non esiste, ma esiste. Per quanto riguarda il messaggio, immagina se il messaggio fosse dettoNo security found for ticker ${trade.ticket} in portfolio ${portfolioName}Una volta che usi
portfolio.securities.finde l'altra voltaawait portfolio.securities.find?? Ho smesso di rivedere in profondità qui ..Per quanto riguarda la visibilità, sono d'accordo con il revisore, immagina che il tuo codice sia iniziato con
router.get( '/:portfolio/getReturns', getPortfolioReturns); router.post('/:portfolio/buy', postPortfolioBuy); router.post('/:portfolio/sell', postPortfolioSell);il lettore saprebbe in 1 frazione di secondo cosa fa questo codice (forse sarebbe infastidito da quello spazio per allinearlo
getcon ilpost;)) Ora il lettore deve scorrere le righe e le righe di codice per capirlo.Per la parte di sintassi, il tuo uso di
asyncinnesca il mio senso spidey, ma ogni volta che penso di aver trovato qualcosa mi sbaglio. O il revisore è davvero intelligente o vuole apparire davvero intelligente
Tutto sommato, e lo dico raramente, questo avrebbe dovuto essere sottoposto a test. Più specificamente, questo avrebbe dovuto avere test per gli errori e avresti trovato e risolto molti di questi problemi.