I have a couple of Factory classes that do the same:
export class XFactory {
private readonly validator: XValidator;
private readonly transformer: XTransformer;
private constructor( private readonly data: any, connection: Connection) {
this.validator = new XValidator(connection);
this.transformer = new XTransformer();
}
public static async build(data: any, connection: Connection) {
const factory = new XFactory (connection);
await factory.validate();
const dto = await factory.transform();
const x = new X(dto, connection);
return x;
}
private async validate(): Promise<void | never> {
await this.validator.validate(this.data);
}
private async transform(): Promise<XDto> {
return await this.transformer.transform(this.data);
}
}
The process is always the same:
- Validate data, throwing an error if not valid
- Transforming the data into a valid DTO consumed by a constructor
- Instantiating the object and return it
I'm having problems creating an abstraction of this class:
I have tried with a parent Factory like this, but i have two problems, marked with an asterisk:
export class Factory<T,U> {
private constructor(
private readonly data: any,
private readonly validator: Validator,
private readonly transformer: Transformer<U>
) {}
protected static
public static async build(data: any, validator: Validator, transformer: Transformer<U>*1, connection: Connection) {
const factory = new Factory (data, validator, transformer);
await factory.validate();
const dto: U*1 = await factory.transform();
const x: T*1 = new T*2(...[dto, connection]);
return x;
}
private async validate(): Promise<void|never> {
await this.validator.validate(this.data);
}
private async transform(): Promise<XDto> {
return await this.transformer.transform(this.data);
}
}
*1 Static members cannot reference class type parameters: this is a minor problem, but I'd love to know a workaround for this. *2 Cannot find name 'T': Is there a way to instantiate this class? I know typescript is uncapable of translating this to js, i just tried to write something that make my intentions clear.
Also the constructor method is private because I don't want to separate validation and transformation from the constructor, but they have an asyncronous nature.
¿Is there another approach to solve this problem or can my code be modified so I can refactor those XFactory classes?
CodePudding user response:
*1 Static members cannot reference class type parameters.
This is like asking a printing press who its author is. The press printed many books, all of which have different authors. The request doesn't make sense.
The class type parameters are per instance. A different instance may have different type parameters. And static methods have no intrinsic relationship with the class they are on other than convention. It's just a namespace to group related functionality. And only by convention do those functions do tasks that are related to the instances.
I think what you want here is for the static build
method to be generic, so it can create a Factory
instance with the correct types. Then you can simply use that instance.
// Make up some types to support what follows.
type Validator<Data> = { validate(data: Data): Promise<boolean> }
type Transformer<Data, Dto> = { transform(data: Data): Dto }
type Connection = { isConnected: boolean }
export class Factory<Data, Dto> {
private constructor(
private readonly data: Data,
private readonly validator: Validator<Data>,
private readonly transformer: Transformer<Data, Dto>
) {}
public static async build<Data, Dto, DtoInstance>(
data: Data,
validator: Validator<Data>,
transformer: Transformer<Data, Dto>,
dtoConstructor: new (dto: Dto, connection: Connection) => DtoInstance,
connection: Connection
): Promise<DtoInstance> {
const factory = new Factory(data, validator, transformer);
await factory.validate();
const dto = await factory.transform();
return new dtoConstructor(dto, connection);
}
private async validate(): Promise<void|never> {
await this.validator.validate(this.data);
}
private async transform(): Promise<Dto> {
return await this.transformer.transform(this.data);
}
}
I've renamed T
and U
here to Data
and Dto
, respectively. Data
is the input format, and Dto
is the output of the transform.
Now note how the build
function is also generic. It accepts its own generic type parameters (I've used the same names, but they are totally disconnected). This allows the arguments to the Factory
constructor to be specifically typed when build()
is called, which then sets the types of the instance.
Lastly, build()
also accepts a constructor, which will take the transform's output and return an instance of something. That instance type we capture as a generic from the constructor to set the return type.
Now to test it:
// mock a connection
const connection = { isConnected: true }
class TestDto {
public finalNum: number
constructor(data: { finalNum: number }, connection: Connection) {
this.finalNum = data.finalNum
}
}
async function test() {
const testDto = await Factory.build(
{ srcNum: 123 },
{ validate: async (data) => data.srcNum > 0 },
{ transform: (data) => ({ finalNum: data.srcNum }) },
TestDto,
connection
)
console.log(testDto) // TestDto: { finalNum: 123 }
console.log(testDto.finalNum) // 123
}
test()
All that said... why are you using a class here at all? This would be far simpler if it was a pure function. The implementation is 3 short lines.
async function build<Data, Dto, DtoInstance>({
data,
validate,
transform,
dtoConstructor,
connection
}: {
data: Data,
validate: (data: Data) => Promise<void>,
transform: (data: Data) => Dto,
dtoConstructor: new (dto: Dto, connection: Connection) => DtoInstance,
connection: Connection,
}): Promise<DtoInstance> {
await validate(data);
const dto = transform(data);
return new dtoConstructor(dto, connection);
}
Which you would use like so:
// Make a reusable function bound to a specific Dto.
// Or maybe add as a static method of `TestDto`, `TestDto.build()`?
async function buildTestDto(data: { srcNum: number }) {
return await build({
data,
validate: async (data) => {
if (data.srcNum < 0) throw new Error("value must be positive")
},
transform: (data) => ({ finalNum: data.srcNum }),
dtoConstructor: TestDto,
connection,
})
}
async function go() {
const testDto = await buildTestDto({ srcNum: 123 })
console.log(testDto) // TestDto: { finalNum: 123 }
console.log(testDto.finalNum) // 123
const invalid = await buildTestDto({ srcNum: -456 }) // throws
console.log(invalid)
}
go()