Немного о проектировании приложений

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

Одним из шаблонов, о котором хочется упомянуть является MVC — это шаблон программирования, который позволяет разделить логику приложения на три части:

  1. Model (модель) - получает данные от контроллера, выполняет необходимые операции и передаёт их в вид.

  2. View (вид или представление) - получает данные от модели и выводит их для пользователя.

  3. Controller (контроллер) - обрабатывает действия пользователя, проверяет полученные данные и передаёт их модели.

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

По реализации MVC подхода в среде Delphi существует отличный цикл статей на habr.com, с которым рекомендуется ознакомиться:

  1. MVC-подход к разработке пользовательских интерфейсов в Delphi. Часть 1. Галочка

  2. MVC-подход к разработке пользовательских интерфейсов в Delphi. Часть 2. Списки

  3. MVC-подход к реализации пользовательского интерфейса в Delphi. Часть 3. Объекты

Цикл статей, представленный выше, хорошо раскрывает основные аспекты темы и детально описывает методы, которые можно реализовать и в среде Lazarus.
Значительно улучшить структуру и функциональность вашего проекта может правильная организация модели данных и в этом должны помочь базовые возможности языка программирования и весь его арсенал по работе с синтаксисом с типами данных.

Ниже приведена информация, дополняющая и расширяющая материалы, разработанные Michalis Kamburelis - Приемы работы в современном Object Pascal, и призванная помочь более качественно организовать работу с моделью данных, разрабатываемого приложения.

Синтаксис и типы данных

Полезные директивы компилятора

{$modeswitch advancedrecords} - разрешает использование записей с методами.

{$modeswitch typehelpers} - разрешает использования хелперов для типов.

{$VarPropSetter+} -

{$codePage UTF-8} - позволяет выводить в консоль кириллические символы.

Подробную информацию о директивах можно получить на странице Директивы компилятора. Руководство программиста Free Pascal (freepascal.ru)

Литералы (Строковые ресурсы)

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

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

В библиотеках Free Pascal есть готовое решение для данной темы - Строки ресурсов. Подробную информацию о работе со строками ресурсов можно получить на странице Строки ресурсов. Руководство программиста Free Pascal.

Перечисляемые типы и наборы

Перечисляемый тип в большинстве случаев используется для того, чтобы перечислить какие-то признаки или определенные варианты значения свойства поля.

 TAnchorKind = (akTop, akLeft, akRight, akBottom);

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

 TAnchors = set of TAnchorKind; 

Наличие выбранного варианта для перечисляемого типа удобно проверять в условии через оператор in

...
myAncor:=akRight;
Options:=[akTop, akLef];
if myAncor in Options then
    Writeln('myAncor in Options - true')
else
    Writeln('myAncor in Options - false');

if myAncor in [akLeft,akRight] then
     Writeln('myAncor in')
  else
     Writeln('myAncor not in');
...

Получить числовое значение для перечисления можно с помощью ORD

i:=ORD(myAncor);

Получить текстовое значение для перечисляемого типа можно с помощью специального оператора из модуля Typinfo

uses
Typinfo;
...
str:=GetEnumName(TypeInfo(TAnchorKind),ORD(myAncor)));
...

Для перечисляемого типа можно использовать Хелперы, что значительно повышает удобство работы с таким типом данных, Примеры Хелперов для перечисляемого типа будут приведены далее в разделе Хелперы (Helpers)

Местные типы, переменные, константы, процедуры и функции

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

Местные типы

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

Ниже приведен пример локальной функции формирующей таблицу для сохранения в файл.

Пример процедуры вывода таблицы на чертеж
// Процедура вывода таблицы на чертеж на основе начальных данных
procedure CreateTable(AInitialData:TData); 
const
 // Местная константа
ColCount = 4;

type
  // Местная структура для хранения числовых данных
  TLocalStructure = record
    InPortContacts: integer;
    OutPortContacts: integer;
    TotalContactsOnType:integer;
  end;

var
  // Двумерный строковый массив
  Grid: array of array of string;
  // Массив для хранения расчетных данных
  LocalData: array of TLocalStructure;
  Block: TBlock;
  i, j: integer;

