如何让HttpClient.get返回抽象类的实际子类实例并存入缓存?
我定义了抽象类DataEntry:
export abstract class DataEntry { ... public abstract clone(): DataEntry; }
以及继承它的实际子类Record:
export class Record extends DataEntry { ... public clone(): Record { return new Record(...); } }
通过DbFacadeService的HttpClient.get从数据库请求数据:
export class DbFacadeService { public static BASEURL: string = 'http://localhost:3000'; constructor(private _http: HttpClient) { } public readAll(): Observable<DataEntry[]> { return this._http.get<DataEntry[]>(`${DbFacadeService.BASEURL}`, { observe: 'response' }) .pipe(map((response: HttpResponse<DataEntry[]>) => response.body || []), first()); } ... }
再通过DataHandlerService定期调用该方法存入缓存:
export class DataHandlerService { ... constructor(private _cache: Map<string, DataEntry>, private _dbFacade: DbFacadeService) { this._cache = new Map<string, DataEntry>([]); timer(0, 60000).subscribe(() => this.updateCache()); } private updateCache(): void { this._dbFacade.readAll() .subscribe((entries: DataEntry[]) => { entries.forEach((entry: DataEntry) => this._cache.set(entry.id, entry)); ... }); } public readEntry(id: string): DataEntry | undefined { return this._cache.get(id); } }
在EditorComponent调用缓存数据时,调用clone方法触发运行时异常——缓存里的对象只是HTTP返回的普通对象,不是真正的DataEntry或Record实例,没有clone方法。
尝试过类型转换,但无法创建实际的DataEntry对象:
.pipe(map((response: HttpResponse<DataEntry[]>) => response.body?.map((entry: any) => entry as DataEntry) || [])
已知需要手动转换,但无法实例化抽象类DataEntry,且DataHandlerService不知道具体子类类型。希望缓存中存储实际的Record等子类实例而非any类型,求可行方案,是否只能每次取缓存时再创建实例?
方案1:给子类添加静态工厂方法(推荐)
在每个DataEntry子类中添加静态方法实现从普通对象到子类实例的转换,同时让抽象类定义这个规范:
首先修改抽象类:
export abstract class DataEntry { public abstract id: string; public abstract clone(): DataEntry; // 定义工厂方法规范,子类必须实现 public static fromJSON<T extends DataEntry>(json: any): T { throw new Error('子类必须实现fromJSON方法'); } }
然后在Record子类实现该方法:
export class Record extends DataEntry { public id: string; // 其他业务属性... constructor(id: string, /* 其他初始化参数 */) { super(); this.id = id; // 初始化其他属性 } public clone(): Record { return new Record(this.id, /* 复制其他属性 */); } // 实现工厂方法,映射JSON属性到类实例 public static fromJSON(json: any): Record { return new Record( json.id, json.title, // 示例属性,根据实际业务补充 json.content ); } }
接下来需要后端返回的数据中携带类型标识(比如type: "record"),然后在DbFacadeService中维护类型到子类的映射并处理转换:
export class DbFacadeService { // 维护类型与子类的映射关系,新增子类时在这里补充 private typeToClassMap: Record<string, typeof DataEntry> = { 'record': Record }; public static BASEURL: string = 'http://localhost:3000'; constructor(private _http: HttpClient) { } public readAll(): Observable<DataEntry[]> { return this._http.get<any[]>(`${DbFacadeService.BASEURL}`, { observe: 'response' }) .pipe( map(response => { const rawEntries = response.body || []; return rawEntries.map(entry => { const clazz = this.typeToClassMap[entry.type]; if (!clazz) { throw new Error(`未知数据类型: ${entry.type}`); } return clazz.fromJSON(entry); }); }), first() ); } }
处理后readAll返回的就是真正的子类实例,存入缓存后调用clone方法不会再触发异常。
方案2:使用类转换器库
借助class-transformer库自动完成普通对象到类实例的转换,无需手动编写工厂方法:
首先安装依赖:
npm install class-transformer
在子类属性上添加装饰器标记需要映射的字段:
import { Expose } from 'class-transformer'; export class Record extends DataEntry { @Expose() public id: string; @Expose() public title: string; // 其他业务属性... public clone(): Record { return new Record(this.id, this.title); } }
然后修改DbFacadeService的转换逻辑:
import { plainToInstance } from 'class-transformer'; export class DbFacadeService { private typeToClassMap: Record<string, typeof DataEntry> = { 'record': Record }; public static BASEURL: string = 'http://localhost:3000'; constructor(private _http: HttpClient) { } public readAll(): Observable<DataEntry[]> { return this._http.get<any[]>(`${DbFacadeService.BASEURL}`, { observe: 'response' }) .pipe( map(response => { const rawEntries = response.body || []; return rawEntries.map(entry => { const clazz = this.typeToClassMap[entry.type]; if (!clazz) { throw new Error(`未知数据类型: ${entry.type}`); } return plainToInstance(clazz, entry); }); }), first() ); } }
方案3:在读取缓存时转换(备选)
如果不想在请求阶段处理转换,也可以在DataHandlerService读取缓存时完成实例化,但每次读取都会创建新实例,适合读取频率较低的场景:
export class DataHandlerService { // 缓存存储原始JSON对象 private _cache: Map<string, any> = new Map<string, any>([]); private typeToClassMap: Record<string, typeof DataEntry> = { 'record': Record }; constructor(private _dbFacade: DbFacadeService) { timer(0, 60000).subscribe(() => this.updateCache()); } private updateCache(): void { this._dbFacade.readAll() .subscribe((rawEntries: any[]) => { rawEntries.forEach(entry => this._cache.set(entry.id, entry)); }); } public readEntry(id: string): DataEntry | undefined { const rawEntry = this._cache.get(id); if (!rawEntry) return undefined; const clazz = this.typeToClassMap[rawEntry.type]; if (!clazz) throw new Error(`未知数据类型: ${rawEntry.type}`); return clazz.fromJSON(rawEntry); } }
内容的提问来源于stack exchange,提问作者Otto Abnormalverbraucher

