Являются ли предупреждения о циклической зависимости проблемой, которую необходимо решить? - PullRequest
1 голос
/ 12 июля 2020

У меня есть древовидная структура, которая отслеживает набор учетных записей. У учетных записей может быть упорядоченный набор братьев и сестер, родителей и детей (все они также являются учетными записями), и я создал класс IndexEntry, чтобы отслеживать все это.

Я разработал свой класс Account с IndexEntry, а мое свойство IndexEntry имеет свойство account, которое возвращает учетную запись, на которую он ссылается, что позволяет мне писать такие вещи, как myAccount.indexEntry.prevSibling.account.title, чтобы возвращать заголовок учетной записи, которая является предыдущим родственником myAccount.

Все мои модульные тесты работают нормально, и эта настройка позволяет моим компонентам легко перемещаться по дереву и представлять нужные мне данные. Но я получаю все эти предупреждения о циклической зависимости в журналах, но я не уверен, следует ли мне просто терпеть их. Первоначально у меня были и Account, и IndexEntry, определенные в одном файле, но файлы на мой вкус стали немного длиннее, поэтому я разбил их и создал модуль AccountModule, состоящий только из

export [Account] from './account.ts'
export [IndexEntry] from './index-entry.ts'

Я знаю есть различные шаблоны, которые я мог бы использовать, чтобы избежать этого, но я не уверен, что понимаю, в чем проблема. Должен ли я просто игнорировать предупреждения, есть ли способ их контролировать или есть какая-то фундаментальная проблема дизайна, которую мне нужно решить?

Обновить TL; DR; код следует в соответствии с запросом

КЛАСС СЧЕТА

import { Account, AccountType} from '../../common/interfaces/account.model'
import { IndexEntry } from './IndexEntry'
import { DataClass } from '../data-class';
 
export interface AccountNode {
    id: string;
    depth: number;
    title: string;
    total: number;
    number: string;
    children?: AccountNode[];
    index : number;
}

export interface AccountsEvent {
    action: AccountsAction;
    id: string | undefined;
    budgetId: string;
}


export enum AccountsAction {
    chartChanged = "chartChanged",  //called when the chart of accounts is modified
    //(when accounts are added or deleted)
    closed = "closed"  //calls when a budget is closed and all accounts are flushed. 
}
export class AccountClass extends DataClass<Account> {//implements IMapSubject {
    static depthNames: string[] = ["Budget", "Section", "Account", "Subaccount", "LineItems"]
    private static _nullAccount: AccountClass
    static get nullAccount(): AccountClass {
        let ac: Account
        if (!this._nullAccount) {
            let ac = <Account>{}
            ac.id = "null_account";
            ac.index = -1;
            ac.title = "Null Account";
            ac.timestamp = -1;
            ac.total = 0;
            ac.priorTotal = 0;
            ac.type = AccountType.null;
            this._nullAccount = new AccountClass(ac);
            this._nullAccount.indexEntry = IndexEntry.nullEntry;
        }

        return this._nullAccount;
    }
    indexEntry: IndexEntry;
    
    constructor(accountObj: Account) {
        super(accountObj);
    }

    /* accessors */


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

    public get index(): number {
        return this.object.index;
    }
    public set index(v: number) {
        this.object.index = v;
    }

    public get title(): string {
        return this.object.title;
    }
    public set title(v: string) {
        this.object.title = v;
    }

    public get sectionId(): string {
        return this.object.sectionId
    }
    public set sectionId(v: string) {
        this.object.sectionId = v;
    }

    public get parentId(): string {
        return this.object.parentId
    }
    public set parentId(v: string) {
        this.object.parentId = v;
    }

    public get subaccountId(): string {
        return this.object.subaccountId;
    }
    public set subaccountId(v: string) {
        this.object.subaccountId = v;
    }
    public get accountId(): string {
        return this.object.accountId;
    }
    public set accountId(v: string) {
        this.object.accountId = v;
    }

    public get type(): AccountType {
        return this.object.type;
    }
    public set type(v: AccountType) {
        this.object.type = v;
    }

