Dial M for Maintainability

Part II

Let's get acquainted!

Let's get acquainted!

First Name: Azat
Last Name: Davliatshin
Experience: 9 years in JS development
Life Science & Healthcare
Expertise: FE, BE, Clouds, Mobile, Legacy
Criminal Records: Had to force push changes into production branch

Agenda

Agenda

  • Follow up
  • Refactoring goals
  • Design, Refactoring, Analysis...
  • Conclusion
  • P.S.

Follow Up

import { json2csv } from 'json-2-csv';
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";

const urlAPI = "...";
const tokenAPI = "...";
const options = {
	headers: '' // use tokenAPI
};

const fetchResponse = await fetch(urlAPI, options);
const data = fetchResponse.json();

const csv = await converter.json2csv(data);

const client = new S3Client({});
const command = new PutObjectCommand({});
await client.send(command);
							

Solution is not maintainable

Maintainability

QA deals with change and the cost in time or money of making a change, including the extent to which this modification affects other functions or quality attributes.

Maintainability Tactics

Software Architecture in Practice (SEI Series in Software Engineering)

Design Principles

  • GRASP
  • SOLID
  • IoC

Patterns, Techniques

  • DI
  • Low Coupling
  • High Cohesion

Refactoring goals

Refactoring goals

  • Modular structure
    • GRASP, SOLID
    • Easy to test (as less as dep-s as possible)
    • Easy to extend/replace/reuse
    • Easy to log, catch errors and debug
  • Defer Binding (no hardcoded values)

Refactoring goals

Design, Refactoring, Analysis...

Run #1

Design

Refactoring

	// index.ts
import { 
	type APIGatewayProxyEvent, 
	type APIGatewayProxyResult 
} from 'aws-lambda';
import { fetchData } from './fetch-data';
import { transformToCSV } from "./transform-to-csv";
import { putCSVtoS3 } from "./put-csv-to-s3";

export const handler = async (
	event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
	try {
		const data = await fetchData();
		const csv = await transformToCSV(data);
		const response = await putCSVtoS3(csv);
	
		return {
			statusCode: 200,
			body: JSON.stringify({
				message: 'Success!'
			}),
		};
	} catch(error) {
		console.error(JSON.stringify(error, null, 2));
	
		return {
			statusCode: 500,
			body: JSON.stringify(error),
		};
	}
};
								
	// put-csv-to-s3.ts
import { 
	PutObjectCommand, 
	S3Client, 
	type PutObjectCommandOutput,
} from "@aws-sdk/client-s3";

const client = new S3Client({});

export const putCSVtoS3 = async (
	csv: string
): Promise<PutObjectCommandOutput> => {
	const command = new PutObjectCommand({
		Bucket: "test-bucket",
		Key: `data_${(new Date()).getTime()}.csv`,
		Body: csv,
	});
	
	return client.send(command);
};
								

Analysis

Refactoring goals

Run #2

Design

Refactoring

	// index.ts
import { 
	type APIGatewayProxyEvent, 
	type APIGatewayProxyResult 
} from 'aws-lambda';
import { fetchData } from './fetch-data';
import { transformToCSV } from "./transform-to-csv";
import { putCSVtoS3 } from "./put-csv-to-s3";

export const handler = async (
	event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
	try {
		const data = await fetchData();
		const csv = await transformToCSV(data);
		const response = await putCSVtoS3(csv);
	
		return {
			statusCode: 200,
			body: JSON.stringify({
				message: 'Success!'
			}),
		};
	} catch(error) {
		console.error(JSON.stringify(error, null, 2));
	
		return {
			statusCode: 500,
			body: JSON.stringify({
				message: 'Failure!',
				...error
			}),
		};
	}
};
								
// put-csv-to-s3.ts
import { 
PutObjectCommand, 
S3Client, 
type PutObjectCommandOutput,
} from "@aws-sdk/client-s3";

const client = new S3Client({});

export const putCSVtoS3 = async (
	csv: string
): Promise<PutObjectCommandOutput> => {
	try {
		const command = new PutObjectCommand({
			Bucket: "test-bucket",
			Key: `data_${(new Date()).getTime()}.csv`,
			Body: csv,
		});
	
		return await client.send(command);
	} catch (error) {
		console.info("Store CSV file to S3 operation is failed");
		throw error;
	}
};
							

Analysis

Refactoring goals

// put-csv-to-s3.ts
import { 
	PutObjectCommand, 
	S3Client, 
	type PutObjectCommandOutput,
} from "@aws-sdk/client-s3";

// THERE MIGHT BE HARDCODED REGION
const client = new S3Client({});

export const putCSVtoS3 = async (
	csv: string
): Promise<PutObjectCommandOutput> => {
	try {
		const command = new PutObjectCommand({
			// HARDCODED!
			Bucket: "test-bucket",
			// HARDCODED!
			Key: `data_${(new Date()).getTime()}.csv`,
			Body: csv,
		});
	
		return await client.send(command);
	} catch (error) {
		console.info("Store CSV file to S3 operation is failed");
		throw error;
	}
};
							
	// put-csv-to-s3.ts
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";

// THERE MIGHT BE HARDCODED REGION
const client = new S3Client({});

export const putCSVtoS3 = async (csv: string) => {
	try {
		const command = new PutObjectCommand({
			Bucket: process.env.BUCKET_NAME,
			Key: `${process.env.BUCKET_KEY}_${(new Date()).getTime()}.csv`,
			Body: csv,
		});
		
		return client.send(command);
	} catch (error) {
		// TO CONSIDER: CARRY OUT ERROR MESSAGES (OPTIONAL)
		console.info("Store CSV file to S3 operation is failed");
		throw error;
	}
};
								

Run #3

Design

Refactoring

	// index.ts
import { 
	type APIGatewayProxyEvent, 
	type APIGatewayProxyResult 
} from 'aws-lambda';
import { fetchData } from './fetch-data';
import { transformToCSV } from "./transform-to-csv";
import { putCSVtoS3 } from "./put-csv-to-s3";
import { getConfig } from './get-config';

export const handler = async (
	event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
	try {
		const config = await getConfig();
		const data = await fetchData(config.apiURL, config.apiToken);
		const csv = await transformToCSV(data, config.separator);
		const response = await putCSVtoS3(
			csv, 
			config.bucketName, 
			config.bucketKey
		);
	
		return {
			statusCode: 200,
			body: JSON.stringify({
				message: 'Success!'
			}),
		};
	} catch(error) {
		console.error(JSON.stringify(error, null, 2));
	
		return {
			statusCode: 500,
			body: JSON.stringify({
				message: 'Failure!',
				...error
			}),
		};
	}
};
								
	// put-csv-to-s3.ts
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { type Config } from './get-config';

const client = new S3Client({});

export const putCSVtoS3 = async (
	csv: string, 
	bucketName: string, 
	bucketKey: string
) => {
	try {
		const command = new PutObjectCommand({
			Bucket: bucketName,
			Key: bucketKey,
			Body: csv,
		});
		
		return await client.send(command);
	} catch (error) {
		console.info("Store CSV file to S3 operation is failed");
		throw error;
	}
};
								

Analysis

Refactoring goals

// index.ts
import { 
	type APIGatewayProxyEvent, 
	type APIGatewayProxyResult 
} from 'aws-lambda';
import { fetchData } from './fetch-data';
import { transformToCSV } from "./transform-to-csv";
import { putCSVtoS3 } from "./put-csv-to-s3";
import { getConfig } from './get-config';

export const handler = async (
	event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
	try {
		const config = await getConfig();
		const data = await fetchData(config.apiURL, config.apiToken);
		const csv = await transformToCSV(data, config.separator);
		const response = await putCSVtoS3(
			csv, 
			config.bucketName, 
			config.bucketKey
		);

		return {
			statusCode: 200,
			body: JSON.stringify({
				message: 'Success!'
			}),
		};
	} catch(error) {
		console.error(JSON.stringify(error, null, 2));

		return {
			statusCode: 500,
			body: JSON.stringify({
				message: 'Failure!',
				...error
			}),
		};
	}
};
							