begin
 // Инициация массива табличных данных +1 - на заголовок
 SetLength(Grid,ColCount,AInitialData.Count+1);
 // Инициализация массива местной структуры
 SetLength(Grid,LocalData,AInitialData.Count); 
 // Формирование заголовка таблицы
 Grid[0,0]:='№';
 Grid[1,0]:='Тип';
 Grid[2,0]:='Число входных портов в типе';
 Grid[3,0]:='Число контактов на входной порт';   
 Grid[4,0]:='Число выходных портов в типе';
 Grid[5,0]:='Число контактов на выходной порт';   
 Grid[6,0]:='Всего число контактов на тип';  
 
 // Анализ данных и заполнение строкового массива
 i:=0;
 for Block in AInitialData do
  with LocalData[i] do
     begin
     // Установка кол-ва контактов в зависимости от типа порта
     if Block.Type_='A' then
      begin
      InPortContacts:=2;
      OutPortContacts:=3;
      end
     else
      begin
      InPortContacts:=4;
      OutPortContacts:=5;
      end;
     // Выполнение необходимых расчетов
     TotalContactsOnType:=Block.InPorts.Count*InPortCotact+Block.OutPorts.Count*OutPortContacts;
     inc(i);
     // Заполнение строки таблицы
     Grid[0,i]:=i.ToString();
     Grid[1,i]:=Block.Type_;
     Grid[2,i]:=Block.InPorts.Count.ToString();
     Grid[3,i]:=InPortCotact.ToString();   
     Grid[4,i]:=Block.OutPorts.Count.ToString();
     Grid[5,i]:=OutPortContacts.ToString();    
     Grid[6,i]:=TotalContactsOnType.ToString(); 
     end;
 
 for i:=0 to Length(LocalData)-1 do
  begin
  // Здесь может производиться дополнительная обработка или вычисления для строки итогов
  end;
  
 // Здесь может располагаться код для вывода StringArray в файл
end;

Местные функции

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

Ниже приведен простой пример кода реализации вложенной функции:

...
function One:integer;
function SubOne(a:integer):integer;
    begin
    result:=a*10;
    end;
var
i,j:integer
begin
i:=SubOne(1);
j:=SubOne(2);
result:=i+j;
end;
...

Условный оператор (If)

Во Free Pascal нет тернарного оператора, но есть альтернативные возможности реализации подобной логики.

Оператор ORD

Можно использовать функцию ORD(Boolean). Эта функция преобразует логическое значение (True или False) в целое число, где ORD(True) равно 1, а ORD(False) равно 0. Это может быть полезно, например, при умножении числа на логическое значение. Например, если мы хотим получить значение переменной x:-1 только в случае, если условие flag истинно, мы можем использовать следующий код:

x:=1-2*ORD(flag);

В примере если flag:=true, то x=-1, а если flag:=false, то x=1.

Словарь

Для определения вариантов значений которые будут получены при том или ином значении переменной типа boolean можно использовать словарь, как в коде ниже:

const
  direction:array[false..true] of integer =(-10,10);
var
  flag:boolean;
  i:integer;
begin
  flag:=false;
  i:=direction[flag];
  i+=100;
  i+=100;
end; 

Функция из модуля math

Для числовых значений можно использовать функцию из модуля math,

function IfThen(val:boolean;const iftrue:integer; const iffalse:integer= 0) :integer;

Несколько условий в операторе If

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

При объединении условий если первое условие не выполняется, то последующие условия не проверяются. В примере ниже функция two не будет вызвана. Дополнительно стоит обратить внимание на то что компилятор в первую очередь будет пытаться выполнить побитовый оператор and и в случае записи кода условия следующим образом: if one and two then - будет выходить ошибка, поэтому при объединении условий их необходимо брать в скобки.

program Project1;

function one:boolean;
begin
  result:=false;
end;

function two:boolean;
begin
  result:=true;
end;

begin
  if (one) and (two) then
  begin
    writeln('and');
  end;
end. 

