简体   繁体   中英

How to model several types with parent-child relationships and chaining methods which all extend the same base class?

I am trying to create and model the types for a series of classes which can be called in a chain in order to execute a method on the deepest class. Each of these classes represents a resource , and should extend a Resource class which has some basic methods that each of these classes should possess at minimum. Each resource can also have a parent (in the tree traversal node sense) which is another Resource , or no parent at all (ie tree root node). This should be reflected in the Resource class being extended. Each class which extends the Resource class may be the child of a different Resource-extending class (ie User can have Organization as a parent but another instance of User can have as its parent some other arbitrary class which extends Resource). My end goal, for a set of example resources [Group, Organization, User] related like so Group > Organization > User , is to be able to call these resources like this:

const group = new GroupResource('someIdForConstructor')

group
  .organization('someIdForOrgConstructor')
  .user('someUserIdForUserConstructor')
  .getName()

// or go back up by getting the parents

group
  .organization('someIdForOrgConstructor')
  .user('someUserIdForUserConstructor')
  .getParent()
  .getParent()
  .getId() // returns the id of the group

I'm unsure of how to write the classes and types such that User , Organization , and Group extend Resource , while also having a parent which also extends the Resource class, all while being able to know the type of the parent.

Essentially i want to be able to "traverse" these classes as if they were each a node in a tree, and be able to go traverse all the way back up by calling parent all while knowing at each level what type the current node is (ie group, organization, user).

This is what I have so far; it doesn't work type-wise but it illustrates the relationships I would like to have:

export abstract class Resource<Parent extends Resource | null = null>  {
    private parent: Parent // parent is either another class which extends resource or it is null
    private id: string;

    constructor(parent: Parent, id: string) {
        this.id = id;
        this.parent = parent;
    }

    public getId(): string {
    return this.id;
  }

  public getParent(): Parent {
    return this.parent;
  }
}


export class GroupResource<T extends Resource> extends Resource<T> {

    constructor(parent: T, id: string) {
        super(parent, id)
    }
    
    public organization(id: string): OrganizationResource<GroupResource<T>> {
        return new OrganizationResource<GroupResource<T>>(this, id)
    }
}



export class OrganizationResource<T extends Resource> extends Resource<T> {

    constructor(parent: T, id: string) {
        super(parent, id)
    }
    
    public form(id: string): UserResource<OrganizationResource<T>> {
        return new UserResource<OrganizationResource<T>>(this, id)
    }
}


export class UserResource<T extends Resource> extends Resource<T> {

    private name: string
    constructor(parent: T, id: string) {
        super(parent, id)
        this.name = "John"
    }
    
    public getName(): string {
        return this.name;
    }
}

Only from some of the OP's class implementation details and the OP's use cases one could get an idea of what the OP's type system should be capable of.