    public get masterIndex(): string {
        return this.object.masterIndex;
    }
    public set masterIndex(v: string) {
        this.object.masterIndex = v;
    }
    public get number(): string {
        return this.object.number;
    }
    public set number(v: string) {
        this.object.number = v;
    }

    public get depth(): number {
        return this.object.depth;
    }
    public set depth(v: number) {
        this.object.depth = v;
    }

    public get total(): number {
        return this.object.total;
    }
    public set total(v: number) {
        this.object.total = v;
    }
    public get priorTotal(): number {
        return this.object.priorTotal;
    }
    public set priorTotal(v: number) {
        this.object.priorTotal = v;
    }
    public get timestamp(): number {
        return this.object.timestamp;
    }

    public get budgetId(): string {
        return this.object.budgetId;
    }

    public set budgetId(v: string) {
        this.object.budgetId = v;
    }
    public get isNull(): boolean {
        return this.indexEntry.isNull;
    }
     
    /**
     * Used by Chart of Accounts component for display purposes.
     */
    toTreeNode(): AccountNode {
        let node: AccountNode = <AccountNode>{};
        node.id = this.id;
        node.depth = this.depth;
        node.title = this.object.title;
        node.total = this.object.total;
        node.number = this.object.number;
        node.index = this.index;
        node.children = <any>[];

        for (let child of this.indexEntry.orderedChildren) {
            node.children.push(child.account.toTreeNode())
        }
        return node;
    }
}

Класс IndexEntry

import * as utils from '../../utils/utilityfunctions';
import { AccountClass } from './account-class';
export class IndexEntry {
    id: string;
    depth: number;
    account: AccountClass;
    private _isNull: boolean = false;
    children: IndexEntry[] = [];
    private _parent: IndexEntry;
    private _nextSibling: IndexEntry;
    private _prevSibling: IndexEntry;
    private _prevEntry: IndexEntry;
    private _nextEntry: IndexEntry; //this represents the next item in the master sort order
    private _title: string;
    private static _nullEntry: IndexEntry;


    /**
     * Gets the singleton, readonly  "nullEntry" index entry.
     */

    static get nullEntry(): IndexEntry {
        if (!this._nullEntry) {
            let nullent = new IndexEntry(null);
            nullent.depth = -1;
            nullent.id = "null_entry";
            nullent._isNull = true;
            nullent._parent = nullent;
            nullent._nextSibling = nullent;
            nullent._prevSibling = nullent;
            nullent._nextEntry = nullent;
            nullent._prevEntry = nullent;
            this._nullEntry = nullent;
            this._nullEntry.account = AccountClass.nullAccount;
        }
        return this._nullEntry;
    }

    
    /**Find's entry last child, grandchild or great grandchild. */
    static findLastDescendent(entry : IndexEntry ) : IndexEntry {
        if (!entry.hasChildren) {return entry};
        let lastChild = entry.lastChild;
        if (lastChild.hasChildren) {
             lastChild = this.findLastDescendent(lastChild);
        } 
        return lastChild;
    }
    /**
     * Generates a IndexEntry for an account based on either the immediately prior account (inserts after)
     * or the immediately following account (inserts before).  In either case, the referenced adjacent account
     * is the account is the chart that follows or precedes, irrespective of depth.
     * @param item The AccountClass for the created indexItem
     * @param prev Either null or the entry for the immediately preceding account.
     * @param next Either null or the entry for the immediately following account.
     */
    constructor(item: AccountClass, prev?: IndexEntry, next?: IndexEntry) {
        if (!item)
            return;
        this.account = item;
        this.id = item.id;
        this.depth = item.depth;
        this.children = [];
        this._title = item.title;
        if (item.depth == 0) {
            this.makeBudgetRootIndex(item);
            return;
        }
        if (!prev && !next) {
            return;
        }
        if (prev && !next) {
            this.insertAfter(prev, item);
            return;
        }
        if (next && !prev) {
            this.insertBefore(next, item);
            return;
        }
        if (next && prev) {
            if (next.prevEntry == prev && prev.nextEntry == next) {
                //these two are adjacent, just use insertAfter
                this.insertAfter(prev, item);
            }
            else {
                throw new Error(`IndexEntry.constructor:  If both 'prev' and 'next' are supplied, they must be adjacent. 
                 prev:  ${prev.account.title} - depth ${prev.account.depth} next ${next.account.title} - depth: ${next.account.depth}`);
            }
        }
    }
    private makeBudgetRootIndex(item: AccountClass) {
        this.account = item;
        this.depth = 0;
        this.children = [];
    }

