By writing down the type of batch in .env, the goal is to be able to process all batches with one program.
The purpose of wanting to process batches with one program is because I do not want to configure a separate CD for each batch.
Even if one program uses the same image, I wanted to perform different operations depending on the purpose, so I used .env. This is because only one type of batch is performed when the program is executed once.
When the number of batchTypes was small, I wrote the function right away.
function oldOne(){
const batchType = process.env.BATCH_TYPE
if(batchType === EnumBatchType.AD_KEYWORD_INPUT){
// do something about ad keyword input
}else if(batchType === EnumBatchType.AD_KEYWORD_OUTPUT){
//do something about ad keyword output
}
...
process.exit(0)
}
When the number of messages I need to handle exceeds 10, I create a class that inherits an interface that can handle those messages.
It was quite satisfying.
export interface Batchable{
doBatch():Promise<void>; // every batch class has this function
}
------ InputBatch, OutputBatch, RefineBatch, and so on...
export abstract class InputBatch<T> implements Batchable {
abstract getDataFromKurve(): Promise<T[]>
abstract getBodyFromData(datas: T[]): Promise<NexusTagDto>
abstract updateNexusTag(body: NexusTagDto): Promise<NexusResponseDto>
async doBatch(): Promise<void> {
const datas = await this.getDataFromKurve()
const body = await this.getBodyFromData(datas)
const res = await this.updateNexusTag(body)
unifiedLog('batch-finish', JSON.stringify(res), true)
}
}
------
export class Batch1 extends InputBatch<Something> {...}
export class Batch2 extends InputBatch<Something> {...}
export class Batch3 extends OutputBatch<Something> {...}
export class Batch4 extends RefineBatch<Something> {...}
export class Batch5 extends InputBatch<Something> {...}
export class Batch6 extends OutputBatch<Something> {...}
export class Batch7 extends InputBatch<Something> {...}
export class Batch8 extends InputBatch<Something> {...}
Of course, each batch has a name for what it is, but for security reasons, I wrote them as Batch1, Batch2, ...
Excluding the switch and each batch codes, the all of code is bellow.
export async function batchStart() {
const batchType = process.env.BATCH_TYPE
unifiedLog('batch_start', process.env.TARGET_SCHEMA)
const client = getBatchClient(batchType as EnumBatchType)
if (client) {
await client.doBatch()
process.exit(0)
}
}
When I had over 50 messages to process, I realized that all I had done was push a pile of trash out of sight.
The problem is that the number of batchTypes currently exceeds 60, and as a result, the getBatchClient function with only switch-case exceeds 200 lines.
My company has a rule that switches must be sorted alphabetically.
Now I have to do a lot of scrolling to find where I should put the message.
function getBatchClient(batchType: EnumBatchType): Batchable {
let client: Batchable
switch (batchType as EnumBatchType) {
// input
case EnumBatchType.BATCH_TYPE1:
client = new Batch1()
break
case EnumBatchType.BATCH_TYPE2:
client = new Batch2()
break
case EnumBatchType.BATCH_TYPE3:
client = new Batch3()
break
case EnumBatchType.BATCH_TYPE4:
client = new Batch4()
break
case EnumBatchType.BATCH_TYPE5:
client = new Batch5()
break
....
default:
unifiedError('invaild_type', batchType)
new SlackClient('Init Batch').sendErrorMessage({
message: `${batchType} is invalid type (from ${process.env.JOBTAG})`,
target: EnumNotiTarget.BATCHCODE
})
return null
}
return client
}
The number of message types is likely to increase, and I don't like seeing switch statements that are too long.
I also considered using a map with key-value message-client, but it is not a good choice because it only uses one message handler and the program terminates.
It feels like a waste to create all the other classes when I'm only going to use one.
function useMapCase(batchType: EnumBatchType): Batchable {
const map = new Map<EnumBatchType, Batchable>([
[EnumBatchType.BATCH_TYPE1, new Batch1()],
[EnumBatchType.BATCH_TYPE2, new Batch2()],
[EnumBatchType.BATCH_TYPE3, new Batch3()],
[EnumBatchType.BATCH_TYPE4, new Batch4()],
[EnumBatchType.BATCH_TYPE5, new Batch5()],
[EnumBatchType.BATCH_TYPE6, new Batch6()]
])
return map.get(batchType)
}
If the number of batches exceeds 200, I will see that the map constructor is over 200 lines long.
I don't even have power, so it is not possible to change the layout so that it can be set in several steps as shown below.
If I were to use the method from another article, it would be like this:
function otherWays(batchType: SomeObject): Batchable {
const { batchType, level1, level2 } = batchType
try {
const batchTypeMap = new Map<string, BatchSelector>([
['input', new InputBatchSelector()],
['output', new OutputBatchSelector()],
['refine', new RefineBatchSelector()]
])
const level1Map = batchTypeMap.get(batchType)
const level2Map = level1Map.get(level1)
return level2Map.get(level2)
} catch (e) {
unifiedError('invaild_type', batchType)
new SlackClient('Init Batch').sendErrorMessage({
message: `${JSON.stringify(batchType)} is invalid type (from ${process.env.JOBTAG})`,
target: EnumNotiTarget.BATCHCODE
})
return null
}
}
Unlike the above method, it is divided into parts, so if I create a map when doing get and process it, there is no need to create an entire class.
Although it does not create only one object, this is acceptable.
So, if I can use this method, I will be happy and you who read this will be happy too. But You may not be able to understand, I cannot determine the text of the batchType.
So it is impossible to split batchType like that and it cannot be used.
If there was any regularity in the batchType, the decision would be made based on that, but there is no such thing.
Is there any way?
Should I just be covered in hundreds of cases?
Or should I use map even if it wastes memory due to the variables inside of an object that will not be used?
I also considered using a map with key-value message-client, but it is not a good choice because it only uses one message handler and the program terminates.
One could argue that defining all of the batch classes is equally unnecessary if you're only going to use one on any given run.
Now I have to do a lot of scrolling to find where I should put the message.
That tells me that the fundamental problem you're having with the switch
statement is it's a pain when dealing with this large list. That being the case, it's going to be a pain whether it's a large switch
or a large initializer on a Map
or any other list-based solution.
Assuming the values in EnumBatchType
can be meaningfully converted to strings, you could consider dynamically importing the batch class. Then you don't have a massive list of these anywhere except the file system, and changes you make to the code in any one batch are clearly identified in source control as relating only to that batch, not others defined in the same source file (since each batch is in its own file).
Here's a sketch of that approach:
Instead of:
export class Batch1 extends InputBatch<Something> {/*...*/}
export class Batch2 extends InputBatch<Something> {/*...*/}
export class Batch3 extends OutputBatch<Something> {/*...*/}
export class Batch4 extends RefineBatch<Something> {/*...*/}
export class Batch5 extends InputBatch<Something> {/*...*/}
export class Batch6 extends OutputBatch<Something> {/*...*/}
export class Batch7 extends InputBatch<Something> {/*...*/}
export class Batch8 extends InputBatch<Something> {/*...*/}
You could have Batch1
in a file named for the string equivalent of EnumBatchType.BATCH_TYPE1
(say, batches/BatchType1.ts
):
export default class Batch1 extends InputBatch<Something> {/*...*/}
...and similarly, batches/BatchType2.ts
:
export default class Batch2 extends InputBatch<Something> {/*...*/}
...and so on. (For me, this is one of the very few acceptable uses of default exports.)
Then, getting the right batch class to use is roughly:
export async function batchStart() {
const batchType = process.env.BATCH_TYPE;
unifiedLog('batch_start', process.env.TARGET_SCHEMA);
const client = await getBatchClient(batchType as EnumBatchType);
// ^−−− note
if (client) {
await client.doBatch();
process.exit(0);
}
}
// ...
async function getBatchClient(batchType: EnumBatchType): Promise<Batchable> {
const BatchClass = (await import(`./batches/${batchType}.ts`)).default as new () => Batchable;
const client = new BatchClass();
return client;
}
Or if you want it to return null
as it current does when there's no matching batch class, you could have getBatchClient
handle the import error if the batch class didn't exist:
async function getBatchClient(batchType: EnumBatchType): Promise<Batchable> {
let importError = true;
try {
const BatchClass = (await import(`./batches/${batchType}.ts`)).default as new () => Batchable;
importError = false;
const client = new BatchClass();
return client;
} catch (error) {
if (importError) {
return null; // Like your current code does
}
throw error;
}
}
In both cases, this does have the concern that if the default export of one of these files is not a valid constructor for Batchable
, the code will be incorrect in a way TypeScript can't detect for you, but it offloads the need for massive lists to the file system.