Нумератор документов
- Автор(ы): Чобиток Василий, 02.02.2010
Во многих системах документоборота, реестрах и т. п. используются различные схемы нумерации документов. Часто схема нумерации зависит от субъективного понимания удобства присвоения и использования номера конкретными исполнителями организации, которые организовывают её документооборот.
При реализации программного обеспечения, отвечающего за автоматизацию документооборота или его отдельных частей, разработчики часто сталкиваются с этой проблемой. Как правило, к сожалению, разработчики идут «простым» путём и жёстко реализовывают в программе схему присвоения номера документа. В последующем, при изменении внутренних инструкций документооборота предприятия и/или порядка нумерации документов, программа перестаёт отвечать реалиям жизни и требует переработки.
Не раз наблюдал попытки сделать «универсальный нумератор», который у разработчиков получался сложным с точки зрения реализации и часто очень трудным в использовании и применении.
Как-то раз вместо техзадания на такой «нумератор», на которое понадобилось бы порядка недели, я за пару дней набросал в Delphi 7 работающий прототип такого нумератора.
Недавно поднял исходники прототипа нумератора и за несколько вечеров в Delphi 2009 довёл их до готовности к использованию в любом проекте, в котором необходимо присваивать каким-либо сущностям номера отличные от обычных чисел. Как оказалось, сделать «универсальный» нумератор не так уж и сложно.
Содержание
Анализ проблемы
Номера документов могут иметь различный формат и зависеть от разных внешних условий. Например, частью номера документа может быть код номенклатуры, счетчик порядкового номера, код вида (типа) документа и другие значения, которые зависят от вида текущего документа. Часто в номерах документов используется текущая дата или любые её части (год, месяц, день). Составные части номера могут разделяться различным символами и словами. И так далее.
Например, номер «03-123/2010» может означать 123-й по счету документ номенклатуры с кодом 03 в 2010 году. Этот же номер с сокращенной записью года может иметь вид: «03-123/10».
Встречаются случаи, когда счетчик работает в рамках других периодов, например в течении дня. Такой номер «3-2010/01/31» может означать третий документ за 31 января 2010 года.
Отсюда можно сделать выводы:
- заранее (при разработке программы) формат номера документа знать невозможно, должна быть возможность его настроить;
- в номере могут использоваться различные составные части:
- текстовые значения (константы, задаваемые при настройке);
- даты и их составные части;
- различные счетчики (триггеры), которых в основной программе может быть несколько;
- значения атрибутов текущего документа.
Решение (проектирование)
Принятое решение предполагает возможность повторного использования программного кода нумератора в различных проектах.
С учетом того, что нумератор делается универсальным, то определённо можно сказать, что в момент его разработки неизвестно о том, какие счётчики используются в той системе, к которой его будут подключать, какие атрибуты имеют те виды документов, которым присваивается номер. Поэтому принято решение чётко разделить обязанности между нумератором и системой, к которой он будет подключен.
Нумератор отвечает за:
- возможность настройки формата номера;
- генерацию нового номера, при этом значения счётчиков (триггеров) и полей текущего документа запрашиваются им у внешней системы;
- автоматическую генерацию значений частей номера, не связанных с предметной областью (текстовые значения, даты и их составные части).
Внешняя система отвечает за:
- хранение настроенных форматов номеров в их привязке к тем сущностям (видам документов), для которых он настраивался;
- передачу нумератору списков используемых в системе счетчиков и используемых для текущего вида документа полей;
- передачу нумератору следующего за текущим значения поля или счетчика, который у системы запрошен нумератором.
Поскольку нумератор (класс TCounter
, см. диаграмму ниже) заранее не знает о внешней системе, к которой он будет подключен, то вместе с ним в одном модуле определён интерфейс ICounter
, определяющий методы взаимодействия с внешней системой. Кроме того, в этом же модуле определен абстрактный класс TCounterPart, инкапсулирующий от TCounter
поведение различных типов составных частей номера, и наследники TCounterPart
, реализующие поведение конкретных типов частей номера.
При подключении нумератора к внешней системе ExternalSystem
задача разработчика сводится к тому, чтобы реализовать методы интерфейса ICounter
, подключить в интерфейсе программы вызов диалога TdlgOptions
настройки нумератора и организовать вызов нумератора в момент присвоения номера документу или другому объекту.
Подробнее см. на диаграмме классов нумератора.
Для хранения и восстановление настроек нумератора принят следующий формат строки-шаблона для каждой части номера:
{<ТипКлассаЧастиНомера>:<Значение>}
Здесь <ТипКлассаЧастиНомера>
может принимать значения имен классов наследников класса TCounterPart
, а <Значение>
— текущее значение настройки части номера. Например, в номере «Вх-12/09» первая часть номера «Вх-» является текстовой, поэтому её шаблон будет иметь вид:
{TCounterPartText:Вх-}
Вторая часть этого примера является порядковым номером, т.е. получается вызовом системного счетчика. Например, этот счетчик в системе носит имя «tgrInputNo», тогда шаблон настройки будет следующим:
{TCounterPartTrigger:tgrInputNo}
Здесь можно заметить, что для текстового поля в шаблоне хранится его непосредственное значение, которое будет вставлено в номер при генерации, а для счетчика хранится имя счётчика, так как для счётчика важно знать не его значение в момент настройки, а то какой именно это счётчик, чтобы по его имени в момент генерации запросить значение у внешней системы. То же самое относится и к системным полям.
Таким образом, полная строка настройки для примера «Вх-12/09» должна иметь примерно такой вид («09» — текущий год в формате «ГГ»):
{TCounterPartText:Вх-}{TCounterPartTrigger:tgrInputNo}{TCounterPartText:/}{TCounterPartDate:ГГ}
Более того, значение «Вх-» определяет, что при классификации документа по направлению, он является входящим. Соответственно, для исходящих при такой настройке следовало бы осуществить отдельную настройку нумератора со значением «Исх-». Так как внешней системе классификация регистрируемого документа по направлению известна, то вместо двух настроек можно сделать одну путём доработки предыдущего примера, в котором часть {TCounterPartText:Вх-}
будет заменена такой настройкой:
{TCounterPartField:directType}{TCounterPartText:-}
Где, directType — имя атрибута (свойства) внешней системы, отвечающего за классификацию текущего документа по направлению и возвращающего «Вх» для входящих документов и «Исх» для исходящих.
Использование в настройках имён классов — решение уровня реализации. В принципе можно было бы ввести любые условные наименования типов частей номера, но так оказалось проще всего реализовать, поэтому сразу приводится это решение.
Реализация
Компонентная структура нумератора представлена на диаграмме.
Модуль Counter
Все классы нумератора, кроме формы его настройки, и интерфейс ICounters
определены в модуле counter
. Кроме стандартных модулей дополнительно используется библиотека RegExpr
для работы с регулярными выражениями.
unit counter;
interface
uses
Classes, StdCtrls, ExtCtrls, Controls, SysUtils, RegExpr, Dialogs;
...
Интерфейс ICounters
объявлен следующим образом (назначение методов см. в комментариях):
ICounters = interface(IUnknown)
['{75533674-8A39-4501-97E7-0A8F9BB5625F}']
// Возвращает список имеющихся во внешней системе счётчиков triggerName=Caption
// triggerName - код (имя) счётчика в системе
// Caption - описание счётчика
procedure GetTriggers(aList: TStrings);
// Возвращает следующее значение счётчика
function TriggerNext(TriggerName: string): string;
// Возвращает список системных полей SystemCode=Caption
// SystemCode - код (имя) поля в системе
// Caption - описание поля
procedure GetSystemCodes(aList: TStrings);
// Возвращает текущее значение поля в системе
function SystemCode(CodeName: string): string;
end;
Класс TCounter
интересен тем, что с одним объектом этого класса могут быть ассоциированы несколько объектов класса TCounterPart
, при этом последние должны идти в строго определённом пользователем порядке. Опять же, в любой момент пользователь может добавлять, удалять, менять местами объекты TCounterPart
.
Есть ли смысл реализовывать подобный функционал самостоятельно? Существует стандартный класс TStringList
, в котором кроме списка строк могут задаваться соответствующие им объекты базового класса TObject
. Воспользовавшись этими возможностями, определяем TCounter
наследником класса TStringList
.
TCounter
кроме прочих задач решает задачу создания строки (шаблона) настройки нумератора (метод GetTemplate
) для её хранения внешней системой в привязке к типу сущностей, для которых используется нумератор, а так же возможность восстановления объектной структуры нумератора при передаче ему строки настройки (метод ParceTemplate
).
Класс TCounter
объявлен следующим образом.
TCounter = class(TStringList)
private
FCounterName: string;
FICounters: ICounters;
public
constructor Create(aName: string; I: ICounters; template: string = '');
procedure AddPart(Part: TCounterPart);
procedure Clear; override;
function GetTemplate: string;
procedure ParceTemplate(template: string);
function GetSample: string;
function Generate: string;
property CounterName: string read FCounterName write FCounterName;
property CounterInterface: ICounters read FICounters;
end;
Опишем некоторые методы:
- AddPart — добавление нового объекта класса
TCounterPart
; - CounterName — название нумератора для пользователя;
- GetTemplate — возвращает строку-шаблон текущих настроек нумератора;
- ParceTemplate — по заданной строке-шаблону восстанавливает объектную структуру и настройки нумератора;
- Generate — генерирует и возвращает текущее значение номера;
- GetSample — возвращает пример внешнего вида номера, который может получиться при использовании текущих настроек нумератора. Требуется для взаимодействия с формой настройки, в которой необходимо продемонстрировать пользователю пример внешнего вида номера, но это нельзя делать вызовом метода
Generate
, так как генерация номера может вызвать нежелательное при настройке срабатывание счётчиков.
Абстрактный класс TCounterPart
объявляем наследником от TPersistent
. Такое наследование необходимо для того, чтобы была возможность зарегистрировать эти классы с целью их последующего поиска по текстовому значению имени класса при создании объектов по текстовому шаблону.
TCounterPart = class abstract (TPersistent)
private
FValue: string;
FCounter: TCounter;
function GetValue: string;
procedure SetValue(const aValue: string);
public
function GetControlType: TControlClass; virtual;
function GetName: string; virtual;
function GetTemplate: string;
function GetSample: string; virtual;
function Generate: string; virtual;
procedure InitControlData(aControl: TControl); virtual;
property Value: string read GetValue write SetValue;
end;
Методы и свойства имеют следующее назначение:
- GetControlType — фабричный метод, возвращает тип элемента управления («контрола»), отвечающего за настройку объекта текущего класса. Например, для текстовой части номера класса
TCounterPartText
элементом управления будет объект ввода классаTEdit
. - GetName — возвращает название (описание) элемента номера, отображаемое в интерфейсе программы пользователю, например, в форме настройки.
- GetTemplate — возвращает строку-шаблон настройки текущего объекта части номера;
- GetSample — возвращает пример, как может выглядеть текущая часть номера при генерации номера;
- Generate — вызывает генерацию и возвращает значение части номера;
- InitControlData — подготавливает к использованию в форме настройки (заполняет исходными данными) элемент управления, тип которого возвращает метод
GetControlType
; - Value — возвращает текущее строковое значение настройки части номера.
Следует акцентировать внимание на отличии метода Generate
от свойства Value
. Так, например, для текстовой части номера (класс TCounterPartText
) с настройкой «Вх-», хранимой в переменной FValue, они оба будут возвращать значение «Вх-». Для класса TCounterPartDate
свойство Value
будет возвращать настройку формата выдачи текущей даты, например «ГГГГ», хранимое в переменной FValue, а метод Generate
— значение даты, соответствующее этому формату, в данном случае — «2010».
Далее приведены объявления остальных классов этого модуля.
TCounterPartText = class(TCounterPart)
public
function GetName: string; override;
function GetControlType: TControlClass; override;
end;
TCounterPartDate = class(TCounterPart)
public
function GetName: string; override;
function GetControlType: TControlClass; override;
function Generate: string; override;
procedure InitControlData(aControl: TControl); override;
end;
// Переопределяем TComboBox для обеспечения его взаимодействия
// с классами TCounterPartTrigger и TCounterPartField.
// Строки в списке полей и счетчиков имеют вид «Name=Caption»,
// а пользователю следует показывать только «Caption»
TFieldsComboBox = class(TComboBox)
private
FFieldItems: TStrings;
procedure SetFieldItems(const Value: TStrings);
function GetFieldName: string;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
property FieldItems: TStrings read FFieldItems write SetFieldItems;
property FieldName: string read GetFieldName;
end;
TCounterPartTrigger = class(TCounterPart)
public
function GetName: string; override;
function GetControlType: TControlClass; override;
function Generate: string; override;
procedure InitControlData(aControl: TControl); override;
function GetSample: string; override;
end;
TCounterPartField = class(TCounterPart)
public
function GetName: string; override;
function GetControlType: TControlClass; override;
function Generate: string; override;
procedure InitControlData(aControl: TControl); override;
end;
const
PartsClassArray : array [0..3] of TCounterPartClass = (
TCounterPartText,
TCounterPartDate,
TCounterPartTrigger,
TCounterPartField
);
implementation
{**
* Реализация условно не показана :-)
*}
var
PC: TCounterPartClass;
initialization
for PC in PartsClassArray do
RegisterClass(PC);
end.
Особенности реализации класса TCounter
Приведем пример реализации некоторых методов класса TCounter
.
Метод AddPart
позволяет одним вызовом добавить объект части номера с присвоением элементу списка его названия и сообщить объекту Part
о себе.
procedure TCounter.AddPart(Part: TCounterPart);
begin
AddObject(Part.GetName, Part);
Part.FCounter := Self;
end;
Каждый объект класса TCounterPart
отвечает за генерацию собственной части номера. Поэтому генерировать номер целиком элементарно — вызываем последовательную генерацию каждой части номера и конкатенируем результат.
function TCounter.Generate: string;
var
i: integer;
begin
result := '';
for i := 0 to Count - 1 do
result := result + TCounterPart(Objects[i]).Generate;
end;
Методы GetSample
и GetTemplate
аналогичны.
Метод ParceTemplate
реализован самым простым (как для меня) способом — строка шаблона разбирается с использованием регулярного выражения.
procedure TCounter.ParceTemplate(template: string);
var
re: TRegExpr;
part: TPersistent;
begin
if trim(template) = '' then exit;
re := TRegExpr.Create;
try
Clear;
re.Expression := '\{([^:]+):([^}]*)\}';
if re.Exec(template) then
repeat
part := FindClass(re.Match[1]).Create;
TCounterPart(part).Value := re.Match[2];
AddPart(TCounterPart(part));
until (not re.ExecNext);
finally
re.Free;
end;
end;
Модуль Options (форма настройки)
Задача формы настройки (класс TdlgOptions
) состоит в том, чтобы предоставить пользователю экранный интерфейс настройки номера.
При создании формы её конструктору передаётся объект счётчика: constructor Create(AOwner: TComponent; ACounter: TCounter)
, ссылка на который хранится в переменной FExtCounter
класса TdlgOptions
.
В процессе настройки используется внутреннее свойство этого класса property Counter: TCounter
. При подтверждении пользователем сделанных настроек, состояние объекта Counter
копируется в объект FExtCounter
, в противном случае FExtCounter
остаётся неизменным.
Черновой вариант формы настройки в рабочем состоянии имеет вид, представленный на скриншотах:
Форма настройки позволяет выбрать необходимые части номера, список которых доступен в левой части окна, добавить любое их число в список настройки в правой части, там их расположить в требуемом порядке и каждый в отдельности настроить, для чего используется создаваемый динамически компонент редактирования под списком элементов номера.
Пример использования
Для тестирования и демонстрации возможностей нумератора создан проект, имитирующий работу внешней по отношению к нумератору системы. Его главная форма имеет такой совсем непрезентабельный вид:
В модуле main
класс TForm1
этой формы объявлен следующим образом:
type
TForm1 = class(TForm, ICounters)
{**
* Перечень элементов управления
* и методов обработки событий
* условно не показан
*}
public
// Ниже следуют методы, объявленные в ICounters
procedure GetTriggers(aList: TStrings);
function TriggerNext(TriggerName: string): string;
procedure GetSystemCodes(aList: TStrings);
function SystemCode(CodeName: string): string;
end;
Важным здесь является то, что в этом классе реализуются методы интерфейса ICounters
, объявленного в модуле Counter
.
Поскольку данная программа призвана имитировать работу реальной системы, то реализация этих методов имеет предельно примитивный вид. Например, работа со счетчиками реализована так:
{**
* ВНИМАНИЕ! Этот говнокод имитирует работу внешней системы.
*
* В реальной жизни не повторять!
*}
procedure TForm1.GetTriggers(aList: TStrings);
begin
aList.Add('trigger1=Счетчик 1');
aList.Add('trigger2=Счетчик 2');
aList.Add('trigger3=Счетчик 3');
end;
function TForm1.TriggerNext(TriggerName: string): string;
function NextFromEdit(aEdit: TEdit): string;
begin
aEdit.Text := IntToStr(StrToInt(aEdit.Text) + 1);
result := aEdit.Text;
end;
begin
if TriggerName = 'trigger1' then
result := NextFromEdit(CounterEdit1);
if TriggerName = 'trigger2' then
result := NextFromEdit(CounterEdit2);
if TriggerName = 'trigger3' then
result := NextFromEdit(CounterEdit3);
end;
Скачать пример
- counters.zip — скачать пример программы, использующей нумератор, в архиве.
- Примечание: в демонстрационном примере хранение настроек нумераторов после выхода из программы не реализовано. Прежде чем вызывать генерацию номера командой Генерировать номер следует в списке «Генераторы» выбрать любой нумератор и настроить его командой Настройка.