    /**
     * Unlike the children property which returns children in the order in which they are added,
     * the orderedChildren property returns array which respects the specified order as determined by nextSibling, starting with the firstChild
     * (i.e. the child which has no prevSibling).
     * orderedChildren returns an array of children, ordered from firstChild to lastChild, calling next sibling recursively until it encounters a 
     * child with no nextSibling. 
     */
    get orderedChildren() : IndexEntry[] {
        if (this.childCount == 0) {return [];}
         
        let retval : IndexEntry[] =[];
        let iChild = this.firstChild;
        retval.push(iChild);
        while (!iChild.nextSibling.isNull) {
            retval.push(iChild.nextSibling);
            iChild = iChild.nextSibling;
        }
        if (this.childCount  != retval.length) {
            throw new Error("IndexEntry.getOrderedChildren() didn't completely iterate the child collection");
        }
        return retval;
    }

    private insertAfter(prev: IndexEntry, item: AccountClass) {
        utils.assert(!prev.isNull, "IndexEntry.constructor:  Cant insert an item after a null item.");
        utils.assert(prev.account !== item, "Index entry -- Account can't precede itself");
         
        let next = prev.nextEntry;
        this.prevEntry = prev;
        this.nextEntry = next;
        prev.nextEntry = this;
        next.prevEntry = this;

        let depthDiffToPrior = this.depth - prev.depth;

        utils.assert(depthDiffToPrior >= -2 && depthDiffToPrior <= 1, `IndexEntry.insertEntry failed.  Invalid placement of new account:
            priorEntry (${prev.account?.title} is depth ${prev.depth} item ${this.account?.title} depth is ${this.depth}`);

        switch (depthDiffToPrior) {
            case 0:
                this.insertAsNextSibling(prev);
                break;
            case 1:
                this.insertAsFirstChild(prev);
                break;
            case -1:
                this.insertAsNextUncle(prev); //current Account: prior Subacount || current Section, Prior Account
                break;
            case -2:
                this.insertAsNextGreatUncle(prev); //curent: Section, prior: SubAccount
                break;
        }
        //this.parent.addChild(this);
        this.prevSibling.nextSibling = this;
        this.nextSibling.prevSibling = this;
    }


    private insertAsNextSibling(priorSibling: IndexEntry) {

        this.prevSibling = priorSibling;
        if (!priorSibling.nextSibling.isNull) {
            this.nextSibling = priorSibling.nextSibling;
        }
        this.prevSibling.nextSibling = this;
        this.parent = priorSibling.parent;
        utils.assert(this.nextSibling !== this, "IndexEntry.insertAsNextSibling created circular sibling");
    }
    private insertAsFirstChild(parent: IndexEntry) {
        utils.assert(!parent.isNull);
        utils.assert(!this.isNull);

        this.prevSibling = IndexEntry.nullEntry;
        this.nextSibling = parent.firstChild;

        utils.assert(this.nextSibling !== this, `IndexEntry.insertAsFirstChild created circular sibling 
        this:  ${this.account.title}  nextSibling ${this.nextSibling.account.title}`);

        this.parent = parent;
    }

    private insertAsNextUncle(prevNephew: IndexEntry) {
        this.prevSibling = prevNephew.parent;
        this.nextSibling = this.prevSibling.nextSibling;
        this.parent = prevNephew.parent.parent;
        utils.assert(this.nextSibling !== this, "IndexEntry.insertAsNextUncle created circular sibling");
    }

    private insertAsNextGreatUncle(prevGrandNewphew: IndexEntry) {
        this.prevSibling = prevGrandNewphew.parent.parent;
        this.nextSibling = this.prevSibling.nextSibling;
        this.parent = prevGrandNewphew.parent.parent.parent;
        utils.assert(this.nextSibling !== this, "IndexEntry.asNextGreatUncle created circular sibling");
    }