Run #4

Design

Refactoring

// index.ts
import { 
	type APIGatewayProxyEvent, 
	type APIGatewayProxyResult 
} from 'aws-lambda';
import * as API from "@api";
import { convert } from "@converter";
import { putToStorage } from "@storage";
import { getConfig } from '@config';

export const handler = async (
	event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
	try {
		const config = await getConfig();
		const data = await API.get(config);
		const transformedData = await convert(data, config);
		const response = await putToStorage(transformedData, config);

		return {
			statusCode: 200,
			body: JSON.stringify({
				message: 'Success!'
			}),
		};
	} catch(error) {
		console.error(JSON.stringify(error, null, 2));

		return {
			statusCode: 500,
			body: JSON.stringify({
				message: 'Failure!',
				...error
			}),
		};
	}
};
							
// index.ts
import { 
	type APIGatewayProxyEvent, 
	type APIGatewayProxyResult 
} from 'aws-lambda';
import * as API from "@api";
import { convert } from "@converter";
import { putToStorage } from "@storage";
import { getConfig } from '@config';

export const handler = async (
	event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
	try {
		const config = await getConfig();
		const data = await API.get(config);
		const transformedData = await convert(data, config);
		const response = await putToStorage(transformedData, config);

		return {
			statusCode: 200,
			body: JSON.stringify({
				message: 'Success!'
			}),
		};
	} catch(error) {
		console.error(JSON.stringify(error, null, 2));

		return {
			statusCode: 500,
			body: JSON.stringify({
				message: 'Failure!',
				...error
			}),
		};
	}
};
							
// index.ts
import { 
	type APIGatewayProxyEvent, 
	type APIGatewayProxyResult 
} from 'aws-lambda';
import * as API from "@api";
import { convert } from "@converter";
import { putToStorage } from "@storage";
import { getConfig } from '@config';

export const handler = async (
	event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
	try {
		const config = await getConfig();
		const data = await API.get(config);
		const transformedData = await convert(data, config);
		const response = await putToStorage(transformedData, config);

		return {
			statusCode: 200,
			body: JSON.stringify({
				message: 'Success!'
			}),
		};
	} catch(error) {
		console.error(JSON.stringify(error, null, 2));

		return {
			statusCode: 500,
			body: JSON.stringify({
				message: 'Failure!',
				...error
			}),
		};
	}
};
							
// @storage/index.ts
import { putObjectToS3 } from './s3-realization';
//import { somethingElse } from './something-else';
import { type Config } from '@config';

export const putToStorage = async (data: string, config: Config) => {
	try {
		return await putObjectToS3(
			data, 
			config.bucketName,
			config.bucketKey,
		);
		// in the case of dynamic changes, use if-else/switch
	} catch(error) {
		console.info("Error inside 'putToStorage' function");
		throw error;
	}
};
					

Analysis

Refactoring goals

Run #5

Design

Inversion of Control
Inversion of Control (IoC) is a design principle in which a software component is designed to receive its dependencies from an external source, rather than creating them itself.
DI
Dependency injection is a programming technique that makes a class independent of its dependencies. It achieves that by decoupling the usage of an object from its creation. This helps you to follow SOLID's dependency inversion and single responsibility principles.

Refactoring

// index.ts
import { 
	type APIGatewayProxyEvent, 
	type APIGatewayProxyResult 
} from 'aws-lambda';
import { createContainer } from './inversify.container';
import { TOKENS, type IConductorProcessor } from '@infrastructure';

export const handler = async (
	event: APIGatewayProxyEvent,
): Promise<APIGatewayProxyResult> => {
	try {
		// there might be params
		const container = await createContainer();
		const conductorProcessor = 
			container.get<IConductorProcessor>(TOKENS.CONDUCTOR_PROCESSOR);
		await conductorProcessor.processData();
		
		return {
			statusCode: 200,
			body: JSON.stringify({
				message: 'Success!'
			}),
		};
	} catch (error) {
		console.error(JSON.stringify(error, null, 2));

		return {
			statusCode: 500,
			body: JSON.stringify({
				message: 'Failure!',
				...error
			}),
		};
	}
};
							
