Analysis Patterns 1.0 Help

7. Использование моделей учета

Чтобы полностью понять эту главу, вам нужно сначала прочитать главу 6. Это необычная глава в данной книге. Вместо того чтобы описывать группу паттернов, в этой главе мы рассмотрим как можно использовать паттерны, представленные в предыдущей главе. Это непростая задача, потому что бухгалтерские паттерны довольно абстрактны. Чтобы понять, как на самом деле они работают, нам нужно рассмотреть полностью рабочий пример.

В этой главе рассматриваются счета и правила проводок, используемые в модели телефонной компании Total Telecommunications (TT). В лучших традициях учебников примеры, представленные здесь, несколько упрощены. Их должно быть достаточно, чтобы вы хотя бы почувствовали, как работают модели. Цель состоит в том, чтобы проиллюстрировать использование модели счета, а не моделировать телефонную компанию.

Поскольку это глава с примерами, я использовал некоторое количество кода для иллюстрации примеров. Я выбрал Digitalk Smalltalk вместо C++, потому что с помощью Smalltalk мне проще передать основные идеи. Концепции должны быть легко переносимы на C++. При преобразовании моделей я использовал паттерны из главы 14. Также были использованы паттерны кодирования Кента Бека [1], с некоторыми вариациями. Должен подчеркнуть, что не было предпринято никаких попыток оптимизировать код. Приведен даже не полный код, а только его основные моменты.

Базовый тарифный план TT очень прост. Все звонки делятся на дневные и ночные. Дневной период длится с 7:00 утра до 7:00 вечера. Классификация основана на времени начала звонка (для простоты я опустил случай, когда звонок пересекает границу). Дневные звонки стоят 98 центов за первую минуту и 30 центов за последующие минуты. Ночные звонки стоят 70 центов за первую минуту, 20 центов за последующие 20 минут и 12 центов за все последующее время. Правительство взимает 6% налог с первых 50 долларов США за календарный месяц и 4% с последующих звонков.

Глава начинается с обсуждения структурных моделей (7.1), которые, естественно, основаны на паттернах, представленных в главе 6. Затем мы рассмотрим некоторые интересные особенности реализации структуры (7.2). Чтобы настроить объекты, мы начинаем с создания новых телефонных услуг (7.3), а затем настраиваем звонки (7.4). После этого мы впервые рассмотрим правила проводки, изучив код для реализации запуска на основе учетной записи (7.5). Приводятся три примера правил проводки: разделение звонков на дневные и ночные (7.6), тарификация по времени (7.7) и расчет налога (7.8). Каждое правило иллюстрирует определенный аспект поведения. Первые два правила действуют на основе каждой проводки, а общий супертип — правило проводки каждой проводки — управляет общим поведением. Разделение платежей на дневные и ночные осуществляется с помощью простого подтипа синглтон каждого правила проводки. Для дневных и ночных звонков требуются разные тарифы, но поскольку основной процесс одинаков, мы можем использовать объект стратегии, параметризованный таблицей тарифов. Это позволит нам работать с любым правилом проводки, которое взимает плату в соответствии с некоторым тарифом, основанным на длительности звонка. Таким образом класс таблицы тарифов, используемый в качестве объекта стратегии, может быть использован для любых расчетов, основанных на продолжительности звонка. Действительно, он используется для следующего правила проводки, которое рассчитывает налог. В отличие от предыдущих правил, это правило должно работать по месяцам, но мы не можем считать, что оно выполняется один раз в месяц.

Три класса правил проводки должны дать представление о том, как можно использовать шаблоны счета/инвентаря для отображения как денежных, так и неденежных операций.

При разработке кода мне нравится начинать с построения скелета структурной модели. Затем я создаю прототип, следя за тем, чтобы обновлять структурную модель по ходу работы (иначе я могу забыть, где нахожусь). При возникновении сложных моделей поведения я могу использовать диаграммы событий или диаграммы взаимодействия в начале или в процессе программирования. Если я считаю важным документировать то, что я сделал с этими поведениями (как я делаю для этой книги), я создаю диаграммы после того, как разберусь с кодом. Диаграммы не заменяют код; они помогают проиллюстрировать, что делает код. (Однако, при наличии подходящего инструмента, диаграммы событий можно использовать как код).

7.1 Структурные модели

Лучше всего начать со структурных моделей, поскольку они дают представление о различных частях окончательной модели. На рисунке 7.1 показаны пакеты внутри модели. Я разделил модель на два пакета: телефонные услуги и счета. Одно из достоинств системы бухгалтерского учета заключается в том, что она может использоваться для различных отраслей, поэтому нам нужно убедиться, что модель бухгалтерского учета отделена от любых отраслевых концепций (то есть не имеет связи с ними).

7 1

Пакет Счета содержит абстрактные типы учета, которые расширяются пакетом Телефонные услуги для данного конкретного домена.

Рисунок 7.1. Пакеты для примера TT.

На рисунке 7.2 показана модель учета для TT, основанная на шаблонах из главы 6. В этой модели есть три ассоциации от правила проводки к счету. Триггер и результат знакомы, но «результат с ключом» — это что-то новое. Это позволяет использовать несколько целевых счетов для тех правил проводки, которым они необходимы. Необходимость этого станет понятна на примерах, приведенных далее в главе.

7 2

Рисунок 7.2. Модель счетов для TT.

Если вы теряетесь в нотации некоторых диаграмм, то обратитесь к полному описанию в приложении А или к его сокращенной версии в приложении C

    На рисунке 7.3 показана модель телефонной услуги. Клиентам разрешено использовать несколько телефонных линий. Телефонная услуга — это фактически телефонная линия, закрепленная за клиентом. Каждая телефонная услуга привязана к бухгалтерской практике, которая описывает, как услуга будет рассчитываться. Эта диаграмма иллюстрирует, почему сопоставление субъекта (линия между детальным счетом и объектом) было добавлено к детализированному счету, показанному на рисунке 7.2. Нужен способ узнать, что именно учитывает детальный счет, но мы не хотим, чтобы из пакета счета можно было попасть в пакет телефонных услуг, поскольку это поставит под угрозу повторное использование. Таким образом, мы формируем подтип детализированного счета. При подтипизации видимость идет только от подтипа к супертипу. Вполне допустимо, чтобы счет обслуживания знал телефонную услугу, потому что они оба находятся в пакете телефонной услуги. Однако мы можем ссылаться на детализированного счет и не знать, что он является счетом услуги. Абстрактное сопоставление детализированного счета говорит нам, что он может быть связан с объектом (тип не определен) в качестве субъекта. Все это будет реализовано подтипами детализированного счета — классический случай полиморфизма.

    7 3

    Рисунок 7.3. Структурная модель телефонной услуги.

    Если вы теряетесь в нотации некоторых диаграмм, то обратитесь к полному описанию в приложении А или к его сокращенной версии в приложении C

      7.2 Реализация структуры

      Для реализации моделей мы можем использовать шаблоны проектирования, основанные на паттернах, описанных в главе 14. Все ассоциации представлены операциями access и modify. Однозначные отображения следуют обычной конвенции Smalltalk. Так, отображение с именем trigger в правиле проводки реализуется триггером accessor и триггером modifier: anAccount. Многозначные отображения — например, правила проводок по бухгалтерской практике — имеют аксессор postingRules и модификаторы addPostingRule: aPostingRule и removePostingRule: aPostingRule.

      Операция записи на счет является полиморфной — детализированный счет возвращает переменную экземпляра, а сводный счет суммирует все свои дочерние элементы (как показано в листинге 7.1).

      Account»entries ^self subclassResponsibility SummaryAccount»entries |answer| answer := SortedCollection sortBlock:[:a :b| a whenBooked > b whenBooked]. self detailAccounts inject: answer into: [:total :each | total addAll: each entries; yourself]. ^answer Detail Account»entries ^entries copy

      В этой модели нет типов счетов. Правила проводок определяются сводными счетами. Для примеров в этой главе мы можем использовать как типы счетов, так и сводные счета для определения правил проводок. Использование сводных счетов немного сложнее, что делает его лучшей иллюстрацией. Определенные сводные счета высокого уровня хранятся в переменной в классе счета и доступны с помощью метода класса findWithName: aString, в соответствии со стилем раздела 14.5.1.

      Различные фрагменты кода должны найти счет обслуживания для конкретной телефонной услуги под конкретным сводным счетом. Нетрудно придумать различные способы сделать это: попросить телефонную услугу найти счет по заданному сводному счету или попросить сводный счет найти его потомка, привязанного к телефонной услуге. Оба эти способа разумны, но трудно выбрать, какой из них лучше. Кроме того, каждый из них подразумевает определенный путь поиска, и один из них может быть лучше другого. В таких случаях мы можем использовать совершенно другую технику — создать метод класса, следуя разделу 14.5.1. Тогда мы сможем реализовать метод с любым из путей и изменить его без изменения декларативного интерфейса. Это также облегчает запоминание того, где находятся эти методы поиска, как показано в листинге 7.2.

      ServiceAccount class»findWithPhoneService: aPhoneService topParent: aTopSummaryAccount ^aPhoneService serviceAccounts detect: [:i| i parentTop = aTopSummaryAccount] PhoneService»accountNamed: aString ^ServiceAccount findWithPhoneService: self topParent: (Account findWithName: aString)

      На практике часто удобнее использовать метод в телефонной услуге, например accountNamed: aString. Этот метод вызывает findWithPhoneService: topParent и обеспечивает преимущества обоих подходов.

      Во всех приведенных здесь примерах используются простые транзакции (с только двумя проводками), хотя модель поддерживает сложные транзакции. Мы можем создать простую транзакцию с помощью специальных методов создания транзакции, показанных в листинге 7.3. Один метод несет всю информацию, включая исходные проводки и правило создания проводок. Другой метод используется для начальных атрибутов, считываемых в самом начале.

      Transaction class»newWithAmount: aQuantity from: fromAccount to: toAccount whenCharged: aTimepointOrDate ^self newWithAmount: aQuantity from: fromAccount to: toAccount whenCharged: aTimepointOrDate creator: nil sources: Set new newWithAmount: aQuantity from: fromAccount to: toAccount whenCharged: aTimepointOrDate creator: aPostingRule sources: aSetOfEntries ^self new setAmount: aQuantity from: fromAccount to: toAccount whenCharged: aTimepointOrDate creator: aPostingRule sources: aSetOfEntries Transaction»setAmount: aMoney from: aDebitAccount to: aCreditAccount whenCharged: aTimepointOrDate creator: aPostingRule sources: aSetOfEntries "private" self require: [aMoney isKindOf: Money. aDebitAccount isKindOf: ServiceAccount. aCreditAccount isKindOf: ServiceAccount. (aTimepointOrDate isKindOf: Date) or: [aTimepointOrDate isKindOf: Timepoint]. (creator == nil) or: [creator isKindOf: PostingRule]]. self initialize. self addEntry: (Entry new setAccount: aCreditAccount amount: aMoney charged: aTimepointOrDate). self addEntry: (Entry new setAccount: aDebitAccount amount: aMoney negated charged: aTimepointOrDate). creator:= aPostingRule. aSetOfEntries do: [:i| self sourcesAdd: i]. self checklnvariant. Object»require: aBooleanBlock aBooleanBlock value ifFalse: [self error: 'Precondition Violation'] Transaction»checklnvariant |balance| balance := entries inject: Quantity zero into: [:total :each | total := total + each amount]. self require: [balance = Quantity zero].

      В листинге показан ряд приемов кодирования. Параметрический метод конструктора [1] (с префиксом set) инициализирует новый объект параметрами. Внутри параметрического метода создания проверка предварительного условия выполняется с помощью сообщения require:. Для повышения производительности проверку можно убрать, переопределив метод require:. Еще одним элементом проектирования по контракту [3] является использование проверки инвариантов.

      7.3 Настройка новой телефонной услуги

      Создание новой телефонной услуги — это не просто инстанцирование объекта телефонной услуги. Необходимо также создать учетные записи услуги, чтобы запустить систему учета. Хотя этот пример не содержит более одной бухгалтерской практики, он должен быть достаточно гибким, чтобы настроить счета для любой используемой бухгалтерской практики, как показано на рисунках 7.4 и 7.5, а также в листинге 7.4.

      7 4

      В этой диаграмме используется условие управления между продуктами (расширение обычных диаграмм событий). Управляющее условие оценивается для каждой комбинации входящих в него триггеров, в данном случае для каждой комбинации новой телефонной услуги и счета проводки. Оно вызывает операцию создания счета обслуживания для каждой телефонной услуги и сводного счета проводки в бухгалтерской практике.

      Рисунок 7.4. Диаграмма событий при создании новой телефонной услуги.

      7 5

      Рисунок 7.5. Диаграмма взаимодействия при создании новой телефонной услуги.

      PhoneService class»newWithAccountingPractice: anAccountingPractice customer: aCustomer phoneLine: aPhoneLine ^self new setAccountingPractice: anAccountingPractice customer: aCustomer phoneLine: aPhoneLine PhoneService»setAccountingPractice: anAccountingPractice customer: aCustomer phoneLine: aString |newObj summaryAccounts| self require: [(anAccountingPractice isKindOf: AccountingPractice) & (aCustomer isKindOf: Customer)]. name := (aCustomer name), '#', (aCustomer phoneServices size + 1) printString. accountingPractice := anAccountingPractice. self setCustomer: aCustomer. line := aString. self createServiceAccounts. ^self PhoneService»createServiceAccounts "private — initializing" (self accountingPractice summaryAccounts) do: [:each | ServiceAccount newWithPhoneService: self parent: each].

      Чтобы определить, какие счета требуются, у бухгалтерской практики запрашиваются ее счета проводок, как показано на рисунке 7.6 и в листинге 7.5. Практика бухгалтерского учета может содержать правила проводок, которые ссылаются на детализированные счета (хотя в данном случае это не делается). Таким образом, счета проводок должны быть отфильтрованы, чтобы сохранить только сводные счета.

      7 6

      Нам нужен счет запуска для каждого правила проводки и все итоговые счета для каждого правила проводки.

      Рисунок 7.6 Поиск счетов проводки.

      AccountingPractice»summaryAccounts ^self postingAccounts select: [:each | each isSummary] AccountingPractice»postingAccounts |answer| answer := Set new. postingRules do: [:each | answer add: each trigger. answer addAll: each outputs]. ^answer

      7.4 Настройка звонков

      Телефонные звонки моделируются как транзакции со счета сети на счет основного времени. Единицами измерения для записей телефонных звонков являются минуты. Следующий метод показывает настройку телефонной услуги и размещение некоторых примеров звонков. Обратите внимание, что он создан на основе класса Scenario1, как показано в листинге 7.6. Тестовые методы могут стать довольно сложными; поэтому хорошей практикой является размещение их на объекте сценария (используя сценарий в смысле сценария использования, а не как определено в разделе 9.4), чтобы держать их под контролем, если нет надлежащей структуры тестирования. Переменные basicAccount и theService являются переменными класса этого тестового класса.

      Scenario1»setupCalls |adams network | self init. adams := Customer new name: 'Adams'; persist. theService := PhoneService newWithAccountingPractice: (AccountingPractice basicBillingPlan) customer: adams phoneLine: (PhoneLine new name: '617 123 1234'). network := theService accountNamed: 'Network'. basicAccount := ServiceAccount findWithPhoneService: theService topParent: (Account findWithName: 'Basic Time'). Transaction newWithAmount: (Quantity n:'10 min') from: network to: basicAccount whenCharged: (Timepoint date: 'jan 1 1995' time: '13:15'). Transaction newWithAmount: (Quantity n: '8 min') from: network to: basicAccount whenCharged: (Timepoint date: 'jan 1 1995' time: '14:25'). Transaction newWithAmount: (Quantity n:'6 min') from: network to: basicAccount whenCharged: (Timepoint date: 'jan 1 1995' time: '19:05'). Transaction newWithAmount: (Quantity n : '33 min') from: network to: basicAccount whenCharged: (Timepoint date: 'jan 1 1995' time: '20:20'). ^basicAccount

      Использование определяемых пользователем фундаментальных классов, таких как quantity, может затруднить создание новых объектов. Поэтому у quantity есть метод n: aString, который создает новое количество из строки. Это моя личная конвенция, которую я использую, поскольку fromString: aString может получиться довольно громоздким.

      7.5 Реализация запуска правила на основе счета

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

      Detail Account»process self allOutboundRules do: [:j| j processAccount: self]. lastProcessed := entries last allOutboundRules "private" |answer| answer := self triggerFor. self allParents do: [:i | answer addAll: i triggerFor]. ^answer

      Записи хранятся в упорядоченной коллекции OrderCollection, а новые добавляются в конец. Переменная экземпляра lastProcessed отслеживает состояние обработки.

      7.6 Разделение звонков на дневные и ночные

      Чтобы разделить звонки на дневные и ночные, мы смотрим на каждую проводку, учитываем время на ней, а затем создаем транзакцию со счета основного времени на счет дневного или ночного времени.

      Правило проводки, действующее по каждому входу, встречается довольно часто. Мы можем создать абстрактный подтип правила проводки, называемый правилом проводки по каждому входу (класс EachEntryPR). Этот подтип вызывает процесс операции Entry: anEntry для каждой необработанной записи в триггерном счете, как показано на рисунке 7.7 и в листинге 7.8.

      7 7

      Мы используем операцию обработки записи для каждой необработанной проводки.

      Рисунок 7.7 Метод обработки счета в правиле проводки по каждой проводке.

      Сообщение currentInput: загружает переменную экземпляра для хранения учетной записи сервиса, которая обрабатывается правилом проводки, как показано в листинге 7.9. Доступ к ней осуществляется с помощью приватных методов и определяется только в рамках выполнения processAccount. Временная, приватная переменная экземпляра часто используется в таких случаях, потому что правила проводки в общем случае (хотя и не в этом) определяются как экземпляры. Таким образом, мы не можем инстанцировать их для вызова правила. Альтернативный вариант — рассматривать определенный экземпляр правила проводки как шаблон "прототип" и клонировать его для выполнения.

      EachEntryPR>>processAccount: anAccount self currentlnput: anAccount. anAccount unprocessedEntries do: [:each | self processEntry: each]. self clean. EachEntryPR>>processEntry: anEntry self subclassResponsibility Detail Account»unprocessed Entries self isUnprocessed ifTrue: [^ entries copy]. ^entries copyFrom: self firstUnprocessedlndex to: entries size. Detail Account»isUnprocessed "private" ^lastProcessed isNil Detail Account>>firstUnprocessedIndex "private" ^(entries indexOf: lastProcessed) + 1
      PostingRule>>currentlnput: anAccount "private" self require: [currentlnput isNil]. currentlnput := anAccount. self setCurrentOutputs, PostingRul e»setCurrentOutputs "private" currentOutputs := Dictionary new. outputs associationsDo: [: each | currentOutputs at: each key put:(ServiceAccount findWithPhoneService: (currentlnput phoneService) topParent: each value)] PostingRul e»clean "private" currentlnput := nil. currentOutputs := nil.

      Сообщение currentInput: также устанавливает текущие выходные счета услуг для той же телефонной услуги, которая была предоставлена в качестве входной.

      Этот метод не выполняет фактического расчета и публикации. Вместо него это делает processEntry:, который является абстрактным и должен быть определен подклассами. Таким образом, мы видим здесь три уровня подклассификации. PostingRule определяет базовый интерфейс и сервисы правил проводки. Метод обработки счета на EachEntryPR — это шаблонный метод, который описывает шаги обработки счета по каждой записи, но оставляет подклассу возможность самому решить, как обрабатывать каждую проводку. Для этого правила проводки мы можем определить новый подкласс EachEntryPR под названием EveningDaySplitPR. Это пример реализации класса-синглтона (см. раздел 6.6.1). В этот класс жестко закодированы соответствующие счета, которые устанавливаются при инициализации, как показано в листинге 7.10.

      EveningDaySplitPR>>initialize super initialize. outputs := Dictionary new. outputs at: #evening put: (Account findWithName: 'Evening Time'). outputs at: #day put: (Account findWithName: 'Day Time')

      Разделение выполняется переопределенным методом processEntry:, как показано на рисунке 7.8 и в листинге 7.11.

      7 8

      Рисунок 7.8. Метод правила разделения процесса «день/ночь» для операции входа в процесс.

      7.7 Расчет оплаты времени звонка

      Тарификация ночных и дневных звонков происходит по одной и той же схеме, как показано на рисунке 7.9. Снова оплата рассчитывается для каждого входа, поэтому используется подкласс EachEntryPR. Используются два правила проводки — одно для дня, другое для ночи. Для обоих используется один и тот же класс TransformPR.

      7 9

      Рисунок 7.9. Диаграмма взаимодействия при обработке счета с правилом трансформации проводок.

      EveningDaySplitPR>>processEntry: anEntry Transaction newWithAmount: (anEntry amount) from: (anEntry account) to: (self outputFor: anEntry) whenCharged: (anEntry timepoint) creator: self sources: (Set with : anEntry) EveningDaySplitPR>>outputFor: anEntry ^(anEntry timepoint time > (Time fromString: '19:00')) | (anEntry timepoint time < (Time fromString: '07:00')) ifTrue: [self currentOutputs at: #evening ] ifFalse: [self currentOutputs at: #day].

      Особенность этого правила проводки в том, что оно запускается проводкой в минутах, но создает проводки в долларах, отсюда и термин «трансформация». Его фактическая реакция заключается в создании двух транзакций. Одна переводит минуты обратно на сетевой счет, завершая тем самым цикл для минут. Вторая генерирует новую транзакцию в денежном мире: со счета доходов сети на счет телефонной активности, как показано на рисунке 7.10 и в листинге 7.12.

      7 10

      Рисунок 7.10. Диаграмма событий для метода правила преобразования проводки для операции входа в процесс.

      TransformPR»processEntry: anEntry Transaction newWithAmount: (anEntry amount) creator: self from: (anEntry account) timepoint: (anEntry timepoint) to: (self currentOutputs at: #out) sources: (Set with : anEntry). Transaction newWithAmount: (self transformedAmount: anEntry) creator: self from: (self currentOutputs at: #transformedFrom) timepoint: (anEntry timepoint) to: (self currentOutputs at: #transformedTo) sources: (Set with: anEntry). TransformPR»transformedAmount: anEntry "private" ^self calculationMethod calculateFor: anEntry amount

      Вычисление transformedAmount проходит с использованием шаблона «метода объекта» (см. раздел 6.6.2), а именно таблицей тарифов, как показано в таблицах 7.1 и 7.2. «Метода класса» определяет абстрактный метод calculateFor:. Таблица тарифов — это подкласс, хранящий таблицу количеств в двух столбцах для получения градуированной тарификации, которую требует задача. Она реализована с помощью словаря. Ключи словаря указывают на различные пороговые точки, а соответствующее значение указывает на тариф, который применяется до этого порога. В листинге 7.13 показано, как настраивается ночной тариф.

      Длина звонка

      Тариф

      <= 1 минуты

      98¢

      > 1 минуты

      30¢

      Таблица 7.1. Тарифы для дневных звонков.

      Длина звонка

      Тариф

      <= 1 минуты

      70¢

      1-20 минут

      20¢

      > 20 минут

      10¢

      Таблица 7.2. Тарифы для ночных звонков.

      RateTable»eveningRateTable | answer | answer := RateTable new. answer rateAt: (Quantity n: '1 min') put: (Quantity n: '.7 USD'). answer rateAt: (Quantity n: '21 min') put: (Quantity n: '.2 USD'), answer topRate: (Quantity n: '.12 USD'), ^answer

      В листинге 7.14 показано, как таблица тарифов рассчитывает сумму. Это делается в два этапа: берется каждый шаг в таблице тарифов и добавляется любая сумма, превышающая верхний порог. Нет особого смысла показывать диаграмму для этого; таблицы достаточно ясно показывают, что нужно с концептуальной точки зрения. Одна особенность этих систем заключается в том, что они могут одинаково обрабатывать как положительные, так и отрицательные числа, используя абсолютные значения.

      RateTable»calculateFor: aQuantity | answer input | self require: [aQuantity unit = self thresholdUnits]. input := aQuantity abs. answer := (self tableAmount: input) + (self topRateAmount: input). ^aQuantity positive ifTrue: [answer] ifFalse: [answer negated] RateTable»tableAmount: aQuantity "private" |input sortedKeys TastKey thisRowKeyAmount answer| sortedKeys := table keys asSortedCollection. lastKey := Quantity zero, answer := Quantity zero. sortedKeys do: [ithisKey | thisRowKeyAmount := ((aQuantity min: thisKey) — lastKey) max: Quantity zero, answer := answer + ((table at: thisKey) * thisRowKeyAmount amount). lastKey := thisKey]. ^answer RateTable>>topRateAmount: aQuantity | amountOverTopRateThreshold | amountOverTopRateThreshold := aQuantity — self topRateThreshold. amountOverTopRateThreshold positive ifTrue: [Aself topRate * amountOverTopRateThreshold amount] ifFalse: [AQuantity zero]. RateTable>>topRateThreshold ^table keys asSortedCollection last

      7.8 Расчет налога

      В последнем правиле проводок показан расчет налога. Это правило отличается от предыдущих тем, что оно не работает на основе каждой записи. В этом правиле проводок для расчета налога необходимо рассмотреть все начисления за месячный период.

      Еще одна сложность заключается в том, что мы не можем (или, скорее, не хотим) гарантировать, что правило проводки будет выполняться только один раз в конце месяца. Таким образом, правило должно учитывать любой уже начисленный налог в течение месяца, который получился в предыдущих расчетах за текущий месяц. Это соответствует принципу, согласно которому правила проводок должны быть определены независимо от того, как они запускаются. Это повышает гибкость и уменьшает связанность модели.

      Класс MonthlyChargePR является подтипом правила проводки и поэтому реализует processAccount, как показано на рисунке 7.11 и в листинге 7.15.

      7 11

      Этот процесс основан на балансе за определенный период времени, а не на каждой проводке.

      Рисунок 7.11. Метод обработки счета по правилу проводки ежемесячных начислений.

      Каждый месяц обрабатывается с помощью processForMonth:, как показано на рисунке 7.12 и в листинге 7.16. Последняя операция выполняется с выходного счета на входной счет, поскольку счет деятельности будет увеличен из-за налогового обязательства.

      7 12

      Рисунок 7.12 Диаграмма событий для обработки месяца.

      MonthlyChargePR»processAccount: anAccount self currentlnput: anAccount. (self monthsToProcess: anAccount) do: [:each | self processforMonth: each]. self clean MonthlyChargePR»monthsToProcess: anAccount ^(anAccount unprocessedEntries collect: [:each | each whenCharged date firstDayOfMonth]) asSet.
      MonthlyChargePR»processforMonth: aDate | inputToProcess totalToCharge | inputToProcess := (self inputBalance: aDate) — (self outputAlreadyCharged: aDate). totalToCharge := (self calculationMethod calculateFor: inputToProcess) - (self outputAlreadyCharged: aDate). Transaction newWithAmount: totalToCharge creator: self from: self currentOutput timepoint: aDate lastDayOfMonth to: self currentlnput sources: (self currentlnput entriesChargedlnMonth: aDate). MonthlyChargePR>>inputBalance: aDate ^self currentlnput balanceChargedlnMonth: aDate. MonthlyChargePR>>outputAlreadyCharged: aDate ^(self currentOutput balanceChargedlnMonth: aDate) negated

      7.9 Заключительные соображения

      Это очень простой пример, поэтому на его основе трудно сделать слишком много выводов. Читатель может убедительно возразить, что эта проблема может быть решена в гораздо более простой форме без всех этих фреймворков. Однако фреймворк ценен с точки зрения масштабируемости. В реальном бизнесе могут быть десятки практик, каждая из которых имеет десятки правил процесса. С помощью этой структуры мы представляем новый биллинговый план бухгалтерской практики. Когда мы создаем новую практику, мы создаем сеть новых экземпляров правила проводки. Мы можем сделать это без перекомпиляции или перестройки системы, пока она еще работает. Будут неизбежные случаи, когда нам понадобится новый подтип правила проводки, но они будут редкими.

      7.9.1 Структура правил размещения

      На рисунке 7.13 показана структура обобщения правил проводки, рассмотренных в этой главе. Абстрактный класс правила проводки имеет абстрактный метод processAccount. Каждый из подтипов реализует processAccount. Правило проводки каждой проводки реализует этот метод, вызывая другой абстрактный метод, processEntry, для каждой проводки. Другие подтипы реализуют processEntry по мере необходимости. Метод правила проводки с разделением на день и ночь жестко закодирован, а правило проводки с преобразованием делегирует полномочия таблице тарифов. Этот пример показывает, как сочетание абстрактных методов, полиморфизма и делегирования может обеспечить такую структуру, которая поддерживает разнообразные правила проводки в организованной структуре.

      7 13

      Рисунок 7.13. Структура обобщения правил размещения.

      # — обозначает абстрактный метод
      + — обозначает публичный метод
      - — обозначает приватный метод

        Это не единственная структура правил проводки, которую мы могли бы использовать. Другой альтернативой может быть объединение двух этапов расчета оплаты в один. Такое правило проводки будет иметь две таблицы тарифов: дневную и ночную, и будет отвечать как за определение тарифного времени, так и за начисление по таблице тарифов.

        Нет никаких четких правил для решения проблемы разделения правила проводок. Наша главная цель — иметь возможность создавать новые практики, не нуждаясь в новых подтипах правил проводок. Мы хотим иметь как можно меньший набор подтипов правил проводок, поскольку это облегчит понимание и поддержку типов правил проводок. Тем не менее нам нужно, чтобы эти подтипы обладали всеми необходимыми функциями, чтобы мы могли объединять их для новых практик. Мы хотим свести к минимуму время, когда нам нужно будет создавать новые подтипы правил проводок.

        Более простые правила проводок приводят к более раздутым практикам и обычно более доступны. Изначально я склонен ограничивать правила проводок небольшими функционалом\поведением. Если я вижу часто используемую комбинацию правил проводки, я могу создать более функциональное правило, которое представит нужное поведение.

        7.9.2 Когда не стоит использовать фреймворк

        Альтернативой использованию этой схемы является наличие только одного класса для каждого биллингового плана для обработки всех действий (разделение на день/ночь, оплаты и начисление налогов), как показано на рисунке 7.14. Этот класс берет все проводки за месяц и создает чек на оплату. Для каждого биллингового плана будет создан один такой объект.

        7 14

        Биллинговый план прост, но не так гибок.

        Рисунок 7.14. Использование биллингового плана.

        Такой подход вполне правдоподобен. Хотя планов выставления счетов может быть много, обычно существует несколько основных методов выставления счетов, которые можно параметризовать, подобно тому как правило трансформации проводок может быть параметризовано для множества различных тарифных планов. Для данной задачи такой тип будет параметризован таблицей ночных тарифов, таблицей дневных тарифов и таблицей налоговых тарифов.

        Ключевой вопрос заключается в том, сколько существует подтипов биллингового плана. Если мы можем представить все биллинговые структуры с помощью дюжины или около того подтипов (хотя экземпляров могут быть сотни — это же пользователи), то использование типа биллингового плана вполне оправдано. Сильной стороной модели учета является то, что она позволяет создавать эквивалент новых подтипов биллингового плана путем объединения объектов правила проводки и счета. Это сильное преимущество, если существует большой или часто меняющийся набор подтипов биллингового плана.

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

        7.9.3 Схемы бухгалтерской практики

        Диаграмма часто помогает наглядно представить сложную проблему. Рисунки 7.15 и 7.16 — это предположения (и, конечно, очень ранние и общие) в этом направлении. Сложным практикам помогут именно такие диаграммы. Можно представить, что, нарисовав диаграмму, мы можем построить систему, уменьшив объем необходимого программирования и тем самым повысив производительность таких приложений.

        7 15

        Диаграмма показывает триггер и основной целевой счет для каждого правила проводки, но скрывает полный поток транзакций.

        Рисунок 7.15. Простой способ диаграммы расположения правил процесса и счетов.

        7 16

        Эта диаграмма делает поток транзакций явным. Каждое правило проводки запускается одним счетом и вызывает несколько потоков. Направление потоков показывает, куда выводятся и куда депонируются элементы. Эта диаграмма отображает больше информации и поэтому является более сложной.

        Рисунок 7.16. Более выразительная диаграмма счетов и правил проводок.

        Практика должна привести к тому, что форма диаграммы будет простой, но при этом передающей ключевую информацию. Преимущество рисунка 7.15 заключается в том, что он прост и отображает ключевые взаимосвязи запуска и результата. Однако он не показывает полный поток учитываемых элементов так, как это делает рисунок 7.16. Если вы используете эти шаблоны, я настоятельно рекомендую использовать диаграммы. Начните с предложенных здесь и позвольте своим диаграмм развиться до того стандарта или вида, который будет наиболее полезным (и дайте мне знать, что у вас вышло).

        Ссылки

        1. Beck, K. Smalltalk Best Practice Patterns. Volume 1: Coding. Englewood Cliffs, NJ: Prentice Hall, in press.

        2. Gamma, E., R. Helm, R. Johnson, and J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley, 1995.

        3. Meyer, B. “Applying ‘Design by Contract,’” IEEE Computer, 25,10 (1992), pp. 40–51.

        Last modified: 16 January 2025