    insertBefore(next: IndexEntry, item: AccountClass) {
        this.insertAfter(next.prevEntry, item);
    }

    get isNull(): boolean {
        return this._isNull;
    }
    get parent(): IndexEntry {
        if (this._parent) { return this._parent; }
        return IndexEntry.nullEntry;
    }
    set parent(parent: IndexEntry) {
        if (this.isNull)
            return;
        if (!parent || parent.isNull)
            return;
        this._parent = parent;
        parent._addChild(this);
    }
    //must only be called by set parent
    private _addChild(child: IndexEntry) {
        if (this.isNull || child.isNull)
            return;
        if (this.children.lastIndexOf(child) == -1) {
            this.children.push(child);
        }
    }
    /**
     * The IndexEntry for the next account at the same depth as this one.
     */
    get nextSibling(): IndexEntry {
        if (this._nextSibling) { return this._nextSibling; }
        return IndexEntry.nullEntry;
    }
    set nextSibling(v: IndexEntry) {
        if (this.isNull)
            return;
        if (!v) {
            this._nextSibling = IndexEntry.nullEntry;
            return;
        }
        this._nextSibling = v;
    }
    /**
     * The IndexEntry for the account of the same depth which preceds this one.
     */
    get prevSibling(): IndexEntry {
        if (this._prevSibling) { return this._prevSibling; }
        return IndexEntry.nullEntry;
    }
    set prevSibling(v: IndexEntry) {
        if (this.isNull)
            return;
        if (!v) {
            this._prevSibling = IndexEntry.nullEntry;
            return;
        }
        this._prevSibling = v;
    }
    /**
     * The account which precedes this one in the chart, irrespective of depth.
     */
    get prevEntry(): IndexEntry {
        if (this._prevEntry) { return this._prevEntry; }
        return IndexEntry.nullEntry;
    }
    set prevEntry(v: IndexEntry) {
        if (this.isNull)
            return;
        if (!v) {
            this._prevEntry = IndexEntry.nullEntry;
            return;
        }
        this._prevEntry = v;
    }
    /**
     * The account which follows this one in the chart, irrespective of depth.
     */
    get nextEntry(): IndexEntry {
        if (this._nextEntry) { return this._nextEntry; }
        return IndexEntry.nullEntry;
    }
    set nextEntry(v: IndexEntry) {
        if (this.isNull)
            return;
        if (!v) {
            this._nextEntry = IndexEntry.nullEntry;
            return
        }
        this._nextEntry = v;
    }
    /**The first child of this account in Index order.  This may be different
     * than the order of accounts in the child list array. To find the first child,
     * look for the child that has no prevSibling;
     */
    get firstChild(): IndexEntry {
        if (this.childCount == 0) { return IndexEntry.nullEntry; }
        for (let child of this.children) {
            if (child.prevSibling == IndexEntry.nullEntry) {
                return child;
            }
        }
    }
    get lastChild(): IndexEntry {
        if (this.childCount == 0) { return IndexEntry.nullEntry; };
        let ichild = this.firstChild;
        while (!ichild.nextSibling.isNull) {
            utils.assert(ichild !== ichild.nextSibling, "Circular sibling relationship in IndexEntry");
            ichild = ichild.nextSibling;
        }
        return ichild;
    }
    get fullyQualifiedTitle() : string {
        if (this.depth==0) {return "Budget Root"};
        if (this.depth==1) {
            return this.account.title;
        }
        if (this.depth==2) {
            return this.parent.account.title + ":" + this.account.title;
        }
        if (this.depth==3) {
            return this.parent.parent.account.title + ":" + this.parent.account.title + ":" + this.account.title;
        }
    }

 
    get isDirty() {
        return this.account.isDirty;
    }

    //Required to avoid circular conversion to JSON beetween AccountClass and IndexEntry which each reference each other.
    toJSON() {
        return undefined;
    }


    get masterIndex(): string {
        return this.account.masterIndex;
    }
    set masterIndex(value: string) {
        this.account.masterIndex = value;
    }


    get hasChildren(): boolean {
        return (this.childCount > 0);
    }
    get childCount(): number {
        return this.children.length;
    }

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...