// index.ts
import { 
	type APIGatewayProxyEvent, 
	type APIGatewayProxyResult 
} from 'aws-lambda';
import { createContainer } from './inversify.container';
import { TOKENS, type IConductorProcessor } from '@infrastructure';

export const handler = async (
	event: APIGatewayProxyEvent,
): Promise<APIGatewayProxyResult> => {
	try {
		// there might be params
		const container = await createContainer();
		const conductorProcessor = 
			container.get<IConductorProcessor>(TOKENS.CONDUCTOR_PROCESSOR);
		await conductorProcessor.processData();
		
		return {
			statusCode: 200,
			body: JSON.stringify({
				message: 'Success!'
			}),
		};
	} catch (error) {
		console.error(JSON.stringify(error, null, 2));

		return {
			statusCode: 500,
			body: JSON.stringify({
				message: 'Failure!',
				...error
			}),
		};
	}
};
							
// inversify.config.ts
import "reflect-metadata";
import { Container } from "inversify";
	
import {
  TOKENS,
  getConfig,
  type IConductorProcessor,
  type IConfigProvider,
  type IAPIProvider,
  type IConverterProvider,
  type IStorageProvider,
} from "@infrastructure";
import { ConductorProcessor } from "@processors";
import { 
	ConfigProvider,
	APIProvider, 
	ConverterProvider, 
	StorageProvider 
} from "@providers";

export const createContainer = (): Container => {
	const container = new Container({ defaultScope: "Singleton" });
	
	// processors
	container
		.bind<IConductorProcessor>(TOKENS.CONDUCTOR)
		.to(ConductorProcessor);
	
	const config = await getConfig();
	// providers
	container
		.bind<IConfigProvider>(TOKENS.CONFIG)
		.tValue(config);
	container
		.bind<IAPIProvider>(TOKENS.API)
		.to(APIProvider);
	container
		.bind<IConverterProvider>(TOKENS.CONVERTER)
		.to(ConverterProvider);
	container
		.bind<IStorageProvider>(TOKENS.STORAGE)
		.to(StorageProvider);

  return container;
};	
							
// inversify.config.ts
import "reflect-metadata";
import { Container } from "inversify";
	
import {
  TOKENS,
  getConfig,
  type IConductorProcessor,
  type IConfigProvider,
  type IAPIProvider,
  type IConverterProvider,
  type IStorageProvider,
} from "@infrastructure";
import { ConductorProcessor } from "@processors";
import { 
	ConfigProvider,
	APIProvider, 
	ConverterProvider, 
	StorageProvider 
} from "@providers";

export const createContainer = (): Container => {
	const container = new Container({ defaultScope: "Singleton" });
	
	// processors
	container
		.bind<IConductorProcessor>(TOKENS.CONDUCTOR)
		.to(ConductorProcessor);
	
	const config = await getConfig();
	// providers
	container
		.bind<IConfigProvider>(TOKENS.CONFIG)
		.toValue(config);
	container
		.bind<IAPIProvider>(TOKENS.API)
		.to(APIProvider);
	container
		.bind<IConverterProvider>(TOKENS.CONVERTER)
		.to(ConverterProvider);
	container
		.bind<IStorageProvider>(TOKENS.STORAGE)
		.to(StorageProvider);

  return container;
};	
							
// inversify.config.ts
import "reflect-metadata";
import { Container } from "inversify";
	
import {
  TOKENS,
  getConfig,
  type IConductorProcessor,
  type IConfigProvider,
  type IAPIProvider,
  type IConverterProvider,
  type IStorageProvider,
} from "@infrastructure";
import { ConductorProcessor } from "@processors";
import { 
	ConfigProvider,
	APIProvider, 
	ConverterProvider, 
	StorageProvider 
} from "@providers";