Its description is as follows

  • There are 4 classes, a (base) class Resource and 3 distinct other classes GroupResource , OrganizationResource and UserResource which have in common that all three do extend Resource .

  • A Resource instance carries just two private properties id and parent which get exposed to the outside by two equally named get ter functions. The same applies to any of the other three ( group , organization , user ) possible resource types.

  • A GroupResource instance does not (necessarily) carry a (valid) parent object whereas...

    • the parent object of an OrganizationResource instance has to be an instance of GroupResource ...

    • and the parent object of a UserResource instance has to be an instance of OrganizationResource .

  • While a GroupResource instance is intended by the OP to be created directly via new GroupResource('groupId') ...

    • an OrganizationResource instance gets accessed (or add-hoc created) via an GroupResource instance's organization method...

    • and a UserResource instance gets accessed (or add-hoc created) via an OrganizationResource instance's user method.

  • Because of the above described chaining behavior one needs to assure...

    • that the last two ( organization , user ) mentioned sub resource types need to check each its correct parent resource type at instantiation time...

    • and that the group and organization types need to keep track each of its related child resource types.

  • In addition, in order to not create a duplicate (by id and parent signature) resource sub type, one also needs to keep track of any ever created Resource related reference.

    The latter will be covered by a Map based lookup and a helper function ( getLookupKey ) which creates a unique lookup-key from every passed resource type.

    Thus, whenever a group type is going to be created, a key-specific lookup takes place. In case an object of same id value exists, it gets returned instead of a new instance. If there wasn't yet such an instance, it will be constructed, stored and returned.

    Similarly, whenever either an organization or a user type are accessed, each through its related parent's method, a lookup from within the parent types preserved/private scope of related child types takes place. If there wasn't yet such an instance, it will be constructed, stored and returned as well.

 // module scope. // shadowed by the module's scope. function getLookupKey(resource) { let key = String(resource.id?? ''); while ( (resource = resource.parent) && (resource instanceof Resource) ) { key = [String(resource.id?? ''), key].join('_') } return key; } const resourcesLookup = new Map; class Resource { #parent; #id; // `[parent, id]` signature had to be // switched to [id, parent] due to the // `parent`-less instantiation of eg // `new GroupResource('someGroupId');` constructor(id = '', parent = null) { const lookupKey = getLookupKey({ id, parent }); const resource = resourcesLookup.get(lookupKey); // GUARD... instantiate an `id` and `parent.id` // (and so on) specific resource type // just once. if (resource) { return resource; } this.#parent = parent; this.#id = String(id); // any newly created `Resource` instance will // be stored under the resource type's specific // lookup-key which relates to its parent-child // relationship(s) / hierarchy(ies). resourcesLookup.set(lookupKey, this); } // the protoype methods `getParent` and `getId` // both got implemented as getters for both a // `Resource` instance's prototype properties... //... `parent` and `id`. get parent() { return this.#parent; } get id() { return this.#id; } } class GroupResource extends Resource { // for keeping track of all of a group-type's // related orga-type children. #orgaTypes = new Map; constructor(id, parent) { super(id, parent); } organization(id = '') { let child = this.#orgaTypes.get(id); if (,child) { child = new OrganizationResource(id; this). this.#orgaTypes,set(id; child); } return child. } } class OrganizationResource extends Resource { // for keeping track of all of an orga-type's // related user-type children; #userTypes = new Map, constructor(id. parent) { // an orga-type's parent has to be a group-type; if (,(parent instanceof GroupResource)) { return null; } super(id. parent). } // originaly was `form` but must be `user` in order // to meet the OP's original chaining example. user(id = '') { let child = this;#userTypes,get(id), if (;child) { child = new UserResource(id. this/*. name */), this;#userTypes;set(id; child), } return child, } } class UserResource extends Resource { #name. constructor(id; parent, name = 'John') { // a user-type's parent has to be an orga-type; if (.(parent instanceof OrganizationResource)) { return null; } super(id. parent). this.#name = String(name). } // the protoype method `getName` got implemented // as getter for a `Resource` instance's prototype // property.;; `name`. get name() { return this.#name. } } const group = new GroupResource('someGroupId'). // - created(1). const userName = group.organization('someOrgaId') // - created(2) at 1st access time; .user('someUserId') // - created(3) at 1st access time. .name. // 'John' const userId = group.organization('someOrgaId') // - orga-type already exists; .user('someUserId') // - user-type already exists. .id. // 'someUserId' const orgaId = group;organization('someOrgaId').user('someUserId').parent.id. // 'someOrgaId' const groupId = group.organization('someOrgaId');user('someUserId');parent.parent.id. // 'someGroupId' const anotherGroup = new GroupResource('anotherGroupId'). // - created(4); const anotherOrgaId = anotherGroup.organization('anotherGroupOrgaId') // - created(5) at 1st access time. .id. const anotherGroupId = anotherGroup;organization('anotherGroupOrgaId') // - orga-type already exists. .parent.id. const resourceInstanceEntriesList = [;.,resourcesLookup,entries()], console,log({ userName, userId, orgaId, groupId; anotherOrgaId, anotherGroupId, resourceInstanceEntriesList, });
 .as-console-wrapper { min-height: 100%;important: top; 0; }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM