一、为什么老Pascal项目需要"动手术"
1.1 老代码带来的日常困扰
想象一下,你接手了一个运行了十几年的Pascal系统,每天要往里面加新功能、改老bug。你会发现一个很现实的困境:代码像是用胶水粘在一起的大蛋糕,动一块就掉一坨。界面和业务逻辑混在一个大文件里,数据库连接写在全局变量里,一个函数干着五件事。想加个新功能?对不起,你得先读懂两千行没有注释的代码。想改个逻辑?改完这,那边可能就又炸了。
更头疼的是,这种老系统往往跑在一个已经很少人维护的环境上,比如XP系统,或者某个特定版本的Delphi。每次要发版,都得小心翼翼地祈祷别把哪个老功能改坏。开发效率低到让人怀疑人生——本来十分钟能搞定的事,因为要绕开一堆历史包袱,变成了一天的工作量。
1.2 不是重写,而是"微创手术"
很多团队面对这种情况,第一反应就是"推倒重来"。但说实话,推倒重来就像给病人做开胸手术——风险大、恢复慢、还可能留下后遗症。老系统里往往藏着很多业务细节和特殊逻辑,这些"知识"只存在于代码和老员工的脑子里。全部重写,很可能把一些关键业务规则给"翻译"丢了。
更务实的做法是做"微创手术":在不推翻整体架构的前提下,一层一层地把代码拆解开,把该分离的分离,把该封装的封装,让新代码和老代码能和平共处。就像给房子做装修,不是拆了重建,而是把旧电线换了、旧水管换了,但墙和梁还留着。这样既保住了老系统的稳定性,又能让新功能开发变得顺畅。
二、先给老项目做个"全面体检"
2.1 摸清代码家底
在动手改之前,第一步是把家底摸清楚。你需要知道这个老项目一共有多少个单元文件、每个文件大概做什么、文件之间的依赖关系是怎样的。有些老项目的目录结构混乱,几十个文件堆在一个文件夹里,名字还叫"Unit1.pas"、"Unit2.pas",根本看不出用途。
一个实用的办法是先画一张简单的模块关系图,不用太专业,手绘都行。把每个主要模块的功能写下来,再画箭头表示调用关系。比如你发现"frmMain.pas"里既画了界面又写了数据库操作,还处理了业务逻辑,那这个文件就是典型的"大杂烩",是后续重构的重点对象。
{
============================================================
项目依赖检查脚本(辅助工具,非项目核心代码)
用途:扫描Pascal项目中的uses依赖关系
技术栈:Pascal (Free Pascal 兼容)
============================================================
}
program DependencyChecker;
uses
SysUtils, Classes,{$IFDEF UNIX}cthreads{$ELSE}Windows{$ENDIF};
{
函数说明:分析单元文件间的依赖关系
参数:FileName - 目标文件路径
返回值:无,结果直接输出到屏幕
}
procedure AnalyzeDependencies(const FileName: string);
var
F: TextFile;
Line: string;
Lines: TStringList;
UsesSection: boolean;
UnitName: string;
begin
Lines := TStringList.Create;
try
{ 打开文件并逐行读取 }
Assign(F, FileName);
{$I-}
Reset(F);
{$I+}
if IOResult <> 0 then
begin
writeln('无法打开文件: ', FileName);
exit;
end;
UsesSection := false;
UnitName := '';
{ 逐行扫描文件内容 }
while not EOF(F) do
begin
ReadLn(F, Line);
Lines.Add(Line);
{ 检查是否进入uses区域 }
if Pos('uses', LowerCase(Line)) > 0 then
UsesSection := true;
{ 检测procedure/function开始,uses区域结束 }
if (Pos('procedure', LowerCase(Line)) > 0) or
(Pos('function', LowerCase(Line)) > 0) then
UsesSection := false;
end;
Close(F);
writeln('========== 文件: ', FileName, ' ==========');
{ 输出uses区域中引用的模块 }
if UsesSection then
begin
writeln(' 依赖的模块:');
for Line in Lines do
begin
if Pos('uses', LowerCase(Line)) > 0 then
then
break;
end;
end;
finally
Lines.Free;
end;
end;
begin
{ 对指定文件进行依赖分析 }
if ParamCount < 1 then
begin
writeln('用法: DependencyChecker <文件路径>');
exit;
end;
AnalyzeDependencies(ParamStr(1));
writeln;
writeln('依赖分析完成。');
end.
2.2 找出性能瓶颈和依赖关系
体检的第二个重点是找性能瓶颈。老Pascal项目常见的问题包括:在循环里做数据库查询、大量使用全局变量、字符串拼接没有用缓冲区等。这些问题不一定会让系统立刻崩掉,但会让系统越跑越慢,到最后用户忍不了就开始抱怨。
还有一种隐蔽的依赖问题需要注意:有些老代码会把数据库连接字符串写死在代码里,或者把配置信息分散在好几个地方。这意味着如果你想把项目部署到测试环境或者生产环境,每次都要手动改代码。这种"硬编码"是重构时必须解决的老大难问题。
三、分层重构:把大坨代码拆成小块
3.1 数据层分离
老Pascal项目最大的毛病之一就是数据操作和业务逻辑混在一起。一个按钮的点击事件里,直接连着SQL语句,连完了又把结果填到界面上,整个过程一气呵成、毫无章法。
重构的第一步就是把数据库操作独立出来,形成一个专门的"数据访问层"。这个层只负责和数据库打交道,不关心界面上长什么样,也不关心业务规则是什么。这样一来,将来如果数据库换了,你只需要改数据层,其他地方不用动。
{
============================================================
数据访问层 - 用户数据操作模块
技术栈:Pascal (Free Pascal / Delphi 兼容)
说明:将所有数据库操作集中在此模块,实现数据层分离
============================================================
}
unit UserDAL;
interface
uses
SysUtils, Classes;
{
类型定义:用户记录
把数据库表结构映射为Pascal记录类型
}
type
TUserInfo = record
UserID: Integer; { 用户ID,主键 }
UserName: string; { 用户名 }
Email: string; { 邮箱地址 }
IsActive: boolean; { 是否激活状态 }
end;
{
类型定义:用户数据访问接口
通过接口定义数据操作契约,方便测试和替换实现
}
IUserRepository = interface
['{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}']
function GetUsers: TList;
procedure SaveUser(const AUserInfo: TUserInfo);
function FindUserById(AUserId: Integer): TUserInfo;
procedure DeleteUser(AUserId: Integer);
end;
{
类定义:用户数据访问实现
实现IUserRepository接口的具体数据库操作逻辑
}
TUserRepository = class(TInterfacedObject, IUserRepository)
private
FConnectionString: string;
{
私有方法:创建数据库连接
封装连接逻辑,避免连接代码散落在各处
}
function CreateConnection: TObject;
procedure ExecuteQuery(const ASQL: string);
public
constructor Create(const AConnectionString: string);
destructor Destroy; override;
function GetUsers: TList;
procedure SaveUser(const AUserInfo: TUserInfo);
function FindUserById(AUserId: Integer): TUserInfo;
procedure DeleteUser(AUserId: Integer);
end;
implementation
{
构造函数:初始化数据访问对象
参数:AConnectionString - 数据库连接字符串
}
constructor TUserRepository.Create(const AConnectionString: string);
begin
inherited Create;
FConnectionString := AConnectionString;
end;
{
析构函数:清理资源
}
destructor TUserRepository.Destroy;
begin
inherited Destroy;
end;
{
创建数据库连接
集中管理连接逻辑,便于将来替换数据库驱动
}
function TUserRepository.CreateConnection: TObject;
begin
{
注意:此处为示例框架,实际使用时需要引入具体的数据库组件
比如TADOConnection、TSQLConnection等
这里用TObject代替以避免依赖特定组件
}
Result := TObject.Create;
{ 在实际项目中,这里会执行:
FConn := TADOConnection.Create(nil);
FConn.ConnectionString := FConnectionString;
FConn.Connected := True;
}
end;
{
执行SQL查询
所有SQL语句都通过这个统一方法执行
}
procedure TUserRepository.ExecuteQuery(const ASQL: string);
var
Conn: TObject;
begin
Conn := CreateConnection;
try
{ 实际执行SQL逻辑 }
{ 示例:使用TADOQuery执行查询 }
{ Query.SQL.Text := ASQL; }
{ Query.Open; }
finally
Conn.Free;
end;
end;
{
获取所有用户列表
返回TList,每个元素是一个TUserInfo指针
}
function TUserRepository.GetUsers: TList;
var
Conn: TObject;
User: TUserInfo;
begin
Result := TList.Create;
try
ExecuteQuery('SELECT UserID, UserName, Email, IsActive FROM Users');
{ 在实际项目中,这里会遍历查询结果集 }
{ 示例伪代码:
while not Query.Eof do
begin
User.UserID := Query.Fields['UserID'].AsInteger;
User.UserName := Query.Fields['UserName'].AsString;
User.Email := Query.Fields['Email'].AsString;
User.IsActive := Query.Fields['IsActive'].AsBoolean;
Result.Add(User);
Query.Next;
end;
}
except
on E: Exception do
writeln('获取用户列表失败: ', E.Message);
end;
end;
{
保存用户信息
处理新增或更新用户的逻辑
}
procedure TUserRepository.SaveUser(const AUserInfo: TUserInfo);
var
User: TUserInfo;
begin
User := AUserInfo;
if AUserInfo.UserID = 0 then
begin
{ 新增用户 }
ExecuteQuery(Format(
'INSERT INTO Users (UserName, Email, IsActive) VALUES (''%s'', ''%s'', %d)',
[AUserInfo.UserName, AUserInfo.Email, Ord(AUserInfo.IsActive)]));
end
else
begin
{ 更新现有用户 }
ExecuteQuery(Format(
'UPDATE Users SET UserName = ''%s'', Email = ''%s'', IsActive = %d WHERE UserID = %d',
[AUserInfo.UserName, AUserInfo.Email, Ord(AUserInfo.IsActive), AUserInfo.UserID]));
end;
end;
{
根据ID查找单个用户
返回TUserInfo记录
}
function TUserRepository.FindUserById(AUserId: Integer): TUserInfo;
begin
Result := Default(TUserInfo);
ExecuteQuery(Format('SELECT * FROM Users WHERE UserID = %d', [AUserId]));
{ 在实际项目中,这里会从结果集中读取用户信息 }
{ 示例伪代码:
if not Query.Eof then
begin
Result.UserID := Query.Fields['UserID'].AsInteger;
Result.UserName := Query.Fields['UserName'].AsString;
Result.Email := Query.Fields['Email'].AsString;
Result.IsActive := Query.Fields['IsActive'].AsBoolean;
end;
}
end;
{
删除用户
通过ID删除指定用户记录
}
procedure TUserRepository.DeleteUser(AUserId: Integer);
begin
ExecuteQuery(Format('DELETE FROM Users WHERE UserID = %d', [AUserId]));
end;
end.
3.2 业务逻辑层提取
数据层分出来了,下一步是提取业务逻辑层。业务逻辑层是"大脑",它不关心数据从哪里来(可能是数据库,也可能是文件),也不关心怎么展示(可能是Windows界面,也可能是Web页面)。它只关心"应该做什么":比如校验用户名是否合法、计算用户的权限、处理订单流程。
{
============================================================
业务逻辑层 - 用户管理服务
技术栈:Pascal (Free Pascal / Delphi 兼容)
说明:封装业务规则,不直接操作数据库,不直接操作界面
============================================================
}
unit UserService;
interface
uses
Classes, UserDAL;
{
类定义:用户业务服务
负责协调数据层,执行业务规则
不直接访问数据库,通过IUserRepository接口操作
}
TUserService = class
private
FRepository: IUserRepository;
{
私有方法:校验用户数据是否合法
参数:AUserInfo - 待校验的用户信息
返回值:校验是否通过
}
function ValidateUser(const AUserInfo: TUserInfo): boolean;
public
constructor Create(ARepository: IUserRepository);
{
注册新用户
包含:校验、生成ID、保存到数据库
}
function RegisterUser(const AUserName, AEmail: string): boolean;
{
激活用户账户
参数:AUserId - 用户ID
返回值:操作是否成功
}
function ActivateUser(AUserId: Integer): boolean;
{
锁定用户账户
当用户多次输错密码时调用
参数:AUserId - 用户ID,AFailCount - 失败次数
}
procedure LockUser(AUserId, AFailCount: Integer);
end;
implementation
{
构造函数:注入数据访问层依赖
使用依赖注入模式,方便测试
}
constructor TUserService.Create(ARepository: IUserRepository);
begin
inherited Create;
FRepository := ARepository;
end;
{
校验用户数据合法性
检查:用户名不为空、邮箱格式正确、用户名长度合规
}
function TUserService.ValidateUser(const AUserInfo: TUserInfo): boolean;
var
AtPos: Integer;
begin
Result := True;
{ 检查用户名是否为空或过短 }
if Length(AUserInfo.UserName) < 3 then
begin
Result := False;
exit;
end;
{ 检查邮箱格式:必须包含@符号 }
AtPos := Pos('@', AUserInfo.Email);
if (AtPos = 0) or (AtPos = Length(AUserInfo.Email)) then
begin
Result := False;
exit;
end;
{ 检查用户名是否包含非法字符 }
{ 注意:此处仅做简单示例,实际项目需要更严格的校验 }
end;
{
注册新用户
业务流程:
1. 校验输入数据
2. 检查用户名是否已存在
3. 生成新用户记录
4. 调用数据层保存
5. 返回结果
}
function TUserService.RegisterUser(const AUserName, AEmail: string): boolean;
var
NewUser: TUserInfo;
begin
{ 第一步:校验输入数据 }
NewUser.UserName := AUserName;
NewUser.Email := AEmail;
NewUser.IsActive := True;
NewUser.UserID := 0; { 新增用户,ID由数据库生成 }
if not ValidateUser(NewUser) then
begin
Result := False;
writeln('注册失败:用户数据校验不通过');
exit;
end;
{ 第二步:检查用户名是否已存在 }
{ 在实际项目中,这里需要调用FRepository查询 }
{ 示例:遍历GetUsers检查是否有同名用户 }
{ 第三步:保存新用户 }
FRepository.SaveUser(NewUser);
Result := True;
writeln('用户注册成功: ', AUserName);
end;
{
激活用户账户
业务流程:
1. 查找用户是否存在
2. 检查是否已激活
3. 更新激活状态
}
function TUserService.ActivateUser(AUserId: Integer): boolean;
var
User: TUserInfo;
begin
Result := False;
{ 查找用户 }
User := FRepository.FindUserById(AUserId);
if User.UserID = 0 then
begin
writeln('激活失败:用户不存在');
exit;
end;
if User.IsActive then
begin
writeln('激活失败:用户已激活');
exit;
end;
{ 更新激活状态 }
User.IsActive := True;
FRepository.SaveUser(User);
Result := True;
writeln('用户激活成功: ', User.UserName);
end;
{
锁定用户账户
业务流程:
1. 判断失败次数是否达到阈值
2. 达到阈值则禁用账户
}
procedure TUserService.LockUser(AUserId, AFailCount: Integer);
const
MaxFailAttempts = 5; { 最大允许失败次数 }
var
User: TUserInfo;
begin
if AFailCount < MaxFailAttempts then
begin
writeln(Format('用户还有 %d 次尝试机会', [MaxFailAttempts - AFailCount]));
exit;
end;
{ 达到最大失败次数,锁定账户 }
User := FRepository.FindUserById(AUserId);
if User.UserID > 0 then
begin
User.IsActive := False;
FRepository.SaveUser(User);
writeln('用户账户已锁定: ', User.UserName);
end;
end;
end.
3.3 界面层适配
最后一个层是界面层。重构后的界面层应该变得很"薄",它只负责显示数据、接收用户输入,然后把输入交给业务逻辑层处理,把返回结果显示出来。它不再直接写SQL,不再直接做业务判断。
{
============================================================
界面层 - 用户管理表单
技术栈:Pascal (Free Pascal / Delphi 兼容)
说明:界面层保持"薄",只负责展示和输入,不写业务逻辑
============================================================
}
unit UserForm;
interface
uses
Classes, Forms, Controls, StdCtrls, UserDAL, UserService;
{
类定义:用户管理表单
界面层的核心职责:
1. 提供输入控件
2. 调用业务服务
3. 显示处理结果
}
TUserForm = class(TForm)
private
FUserService: TUserService;
{ 私有方法:初始化业务服务 }
procedure InitService;
public
constructor Create; override;
{ 公共方法:处理用户注册按钮点击 }
procedure DoRegister;
{ 公共方法:刷新用户列表 }
procedure RefreshUserList;
end;
var
UserForm: TUserForm;
implementation
uses
SysUtils;
constructor TUserForm.Create;
begin
inherited Create;
{ 初始化表单基础属性 }
Caption := '用户管理系统';
Width := 600;
Height := 400;
{ 初始化业务服务 }
InitService;
end;
{
初始化业务服务
创建数据访问层实例,注入到业务层
}
procedure TUserForm.InitService;
var
Repository: IUserRepository;
begin
{ 创建数据访问层实例 }
Repository := TUserRepository.Create('Server=localhost;Database=MyDB');
{ 将数据访问层注入到业务服务 }
FUserService := TUserService.Create(Repository);
{
注意:这里使用依赖注入模式
好处:
1. 业务层不依赖具体的数据库实现
2. 方便单元测试时替换为Mock对象
3. 切换数据库时只需改数据访问层
}
end;
{
处理用户注册
流程:
1. 从输入控件获取数据
2. 调用业务服务处理
3. 显示处理结果
}
procedure TUserForm.DoRegister;
var
UserName, Email: string;
Success: boolean;
begin
{ 第一步:获取用户输入 }
{ 在实际项目中,这些值从Edit控件获取 }
{ UserName := Edit1.Text; }
{ Email := Edit2.Text; }
{ 第二步:调用业务服务 }
Success := FUserService.RegisterUser(UserName, Email);
{ 第三步:显示结果 }
if Success then
writeln('注册成功!')
else
writeln('注册失败,请检查输入信息');
end;
{
刷新用户列表
调用业务服务获取数据,填充到列表控件
}
procedure TUserForm.RefreshUserList;
var
Users: TList;
i: Integer;
User: PUserInfo;
begin
{ 获取用户列表 }
{ 在实际项目中,这里应该通过UserService或Repository获取 }
{ 界面层不应该直接操作数据访问层 }
end;
end.
四、用现代设计模式"包"一层
4.1 单例模式改造
老Pascal项目里到处都是全局变量。比如一个全局的数据库连接对象、一个全局的配置对象。这些全局变量让代码变得难以测试,也难以在多线程环境下使用。
单例模式可以让全局变量变得"有控制"。你不再直接在代码里创建对象,而是通过单例方法获取。这样对象的生命周期变得可控,也能在程序退出时正确释放。
{
============================================================
单例模式 - 全局配置管理器
技术栈:Pascal (Free Pascal / Delphi 兼容)
说明:用单例模式替代散乱的全局变量
============================================================
}
unit ConfigManager;
interface
{
类定义:配置管理器(单例)
用途:统一管理所有配置项,替代散落各处的全局变量
特点:
1. 全局只有一个实例
2. 通过GetInstance方法获取
3. 支持从文件加载配置
4. 线程安全
}
TConfigManager = class
private
class var
FInstance: TConfigManager; { 单例实例 }
FAppTitle: string; { 应用标题 }
FDBHost: string; { 数据库主机 }
FDBPort: Integer; { 数据库端口 }
FDBName: string; { 数据库名称 }
FLogLevel: string; { 日志级别 }
{
私有构造函数
防止外部直接创建实例
}
constructor Create;
public
class function GetInstance: TConfigManager;
destructor Destroy; override;
{ 公共属性 }
property AppTitle: string read FAppTitle write FAppTitle;
property DBHost: string read FDBHost write FDBHost;
property DBPort: Integer read FDBPort write FDBPort;
property DBName: string read FDBName write FDBName;
property LogLevel: string read FLogLevel write FLogLevel;
{ 从配置文件加载 }
procedure LoadFromFile(const AFilePath: string);
{ 生成数据库连接字符串 }
function GetConnectionStr: string;
end;
implementation
uses
SysUtils;
{
私有构造函数:初始化默认配置值
构造函数设为private,确保只能通过GetInstance创建
}
constructor TConfigManager.Create;
begin
inherited Create;
{ 设置默认值 }
FAppTitle := '我的应用';
FDBHost := 'localhost';
FDBPort := 5432;
FDBName := 'default_db';
FLogLevel := 'INFO';
end;
{
获取单例实例
如果实例不存在则创建
如果实例已存在则直接返回
}
class function TConfigManager.GetInstance: TConfigManager;
begin
if FInstance = nil then
begin
FInstance := TConfigManager.Create;
writeln('[Config] 配置管理器已初始化');
end;
Result := FInstance;
end;
{
析构函数:清理资源
}
destructor TConfigManager.Destroy;
begin
FInstance := nil;
inherited Destroy;
end;
{
从配置文件加载配置
支持从文本文件读取键值对配置
}
procedure TConfigManager.LoadFromFile(const AFilePath: string);
var
F: TextFile;
Line, Key, Value: string;
Pos: Integer;
begin
Assign(F, AFilePath);
{$I-}
Reset(F);
{$I+}
if IOResult <> 0 then
begin
writeln('[Config] 无法打开配置文件: ', AFilePath);
exit;
end;
{ 逐行读取配置文件 }
while not EOF(F) do
begin
ReadLn(F, Line);
{ 跳过空行和注释行 }
if (Length(Line) = 0) or (Line[1] = '#') then
continue;
{ 解析键值对,格式:Key=Value }
Pos := Pos('=', Line);
if Pos = 0 then continue;
Key := Trim(Copy(Line, 1, Pos - 1));
Value := Trim(Copy(Line, Pos + 1, Length(Line)));
{ 根据键名设置对应的属性 }
if LowerCase(Key) = 'app_title' then
FAppTitle := Value
else if LowerCase(Key) = 'db_host' then
FDBHost := Value
else if LowerCase(Key) = 'db_port' then
FDBPort := StrToIntDef(Value, 5432)
else if LowerCase(Key) = 'db_name' then
FDBName := Value
else if LowerCase(Key) = 'log_level' then
FLogLevel := Value;
end;
Close(F);
writeln('[Config] 配置加载完成');
end;
{
生成数据库连接字符串
将分散的配置项组合成完整的连接字符串
}
function TConfigManager.GetConnectionStr: string;
begin
Result := Format('Server=%s;Port=%d;Database=%s',
[FDBHost, FDBPort, FDBName]);
end;
end.
4.2 观察者模式接入
老代码里经常出现一个现象:数据变了,界面不更新;界面更新了,业务状态不知道。这种"通知"问题在老代码里往往用全局变量或者反复查询数据库来解决,效率低还容易出错。
观察者模式就是为了解决这类问题的。一个对象变了,所有关注它的对象都会收到通知。比如用户列表更新了,所有显示用户列表的地方都会自动刷新。
{
============================================================
观察者模式 - 数据变更通知系统
技术栈:Pascal (Free Pascal / Delphi 兼容)
说明:当数据发生变更时,自动通知所有关注方
============================================================
}
unit DataChangeNotifier;
interface
uses
Classes;
{
类型定义:观察者接口
任何想要接收数据变更通知的对象都实现这个接口
}
IDataObserver = interface
['{B2C3D4E5-F6A7-8901-BCDE-F12345678901}']
{
数据变更回调方法
参数:ASender - 变更来源,AChangeType - 变更类型
}
procedure OnDataChanged(ASender: TObject; AChangeType: string);
end;
{
类型定义:主题类(被观察者)
负责管理观察者列表,触发通知
}
TDataSubject = class
protected
FObservers: TList; { 观察者列表 }
{
受保护方法:添加观察者
}
procedure Attach(AObserver: IDataObserver);
{
受保护方法:移除观察者
}
procedure Detach(AObserver: IDataObserver);
{
受保护方法:通知所有观察者
}
procedure Notify(ASender: TObject; AChangeType: string);
public
constructor Create;
destructor Destroy; override;
end;
implementation
constructor TDataSubject.Create;
begin
inherited Create;
FObservers := TList.Create;
end;
destructor TDataSubject.Destroy;
begin
FObservers.Free;
inherited Destroy;
end;
{
添加观察者
将观察者添加到通知列表中
}
procedure TDataSubject.Attach(AObserver: IDataObserver);
begin
FObservers.Add(Pointer(AObserver));
end;
{
移除观察者
从通知列表中删除指定观察者
}
procedure TDataSubject.Detach(AObserver: IDataObserver);
var
i: Integer;
begin
for i := FObservers.Count - 1 downto 0 do
begin
if Pointer(FObservers[i]) = Pointer(AObserver) then
begin
FObservers.Delete(i);
break;
end;
end;
end;
{
通知所有观察者
遍历列表,逐个调用OnDataChanged方法
}
procedure TDataSubject.Notify(ASender: TObject; AChangeType: string);
var
i: Integer;
Observer: IDataObserver;
begin
for i := 0 to FObservers.Count - 1 do
begin
Observer := IDataObserver(FObservers[i]);
Observer.OnDataChanged(ASender, AChangeType);
end;
end;
end.
五、让老代码"说新话":接口封装与适配
5.1 封装遗留接口
很多老Pascal项目依赖一些已经很少人维护的组件或DLL。比如某个老版本的数据库驱动、某个特定的硬件通信库。这些遗留依赖不能直接替换,但你可以用一个新接口把它们"包"起来,让外部代码只跟新接口打交道,不直接依赖老组件。
{
============================================================
适配层 - 遗留数据库接口封装
技术栈:Pascal (Free Pascal / Delphi 兼容)
说明:将老版数据库操作封装到新接口后面
============================================================
}
unit LegacyDBAdapter;
interface
uses
Classes;
{
类型定义:新数据库接口
定义现代项目需要的数据库操作契约
这个接口是"新语言",外部代码只跟它打交道
}
IModernDBInterface = interface
['{C3D4E5F6-A7B8-9012-CDEF-123456789012}']
procedure Connect;
procedure Disconnect;
function ExecuteSQL(const ASQL: string): boolean;
function QuerySQL(const ASQL: string): TList;
function GetRowCount: Integer;
end;
{
类定义:遗留数据库适配器
实现IModernDBInterface接口
内部调用老版本的数据库组件
起到"翻译官"的作用
}
TLegacyDBAdapter = class(TInterfacedObject, IModernDBInterface)
private
FConnected: boolean;
FLegacyConn: TObject; { 老版本数据库连接对象 }
{
私有方法:初始化老版数据库连接
封装老版API的初始化逻辑
}
procedure InitLegacyConnection;
{
私有方法:将老版结果集转换为新格式
桥接老数据结构和现代数据结构
}
function ConvertResultSet(ALegacyData: TObject): TList;
public
constructor Create;
destructor Destroy; override;
procedure Connect;
procedure Disconnect;
function ExecuteSQL(const ASQL: string): boolean;
function QuerySQL(const ASQL: string): TList;
function GetRowCount: Integer;
end;
implementation
constructor TLegacyDBAdapter.Create;
begin
inherited Create;
FConnected := False;
FLegacyConn := nil;
end;
destructor TLegacyDBAdapter.Destroy;
begin
if FConnected then Disconnect;
inherited Destroy;
end;
{
初始化老版数据库连接
这里调用老版API,将初始化逻辑封装在此
外部代码不需要知道老版API的细节
}
procedure TLegacyDBAdapter.InitLegacyConnection;
begin
{
示例:老版数据库驱动初始化
{ FLegacyConn := TOldDBConnection.Create(nil); }
{ FLegacyConn.HostName := 'localhost'; }
{ FLegacyConn.Database := 'legacy_db'; }
{ FLegacyConn.Open; }
注意:实际的初始化代码取决于你使用的老版组件
这里用注释代替以避免依赖特定组件
}
FConnected := True;
end;
{
建立连接
调用老版连接逻辑
}
procedure TLegacyDBAdapter.Connect;
begin
if FConnected then exit;
InitLegacyConnection;
end;
{
断开连接
调用老版断开逻辑
}
procedure TLegacyDBAdapter.Disconnect;
begin
if not FConnected then exit;
{
示例:老版数据库断开
{ FLegacyConn.Close; }
{ FLegacyConn.Free; }
}
FConnected := False;
FLegacyConn := nil;
end;
{
执行SQL语句(非查询语句)
将现代接口调用转换为老版API调用
}
function TLegacyDBAdapter.ExecuteSQL(const ASQL: string): boolean;
begin
Result := False;
if not FConnected then
begin
writeln('[LegacyDB] 数据库未连接');
exit;
end;
{
示例:调用老版执行方法
{ FLegacyConn.ExecSQL(ASQL); }
Result := True;
}
Result := True;
end;
{
执行查询SQL并返回结果列表
将老版结果集转换为标准TList格式
}
function TLegacyDBAdapter.QuerySQL(const ASQL: string): TList;
begin
Result := TList.Create;
if not FConnected then
begin
writeln('[LegacyDB] 数据库未连接');
exit;
end;
{
示例:调用老版查询方法并转换结果
{ FLegacyConn.OpenSQL(ASQL); }
{ Result := ConvertResultSet(FLegacyConn.ResultSet); }
实际转换逻辑取决于老版组件返回的数据结构
}
end;
{
获取上一次查询的结果行数
}
function TLegacyDBAdapter.GetRowCount: Integer;
begin
Result := 0;
{
示例:从老版组件获取行数
{ Result := FLegacyConn.RecordCount; }
}
end;
{
将老版结果集转换为现代列表格式
这是适配层的核心工作之一
}
function TLegacyDBAdapter.ConvertResultSet(ALegacyData: TObject): TList;
var
i: Integer;
begin
Result := TList.Create;
{
遍历老版结果集,逐行转换为现代数据结构
具体转换逻辑取决于老版组件的API
}
{ 示例:
for i := 0 to ALegacyData.RecordCount - 1 do
begin
RowData := CreateModernRecord(ALegacyData.FieldNames, ALegacyData);
Result.Add(RowData);
ALegacyData.MoveNext;
end;
}
end;
end.
5.2 桥接模式适配新框架
有些老Pascal项目需要接入新的UI框架,比如从传统的Windows Forms切换到跨平台的Lazarus。桥接模式可以让"实现"和"抽象"分离,你不需要改老代码就能接入新框架。
{
============================================================
桥接模式 - UI框架适配层
技术栈:Pascal (Free Pascal / Delphi 兼容)
说明:分离业务逻辑和UI框架实现,支持切换不同UI框架
============================================================
}
unit UIBridge;
interface
uses
Classes;
{
类型定义:UI实现接口
定义UI框架需要提供的操作
}
IUIImplementation = interface
['{D4E5F6A7-B8C9-0123-DEF1-234567890123}']
procedure ShowMessage(const AText: string);
procedure ShowDialog(ACaption: string);
function GetInput(const APrompt: string): string;
procedure ShowProgress(ATitle: string; AMaxValue: Integer);
procedure UpdateProgress(AValue: Integer);
procedure CloseProgress;
end;
{
类型定义:UI抽象基类
业务代码通过这个类操作UI,不直接依赖具体UI框架
}
TUIAbstract = class
protected
FImpl: IUIImplementation;
public
constructor Create(AImpl: IUIImplementation);
procedure DisplayInfo(const AText: string);
procedure ShowInputDialog;
procedure ShowProgressBar(ATitle: string; ASteps: Integer);
end;
{
类定义:Delphi UI实现
实现IUIImplementation,使用Delphi原生API
}
TDelphiUIImpl = class(TInterfacedObject, IUIImplementation)
public
procedure ShowMessage(const AText: string);
procedure ShowDialog(ACaption: string);
function GetInput(const APrompt: string): string;
procedure ShowProgress(ATitle: string; AMaxValue: Integer);
procedure UpdateProgress(AValue: Integer);
procedure CloseProgress;
end;
{
类定义:Lazarus UI实现
实现IUIImplementation,使用Lazarus跨平台API
}
TLazarusUIImpl = class(TInterfacedObject, IUIImplementation)
public
procedure ShowMessage(const AText: string);
procedure ShowDialog(ACaption: string);
function GetInput(const APrompt: string): string;
procedure ShowProgress(ATitle: string; AMaxValue: Integer);
procedure UpdateProgress(AValue: Integer);
procedure CloseProgress;
end;
implementation
{
UI抽象基类实现
业务代码使用这个类,不关心底层是Delphi还是Lazarus
}
constructor TUIAbstract.Create(AImpl: IUIImplementation);
begin
inherited Create;
FImpl := AImpl;
end;
{
显示信息消息
委托给具体的UI实现执行
}
procedure TUIAbstract.DisplayInfo(const AText: string);
begin
FImpl.ShowMessage(AText);
end;
{
显示输入对话框
委托给具体的UI实现
}
procedure TUIAbstract.ShowInputDialog;
var
Input: string;
begin
Input := FImpl.GetInput('请输入数据:');
writeln('用户输入: ', Input);
end;
{
显示进度条
委托给具体的UI实现
}
procedure TUIAbstract.ShowProgressBar(ATitle: string; ASteps: Integer);
var
i: Integer;
begin
FImpl.ShowProgress(ATitle, ASteps);
for i := 1 to ASteps do
begin
{ 模拟工作 }
Sleep(100);
FImpl.UpdateProgress(i);
end;
FImpl.CloseProgress;
end;
{
Delphi UI实现
使用Delphi原生的MessageDlg等API
}
procedure TDelphiUIImpl.ShowMessage(const AText: string);
begin
{ 示例:Delphi原生消息框 }
{ Application.MessageBox(PChar(AText), '提示', 0); }
writeln('[Delphi UI] ', AText);
end;
procedure TDelphiUIImpl.ShowDialog(ACaption: string);
begin
{ 示例:打开Delphi原生对话框 }
writeln('[Delphi UI] 打开对话框: ', ACaption);
end;
function TDelphiUIImpl.GetInput(const APrompt: string): string;
begin
{ 示例:Delphi输入对话框 }
Result := '';
writeln('[Delphi UI] ', APrompt);
{ 实际实现中会显示输入框并返回用户输入 }
end;
procedure TDelphiUIImpl.ShowProgress(ATitle: string; AMaxValue: Integer);
begin
writeln('[Delphi UI] 进度条: ', ATitle, ' (共', AMaxValue, '步)');
end;
procedure TDelphiUIImpl.UpdateProgress(AValue: Integer);
begin
{ 示例:更新Delphi进度条 }
writeln('[Delphi UI] 进度: ', AValue);
end;
procedure TDelphiUIImpl.CloseProgress;
begin
writeln('[Delphi UI] 进度条已关闭');
end;
{
Lazarus UI实现
使用Lazarus的LCL跨平台API
}
procedure TLazarusUIImpl.ShowMessage(const AText: string);
begin
{ 示例:Lazarus的ShowMessage函数 }
{ LMessageDlg(AText, mtInformation, [mbOK], 0); }
writeln('[Lazarus UI] ', AText);
end;
procedure TLazarusUIImpl.ShowDialog(ACaption: string);
begin
{ 示例:打开Lazarus对话框 }
writeln('[Lazarus UI] 打开对话框: ', ACaption);
end;
function TLazarusUIImpl.GetInput(const APrompt: string): string;
begin
{ 示例:Lazarus输入对话框 }
{ Result := InputQuery(APrompt, '请输入:', ''); }
Result := '';
writeln('[Lazarus UI] ', APrompt);
end;
procedure TLazarusUIImpl.ShowProgress(ATitle: string; AMaxValue: Integer);
begin
writeln('[Lazarus UI] 进度条: ', ATitle, ' (共', AMaxValue, '步)');
end;
procedure TLazarusUIImpl.UpdateProgress(AValue: Integer);
begin
writeln('[Lazarus UI] 进度: ', AValue);
end;
procedure TLazarusUIImpl.CloseProgress;
begin
writeln('[Lazarus UI] 进度条已关闭');
end;
end.
六、实战演练:一个完整重构案例
6.1 原始遗留代码
先看一个典型的遗留代码片段。这是一个用户登录模块,所有东西都堆在一个文件里:
{
============================================================
【重构前】原始遗留代码 - 用户登录模块
技术栈:Pascal (Delphi 7 时代代码风格)
问题:全局变量、数据库操作和业务逻辑混在一起、硬编码配置
============================================================
}
unit LoginUnit;
interface
uses
Windows, Messages, SysUtils, Classes,
Controls, Forms, Dialogs, StdCtrls,
DBAccess, Unit1, Unit2, Unit3, Unit5; { 大量不必要的依赖 }
var
{ 全局变量:数据库连接、配置信息 }
GlobalConn: TObject;
GlobalDBServer: string;
GlobalDBName: string;
GlobalDBUser: string;
GlobalDBPass: string;
GlobalMaxLoginAttempts: Integer;
GlobalCurrentAttempts: Integer;
{ 表单声明 }
TLoginForm = class(TForm)
EditUserName: TEdit;
EditPassword: TEdit;
BtnLogin: TButton;
LblError: TLabel;
procedure BtnLoginClick(Sender: TObject);
end;
implementation
{
登录点击事件处理
问题:
1. 全局变量到处是
2. SQL直接写在事件处理里
3. 错误处理简陋
4. 没有参数校验
5. 硬编码配置
}
procedure TLoginForm.BtnLoginClick(Sender: TObject);
var
SQL: string;
Result: TObject;
PasswordHash: string;
AttemptCount: Integer;
begin
{ 直接用全局变量,没有封装 }
GlobalConn.Connected := False;
GlobalConn.HostName := GlobalDBServer;
GlobalConn.Database := GlobalDBName;
GlobalConn.UserName := GlobalDBUser;
GlobalConn.Password := GlobalDBPass;
GlobalConn.Connected := True;
{ 在循环里校验,逻辑混乱 }
if EditUserName.Text = '' then
begin
LblError.Caption := '用户名不能为空';
Exit;
end;
if EditPassword.Text = '' then
begin
LblError.Caption := '密码不能为空';
Exit;
end;
{ SQL硬编码在事件处理里 }
SQL := 'SELECT * FROM Users WHERE Username = ''' + EditUserName.Text + '''';
GlobalConn.OpenSQL(SQL);
if GlobalConn.EOF then
begin
LblError.Caption := '用户名或密码错误';
GlobalCurrentAttempts := GlobalCurrentAttempts + 1;
Exit;
end;
{ 密码校验逻辑也在这里 }
PasswordHash := GlobalConn.GetField('Password');
if PasswordHash <> EditPassword.Text then
begin
LblError.Caption := '用户名或密码错误';
GlobalCurrentAttempts := GlobalCurrentAttempts + 1;
{ 简单的锁定逻辑 }
if GlobalCurrentAttempts >= 5 then
begin
SQL := 'UPDATE Users SET Active = 0 WHERE Username = ''' + EditUserName.Text + '''';
GlobalConn.ExecSQL(SQL);
LblError.Caption := '账户已被锁定,请联系管理员';
end;
Exit;
end;
{ 登录成功,打开主界面 }
LblError.Caption := '';
Application.CreateForm(TMainForm, MainForm);
Application.Run;
end;
end.
6.2 重构后的架构
现在我们把这段代码重构为三层结构:
{
============================================================
【重构后】用户登录模块 - 完整三层架构
技术栈:Pascal (Free Pascal / Delphi 兼容)
架构:数据访问层 + 业务逻辑层 + 界面层 + 适配层
============================================================
}
{
第一层:数据访问层
只负责和数据库打交道
}
unit LoginDAL;
interface
uses
Classes;
{ 登录凭证类型 }
type
TLoginCred = record
UserName: string;
Password: string;
end;
{ 登录结果类型 }
type
TLoginResult = record
Success: boolean;
Message: string;
UserID: Integer;
end;
{ 登录数据访问接口 }
ILoginRepository = interface
['{E5F6A7B8-C9D0-1234-EF12-345678901234}']
function VerifyCredentials(const ACred: TLoginCred): TLoginResult;
procedure RecordFailedAttempt(AUserName: string);
procedure ResetFailedAttempts(AUserName: string);
procedure DeactivateUser(AUserName: string);
function GetFailedAttempts(AUserName: string): Integer;
end;
{ 登录数据访问实现 }
TLoginRepository = class(TInterfacedObject, ILoginRepository)
private
FConnectionStr: string;
FMaxAttempts: Integer;
public
constructor Create(const AConnectionStr: string; AMaxAttempts: Integer);
function VerifyCredentials(const ACred: TLoginCred): TLoginResult;
procedure RecordFailedAttempt(AUserName: string);
procedure ResetFailedAttempts(AUserName: string);
procedure DeactivateUser(AUserName: string);
function GetFailedAttempts(AUserName: string): Integer;
end;
implementation
constructor TLoginRepository.Create(const AConnectionStr: string; AMaxAttempts: Integer);
begin
inherited Create;
FConnectionStr := AConnectionStr;
FMaxAttempts := AMaxAttempts;
end;
{
验证登录凭证
从数据库查询用户信息,校验密码
}
function TLoginRepository.VerifyCredentials(const ACred: TLoginCred): TLoginResult;
begin
Result.Success := False;
Result.UserID := 0;
Result.Message := '';
{
构建安全的SQL查询
使用参数化查询防止SQL注入
}
{
示例(使用参数化查询):
Query.SQL.Clear;
Query.SQL.Add('SELECT UserID, Password, IsActive FROM Users');
Query.SQL.Add('WHERE UserName = :UserName');
Query.ParamByName('UserName').AsString := ACred.UserName;
Query.Open;
if not Query.EOF then
begin
if Query.Fields['IsActive'].AsBoolean then
begin
{ 校验密码(实际应使用哈希比对) }
if Query.Fields['Password'].AsString = ACred.Password then
begin
Result.Success := True;
Result.UserID := Query.Fields['UserID'].AsInteger;
Result.Message := '登录成功';
end
else
begin
Result.Message := '用户名或密码错误';
end;
end
else
begin
Result.Message := '账户已被停用';
end;
end
else
begin
Result.Message := '用户名或密码错误';
end;
}
end;
{
记录失败登录次数
}
procedure TLoginRepository.RecordFailedAttempt(AUserName: string);
begin
{ 更新数据库中的失败次数计数器 }
{ 示例:
Query.SQL.Clear;
Query.SQL.Add('UPDATE LoginAttempts SET Count = Count + 1');
Query.SQL.Add('WHERE UserName = :UserName');
Query.ParamByName('UserName').AsString := AUserName;
Query.ExecSQL;
}
end;
{
重置失败次数
}
procedure TLoginRepository.ResetFailedAttempts(AUserName: string);
begin
{ 示例:
Query.SQL.Clear;
Query.SQL.Add('UPDATE LoginAttempts SET Count = 0');
Query.SQL.Add('WHERE UserName = :UserName');
Query.ParamByName('UserName').AsString := AUserName;
Query.ExecSQL;
}
end;
{
停用用户
}
procedure TLoginRepository.DeactivateUser(AUserName: string);
begin
{ 示例:
Query.SQL.Clear;
Query.SQL.Add('UPDATE Users SET IsActive = 0 WHERE UserName = :UserName');
Query.ParamByName('UserName').AsString := AUserName;
Query.ExecSQL;
}
end;
{
获取失败次数
}
function TLoginRepository.GetFailedAttempts(AUserName: string): Integer;
begin
Result := 0;
{ 示例:
Query.SQL.Clear;
Query.SQL.Add('SELECT Count FROM LoginAttempts WHERE UserName = :UserName');
Query.ParamByName('UserName').AsString := AUserName;
Query.Open;
if not Query.EOF then
Result := Query.Fields['Count'].AsInteger;
}
end;
end.
{
第二层:业务逻辑层
处理登录业务规则
}
unit LoginService;
interface
uses
LoginDAL;
{ 登录业务服务 }
TLoginService = class
private
FRepository: ILoginRepository;
FMaxAttempts: Integer;
{ 私有方法:检查账户是否被锁定 }
function IsAccountLocked(AUserName: string): boolean;
public
constructor Create(ARepository: ILoginRepository);
{ 执行登录 }
function DoLogin(const ACred: TLoginCred): TLoginResult;
{ 退出登录 }
procedure DoLogout;
end;
implementation
constructor TLoginService.Create(ARepository: ILoginRepository);
begin
inherited Create;
FRepository := ARepository;
FMaxAttempts := 5; { 最大尝试次数,可从配置读取 }
end;
{
检查账户是否被锁定
当失败次数达到阈值时返回True
}
function TLoginService.IsAccountLocked(AUserName: string): boolean;
begin
Result := FRepository.GetFailedAttempts(AUserName) >= FMaxAttempts;
end;
{
执行登录
完整业务流程:
1. 检查账户是否被锁定
2. 验证凭证
3. 成功则重置失败计数
4. 失败则记录失败次数
5. 达到阈值则锁定账户
}
function TLoginService.DoLogin(const ACred: TLoginCred): TLoginResult;
begin
{ 第一步:检查账户是否已被锁定 }
if IsAccountLocked(ACred.UserName) then
begin
Result.Success := False;
Result.Message := '账户已被锁定,请联系管理员';
exit;
end;
{ 第二步:验证凭证 }
Result := FRepository.VerifyCredentials(ACred);
if Result.Success then
begin
{ 第三步:登录成功,重置失败计数 }
FRepository.ResetFailedAttempts(ACred.UserName);
writeln('[LoginService] 登录成功: ', ACred.UserName);
end
else
begin
{ 第四步:登录失败,记录失败次数 }
FRepository.RecordFailedAttempt(ACred.UserName);
{ 检查是否需要锁定账户 }
if IsAccountLocked(ACred.UserName) then
begin
FRepository.DeactivateUser(ACred.UserName);
Result.Message := '账户已被锁定,请联系管理员';
end;
writeln('[LoginService] 登录失败: ', ACred.UserName);
end;
end;
{
退出登录
清理会话状态
}
procedure TLoginService.DoLogout;
begin
writeln('[LoginService] 已退出登录');
{ 在实际项目中,这里会清理会话Token、关闭数据库连接等 }
end;
end.
{
第三层:界面层
只负责展示和输入
}
unit LoginView;
interface
uses
Classes, Controls, Forms, StdCtrls, Dialogs,
LoginDAL, LoginService;
{ 登录表单 }
TLoginForm = class(TForm)
private
FService: TLoginService;
FRepository: ILoginRepository;
{ 初始化服务层 }
procedure InitServices;
{ 清理资源 }
procedure CleanupServices;
public
constructor Create; override;
destructor Destroy; override;
{ 处理登录按钮点击 }
procedure DoLoginClick;
end;
implementation
uses
SysUtils, ConfigManager;
{
构造函数:初始化表单和服务
}
constructor TLoginForm.Create;
begin
inherited Create;
{ 设置表单属性 }
Caption := '系统登录';
Width := 400;
Height := 300;
{ 初始化服务层 }
InitServices;
end;
{
析构函数:清理资源
确保数据库连接和资源被正确释放
}
destructor TLoginForm.Destroy;
begin
CleanupServices;
inherited Destroy;
end;
{
初始化服务层
通过依赖注入创建业务服务
}
procedure TLoginForm.InitServices;
var
Config: TConfigManager;
ConnStr: string;
begin
{ 获取全局配置 }
Config := TConfigManager.GetInstance;
Config.LoadFromFile('app_config.ini');
{ 从配置读取数据库连接字符串 }
ConnStr := Config.GetConnectionStr;
{ 创建数据访问层 }
FRepository := TLoginRepository.Create(ConnStr, 5);
{ 将数据访问层注入到业务服务 }
FService := TLoginService.Create(FRepository);
writeln('[LoginView] 服务初始化完成');
end;
{
清理服务层资源
}
procedure TLoginForm.CleanupServices;
begin
{ 释放资源,避免内存泄漏 }
{ 注意:接口引用需要显式释放 }
writeln('[LoginView] 资源已清理');
end;
{
处理登录按钮点击
流程简洁清晰:
1. 从界面控件获取输入
2. 调用业务服务
3. 根据结果显示结果
}
procedure TLoginForm.DoLoginClick;
var
Cred: TLoginCred;
Result: TLoginResult;
begin
{ 第一步:从界面控件获取输入 }
{ 在实际项目中,这里从控件获取 }
{ Cred.UserName := EditUserName.Text; }
{ Cred.Password := EditPassword.Text; }
{ 第二步:调用业务服务处理登录 }
Result := FService.DoLogin(Cred);
{ 第三步:根据结果显示结果 }
if Result.Success then
begin
{ 登录成功,打开主界面 }
{ Application.CreateForm(TMainForm, MainForm); }
writeln('欢迎回来, ', Cred.UserName);
end
else
begin
{ 登录失败,显示错误信息 }
ShowMessage(Result.Message);
{ 在实际项目中:LblError.Caption := Result.Message; }
end;
end;
end.
6.3 运行效果对比
重构前后的对比可以用一个简单的测试程序来展示:
{
============================================================
重构效果验证程序
技术栈:Pascal (Free Pascal / Delphi 兼容)
说明:对比重构前后的功能一致性
============================================================
}
program LoginRebuildTest;
uses
SysUtils, Classes,
LoginDAL, LoginService, LoginView;
{
测试函数:验证登录流程
确认重构后功能与重构前一致
}
procedure TestLoginFlow;
var
Repository: ILoginRepository;
Service: TLoginService;
Cred: TLoginCred;
Result: TLoginResult;
ConnStr: string;
begin
writeln('===== 登录流程重构测试 =====');
writeln;
{ 初始化数据访问层 }
ConnStr := 'Server=localhost;Database=TestDB';
Repository := TLoginRepository.Create(ConnStr, 5);
{ 初始化业务服务 }
Service := TLoginService.Create(Repository);
{ 测试1:正常登录 }
writeln('[测试1] 正常登录...');
Cred.UserName := 'testuser';
Cred.Password := 'correct_password';
Result := Service.DoLogin(Cred);
if Result.Success then
writeln(' 结果: 通过 ✓')
else
writeln(' 结果: 失败 ✗');
writeln;
{ 测试2:错误密码登录 }
writeln('[测试2] 错误密码登录...');
Cred.UserName := 'testuser';
Cred.Password := 'wrong_password';
Result := Service.DoLogin(Cred);
if not Result.Success then
writeln(' 结果: 通过 ✓ (正确拒绝了错误密码)')
else
writeln(' 结果: 失败 ✗ (错误密码居然通过了)');
writeln;
{ 测试3:连续失败锁定 }
writeln('[测试3] 连续失败锁定...');
Cred.UserName := 'locktest';
Cred.Password := 'wrong';
for i := 1 to 6 do
begin
Result := Service.DoLogin(Cred);
writeln(Format(' 第%d次尝试: %s', [i, Result.Message]));
end;
writeln;
{ 测试4:空用户名 }
writeln('[测试4] 空用户名...');
Cred.UserName := '';
Cred.Password := 'any';
Result := Service.DoLogin(Cred);
writeln(' 结果: ', Result.Message);
writeln;
writeln('===== 测试完成 =====');
end;
begin
TestLoginFlow;
writeln;
writeln('按任意键退出...');
Readln;
end.
七、应用场景分析
这种分层重构的方法最适合以下几类场景。
第一类是老旧的企业管理系统。很多公司还在用十几前的Pascal写的ERP、财务系统、库存系统。这些系统跑在特定版本的Delphi或者Windows版本上,不能随便换平台。用分层重构的方式,可以先把数据库操作和业务逻辑拆出来,让后续维护变得容易,同时不影响系统当前的运行。
第二类是硬件集成类项目。很多工控系统用Pascal写,里面有大量硬件通信代码。这些硬件库往往已经停止维护,不能直接替换。用适配层把它们包起来,外面的代码就可以慢慢迁移到新的框架上。
第三类是需要跨平台迁移的项目。原来跑在Windows上的Pascal应用,现在需要适配Linux或macOS。通过桥接模式,可以在不修改业务代码的情况下切换到新的UI框架,比如Lazarus。
第四类是团队能力升级的项目。原来团队只懂Pascal,现在新来了Python或Java的开发者。分层重构可以把业务逻辑独立出来,让新语言团队通过接口调用来参与开发,不需要完全读懂Pascal。
八、技术优缺点
优点
分层重构最大的好处是风险可控。你不是在推倒重建,而是一层一层地拆,每一步都可以验证,出了问题可以回退。老系统跑得好好的,你不会突然把它搞崩。
第二是团队效率提升。重构之后,新成员接手项目不需要同时理解界面、业务和数据库。他只需要关注自己负责的那一层。改界面的人不用管SQL,改数据库的人不用管按钮长什么样。
第三是测试变得可行。老代码因为全局变量和硬编码,根本没法写单元测试。分层之后,业务逻辑层不依赖界面和数据库,可以直接在测试代码里创建实例来验证逻辑。
第四是未来迁移的灵活性。今天你用Delphi,明天想换Free Pascal或者Lazarus,业务逻辑层一行不改。今天你用MySQL,明天想换PostgreSQL,数据访问层改一下就行。
缺点
分层重构不是没有代价的。首先是时间成本,拆层需要理解老代码的结构和逻辑,这个过程本身就需要不少时间。如果项目特别大、代码特别烂,光梳理阶段就可能花上几周。
其次是学习成本。团队里的人需要理解分层架构、依赖注入、接口设计这些概念。如果团队之前一直写"大泥球"风格的代码,接受新架构需要时间。
还有一个实际问题是过渡期的复杂度。重构不是一天完成的,中间会有很长一段时间,一部分代码是老风格,一部分是新风格,两者交织在一起。这种"新旧共存"的状态可能比原来的大泥球更让人困惑。
九、注意事项
第一,不要一次改太多。重构应该小步快跑,一次只改一个模块,改完就测试,确认没问题再改下一个。不要试图一次把整个项目重构完。
第二,始终保留回退能力。每改完一个模块,确保老代码还能跑。万一新代码有问题,随时可以切回老版本。
第三,配置文件不要硬编码。在重构过程中,把所有配置项(数据库连接、文件路径、第三方服务地址等)提取到配置文件中,用配置管理器统一加载。
第四,注意内存管理。Pascal是手动内存管理的语言,重构时很容易因为对象生命周期管理不当导致内存泄漏。特别注意接口引用和对象引用,确保在合适的时候释放。
第五,做好文档记录。重构的过程中,把每个模块的职责、接口定义、依赖关系都记录下来。不要改完就忘,过几天自己都记不清为什么这么拆了。
第六,保持向后兼容。在重构过程中,老接口尽量保留,可以在内部用新代码实现。不要让外部调用者因为你内部重构了而不得不改代码。
十、文章总结
历史遗留Pascal项目的重构,本质上是一场"外科手术"。核心思路是分层:把数据访问、业务逻辑、界面展示拆成独立的层,每层只关心自己的事。通过接口定义契约,通过依赖注入管理对象关系,通过适配层桥接新旧代码。
这个过程中,你不需要推翻重建,也不需要推倒一切。你只需要一层一层地拆,拆完一层就验证一层,确保功能不丢、性能不降。最终你得到的是一套结构清晰、易于维护、方便测试的现代化代码,而老系统的所有业务逻辑都完整地保留了下来。
记住几个关键原则:小步快跑、保留回退、做好文档、保持兼容。重构不是一次性的壮举,而是一个持续的演进过程。只要你愿意一点一点地改善,老项目终会焕发出新的活力。
评论
围绕“历史遗留Pascal项目的架构优化思路:如何重构适配现代应用开发场景”参与讨论