По аналогии с кодом, приведенным выше, в первом условии выполняется проверка, инициирован ли определенный объект if (Assigned(SomeObject), во втором условии, допустим проверяется, данные в каком либо поле, тогда общая запись будет иметь вид if (Assigned(SomeObject)) and (SomeObject.Value <> 0) then. Такая конструкция с несколькими проверками делает код более читаемым и простым.

Оператор (in) для условий

Для перечислений удобно проверять наличие значения в наборе через оператор in

if (TmyEnum in [meApple, meBanana]) then
begin
end;

Оператор in можно определить для своей структуры и использовать уже его для проверок

uses
  SysUtils;

type
  TDisk = class;
  TScheme = specialize TObjectList;

operator in (const A: TDisk; const B: TScheme):boolean;
begin
  if not (B is TScheme) then
    raise Exception.Create('TScheme expected');

  Result := B.IndexOf(A) >= 0;
end;

Оператор цикла (for)

Помимо классической организации цикла for при которой задается начальное значение, условие продолжения и шаг итерации возможно использовать цикл for item in items, который позволяет перебирать элементы коллекции (например, массива или списка) без явного указания индексов.

...
var
  myArray: array[1..5] of Integer;
  item: Integer;

// Заполнение массива
for item in myArray do
begin
  //операции с элементами массива
end;

В случаях когда нужно обойти коллекцию с использованием индекса, начиная с последнего элемента до первого, то вместо того чтобы вычитать 1 из items.count, мы можем использовать функцию Pred, которая возвращает предыдущее значение. Пример:

var
  i: Integer;

for i := Pred(items.count) downto 0 do
begin
  // Ваш код здесь
end;

О чем еще не сказано

В части 2. статьи "Приемы работы в современном Free Pascal" будут приведены приемы работы со структурами и классами.

Комментарии (11)


  1. bodyawm
    19.12.2024 10:12

    Все еще люблю Pascal и смотрю на язык с теплотой. Выдал инвайт чтобы побольше писали статей о нем :)


  1. vadimr
    19.12.2024 10:12

    Дополнительно стоит обратить внимание на то что компилятор в первую очередь будет пытаться выполнить побитовый оператор and и в случае записи кода условия следующим образом: if one and two then - будет выходить ошибка, поэтому при объединении условий их необходимо брать в скобки.

    Этот момент разъясните, пожалуйста. У вас две булевские функции, о каком побитовом операторе идёт речь? Зачем скобки?


    1. bodyawm
      19.12.2024 10:12

      Особенность синтаксиса Pascal.

      a and b - побитовая операция

      (a < 0) and (b < 0) - boolean


      1. vadimr
        19.12.2024 10:12

        С каких пор вдруг, и благодаря чему?

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

        А самое главное, как скобки вообще могут на это влиять???


        1. gazkom
          19.12.2024 10:12

          приоритет and больше, чем <, поэтому a<0 and b<0 это то же, что a< (0 and b) <0, такое не компилируется ни в какой версии Паскаля, хоть 40 летней давности


          1. vadimr
            19.12.2024 10:12

            Это ясное дело, но мой вопрос о другом. Автор зачем-то ставит в скобки вызов булевских функций:

            if (one) and (two) then

            Я думаю, он как раз дезориентирован приоритетными скобочками для сравнений, о которых написали Вы.


            1. Seenkao
              19.12.2024 10:12

              Маловероятно, что дезориентирован.

              Вероятнее всего автор хотел какие-то "сложные" сравнения делать, а по ходу переделал код. Сложные сравнения без скобок выдадут просто ошибку (по большей части).


              1. vadimr
                19.12.2024 10:12

                Он там некую теоретическую базу подвёл, на которую я и отреагировал.


  1. NeoCode
    19.12.2024 10:12

    На Паскале не писал со школьных времен, но в силу интереса к языкам программирования как таковым тема интересна. Какие существуют отличия FreePascal от Delphi на уровне языка? Есть ли какие-то интересные языковые фичи?

    И еще - я как-то посмотреть на RTTI, где-то вычитал что оно в паскале реализовано весьма неплохо сделано. Скачал Lazarus, запустил - и оказалось что RTTI официальной последней версией компилятора не поддерживается. Якобы нужна (разрабатываемая в настоящее время) версия 3.3.1, а не официальная 3.2.2 от 2021 года. Что нибудь слышно о том когда будет релиз 3.3.1?


    1. HemulGM
      19.12.2024 10:12

      Какие существуют отличия FreePascal от Delphi на уровне языка? Есть ли какие-то интересные языковые фичи?

      Синтаксис Delphi достаточно сильно продвинулся от обычного Pascal. FreePascal (FPC) тоже продвигается, плюс, он пытается быть совместимым и с новшествами в Delphi (FPC позволяет выбрать режим совместимости директивой {$MODE DELPHI}).

      Но далеко не все добавляется в FPC. Например:

      1. Инлайн объявление переменных

      var i: integer := 2024;
      1. Выведение типов

      var i := 2024;
      
      var s := 'Привет';
      1. Анонимные функции (в FPC это на стадии открытого тестирования (в транке))

      var d := function: string begin Result := 'hi there' end;
      var ss := d();
      
      TTask.Run(
        procedure
        begin
          // работа в потоке
        end);
      1. Инлайн методов классов, процедур и функций (встраивание кода без выполнения вызова метода)

      TMyObj = class
        procedure DoSomthing; inline;
      end;
      
      procedure Test1; inline
      begin
      
      end;
      1. Использование переменной цикла без первичного объявления и for in

      for var i := 1 to 10 do ...
      
      for var Item in SomeList do ...
      
      for var Item in [1, 2, 3, 4, 5, 6] do ...
      1. Строки в Delphi по умолчанию UTF-16, в FPC - UTF-8

      2. Дженерики тоже работают по-разному. В fpc дженерики ближе к шаблонам.


      1. HemulGM
        19.12.2024 10:12

        1. Атрибуты (RTTI) в FPC тоже только на стадии тестирования.