export const createContainer = (): Container => {
	const container = new Container({ defaultScope: "Singleton" });
	
	// processors
	container
		.bind<IConductorProcessor>(TOKENS.CONDUCTOR)
		.to(ConductorProcessor);
	
	const config = await getConfig();
	// providers
	container
		.bind<IConfigProvider>(TOKENS.CONFIG)
		.toValue(config);
	container
		.bind<IAPIProvider>(TOKENS.API)
		.to(APIProvider);
	container
		.bind<IConverterProvider>(TOKENS.CONVERTER)
		.to(ConverterProvider);
	container
		.bind<IStorageProvider>(TOKENS.STORAGE)
		.to(StorageProvider);

  return container;
};	
							
// @processors/conductorProcessor.ts
import { inject, injectable } from "inversify";

@injectable()
export class ConductorProcessor implements IConductorProcessor {
	constructor(
		@inject(TOKENS.API)
		private readonly _apiProvider: IAPIProvider,
		@inject(TOKENS.CONVERTER)
		private readonly _converterProvider: IConverterProvider,
		@inject(TOKENS.STORAGE)
		private readonly _storageProvider: IStorageProvider,
	) {}

	public async processData(): Promise<void> {
		try {
			const data = await this._apiProvider.getData();
			const csv = await this._converterProvider.convertData(data);
			await this._storageProvider(csv);
		} catch(error) {
			console.info("[ConductorProcessor][processData] error");
			throw error;
		}
	}
}
							
// index.ts
import { 
	type APIGatewayProxyEvent, 
	type APIGatewayProxyResult 
} from 'aws-lambda';
import { createContainer } from './inversify.container';
import { TOKENS, type IConductorProcessor } from '@infrastructure';

export const handler = async (
	event: APIGatewayProxyEvent,
): Promise<APIGatewayProxyResult> => {
	try {
		// there might be params
		const container = await createContainer();
		const conductorProcessor = 
			container.get<IConductorProcessor>(TOKENS.CONDUCTOR_PROCESSOR);
		await conductorProcessor.processData();
		
		return {
			statusCode: 200,
			body: JSON.stringify({
				message: 'Success!'
			}),
		};
	} catch (error) {
		console.error(JSON.stringify(error, null, 2));

		return {
			statusCode: 500,
			body: JSON.stringify({
				message: 'Failure!',
				...error
			}),
		};
	}
};
							

Analysis

Refactoring goals

// inversify.config.ts
import "reflect-metadata";
import { Container } from "inversify";
	
import {
  TOKENS,
  getConfig,
  type IConductorProcessor,
  type IConfigProvider,
  type IAPIProvider,
  type IConverterProvider,
  type IStorageProvider,
} from "@infrastructure";
import { ConductorProcessor } from "@processors";
import { 
	ConfigProvider,
	APIProvider, 
	ConverterProvider, 
	StorageProvider 
} from "@providers";

export const createContainer = (): Container => {
	const container = new Container({ defaultScope: "Singleton" });
	
	// processors
	container
		.bind<IConductorProcessor>(TOKENS.CONDUCTOR)
		.to(ConductorProcessor);
	
	const config = await getConfig();
	// providers
	container
		.bind<IConfigProvider>(TOKENS.CONFIG)
		.toValue(config);
	container
		.bind<IAPIProvider>(TOKENS.API)
		.to(APIProvider);
	container
		.bind<IConverterProvider>(TOKENS.CONVERTER)
		.to(ConverterProvider);
	container
		.bind<IStorageProvider>(TOKENS.STORAGE)
		.to(StorageProvider);

  return container;
};	
							

Run #4

Run #5

Conclusions

Conclusions

  • Excessive abstraction is not about maintainability
  • Wrong assumptions
  • Complexity of the solution for non-required capabilities and QAs
  • Absence of efforts planning

Communication is a Key

P.S.

P.S.

  • Communities
  • Research and Development
  • Open Source Solutions

Talk from Aleksandr Kanunnikov on HolyJS

GXT

Рәхмәт!

Part I

Part II

Telegram

LinkedIn