У меня есть древовидная структура, которая отслеживает набор учетных записей. У учетных записей может быть упорядоченный набор братьев и сестер, родителей и детей (все они также являются учетными записями), и я создал класс 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;
}
}