标签归档:开发

Delphi 7 操作 MySQL 数据库一例

Delphi 7 在现在生产环境中的开发基本已经绝迹,所剩不多的人员也主要用于旧系统的维护与小功能的升级,还有很少的一部分人沿用旧有的技能线升级到 XE 等后续版本继续完成日常的开发工作。更多的系统要么升级到了 .NET 体系 ,要么就是用了 Java 体系。但不可否认的是在 Windows 桌面软件的开发的某些场合中,其实用它还是蛮快的。

一个项目最初的需求跟数据库毫无关系,在考虑需求的基础上选择了 Delphi 7 ,这样发布一个独立的可执行程序,依赖很少,使用比较方便,但需求总是变化的,有了使用数据库的需求,把单机的应用变成了一个网络的应用,以往在 Windows 系统上一般用微软自家的 SQL Server 多一些,但现在机器上没有,MySQL 倒是现成的,于是决定用它了。

Delphi 7 开发的快速性在于其基于组件的丰富性,在早期好像用过 MySQL,用的是基于 ODBC 的 MySQL 驱动,但相当不好用,具体不好用的细节倒是忘记了,想着这么些年过去了,有没有与时俱进的组件呢,搜了一下还真发现了一个,名称是 ZeosLib ,看介绍挺强大,几乎支持所有的数据库,用了一下除了有一点通常都会出现的编码的坑之外还不错,把基本的使用做个记录归档以便以后使用。

具体的操作系统是 Windows 10 家庭中文版,版本号是 20H2,操作系统内部版本是 19042.928,用的数据库是 MySQL Community Server 8.0.17 for Win64 on x86_64 。

我用的是 ZeosLib 是 7.2.10,可通过网址 https://sourceforge.net/projects/zeoslib/ 下载获得。

1. 组件的安装

这里只说 Delphi 7 的安装,下载组件压缩文件后解压缩 zeosdbo-7.2.10-stable.zip ,解压缩后的目录下有三个文件夹和两个文件,在其中的找到 packages 文件夹,进入该文件夹后找到 delphi7 文件夹进入,鼠标左键双击 ZeosDbo.bpg 文件,在 Delphi 7 打开后选择菜单栏 Project 中的 Compile All Projects 菜单项,编译完成后选择 OK 按钮。在 Library Path 中增加该组件的路径,路径指向 delphi7 文件夹下的 build 文件夹。在 delphi7 文件夹中找到 ZComponentDesign.dpk 文件,鼠标双击打开,选择 install ,这样组件就会安装好了。

2. 数据库的访问与操作

MySQL 数据库的安装就不再详述了,在 Windows 系统上安装也比较简单,通常一路「下一步」即可安装完毕。我们这里写一个简单的登录示例,成功登录后记录一条登录信息,大概梳理一下流程。

1. 用户打开程序;
  1.1. 如果发现没有数据库配置信息,启动数据库配置界面;
  1.2 如果有数据库配置信息,启动用户登录界面;
2. 用户完成数据库连接配置
	……
3. 用户完成登录
	……
4. 记录登录信息

数据库的准备工作

# 数据库脚本

# 创建 examples 数据库
CREATE DATABASE IF NOT EXISTS `examples` 
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

# 创建用户数据表
CREATE TABLE `users` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `loginname` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
  `pwd` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
  `isadmin` tinyint(1) NOT NULL DEFAuLT '0',
  PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

# 创建用户登录信息表
CREATE TABLE `user_logins` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `login_desc` varchar(150) COLLATE utf8mb4_general_ci NOT NULL,
  `login_time` datetime NOT NULL,
  PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

在 Delphi 7 中新建一个 Application ,再添加一个 Data Module 和两个 Form,将 Data Module 命名为 DataBox,将 Application 自带的 Form 和添加的两个 Form 分别命名为 frm_main 、 frm_login 、frm_dbset,保存工程文件为 LoginExample ,代码如下。

LoginExample.dpr 工程文件

program LoginExample;

uses
	Forms,
	u_main in 'u_main.pas' {frm_main},
	u_databox in 'u_databox.pas' {DataBox : TDataModule},
	u_login in 'u_login.pas' {frm_login},
	u_dbset in 'u_dbset.pas' {frm_dbset};

{$R *.res}

begin
	Application.Initialize;
	Application.CreateForm(TDataBox, DataBox);
	if show_FormLogin then
	begin
		Application.CreateForm(Tfrm_main, frm_main);
	end;
	Application.Run;
end.

u_databox.pas 单元文件

unit u_databox;

interface

uses
	Windows,SysUtils, Classes, ZAbstractConnection, ZConnection, DB,
  ZAbstractRODataset, ZAbstractDataset, ZDataset,Registry,
  Forms, DCPcrypt2, DCPsha256,Dialogs;

type
  TDataBox = class(TDataModule)
    ZQOper: TZQuery;
    ZC: TZConnection;
    ZQLogs: TZQuery;
    DCP_sha2561: TDCP_sha256;
    procedure DataModuleCreate(Sender: TObject);
  private
    { Private declarations }
    FDBHost : String;
    FDBPort : Integer;
    FDBName : String;
    FDBConUser : String;
    FDBConPwd : String;
  public
    { Public declarations }
    procedure GetDBConStr;
    function GetSha256(s:String):String;
  end;

var
  DataBox: TDataBox;
  OperID: Integer;
  Operator: string;
  LogsID:string;
  CurDir:string;
  isAdmin:Boolean;

procedure WriteLogs(sOper:Integer;funid:Integer;funName:string);


implementation

uses u_dbset, DateUtils;

{$R *.dfm}

procedure WriteLogs(sOper:Integer;login_desc:string);
begin
  with DataBox.ZQLogs do
  begin

    Close;
    SQL.Clear;
    SQL.Add('INSERT INTO user_logins(user_id,login_desc,log_time) VALUES(:uid,:desc,:logtime)');
    ParamByName('uid').Value:=sOper;
    ParamByName('desc').Value:=funName;
    ParamByName('logtime').Value:=Now;
    ExecSQL;
  end;
end;


procedure TDataBox.GetDBConStr;
var
  myReg : TRegistry;
begin
  myReg := TRegistry.Create;
  with myReg do
  try
    RootKey := HKEY_CURRENT_USER;
    if OpenKey('SOFTWARE\ExampleProg\DBConParam\', False) then
    begin
      FDBHost := ReadString('DBIP');
      FDBPort := StrToInt(ReadString('DBPort'));
      FDBName := ReadString('DBName');
      FDBConUser := ReadString('DBUser');
      FDBConPwd := ReadString('DBPwd');
    end;
  finally
    myReg.CloseKey;
    myReg.Free;
  end;
end;

procedure TDataBox.DataModuleCreate(Sender: TObject);
begin
  GetDBConStr;
  if FDBHost = '' then
  begin
    Application.CreateForm(TfrmDbSet, frmDbSet);
    frmDbSet.ShowModal;
  end;
  GetDBConStr;
  with ZC do
  begin
    Disconnect;
    Protocol := 'mysql';
    LibraryLocation := ExtractFilePath(Application.ExeName)+'libmysql.dll';
    HostName := FDBHost;
    Port := FDBPort;
    User := FDBConUser;
    Password := FDBConPwd;
    Database := FDBName;
    Connect;
  end;
  with ZQOper do
  begin
    Close;
    SQL.Text := 'SELECT Count(*) as OperCount FROM users';
    Open;
    if FieldByName('OperCount').Value = 0 then
    begin
      Close;
      SQL.Text := 'INSERT INTO users(loginname,name,pwd,isAdmin) VALUES(:loginname,:name,:pwd,:isAdmin)';
      ParamByName('loginname').Value := 'admin';
      ParamByName('name').Value := 'admin';
      ParamByName('pwd').Value := GetSha256('admin'); //sha256 admin
      ParamByName('isAdmin').Value := 1;
      try
        ExecSQL;
      finally
        Close;
      end;
    end;
  end;
end;

function TDataBox.GetSha256(s: String): String;
var
  Hash : TDCP_sha256;
  Digest : array[0..31] of byte;  
  Source : String;
  i : Integer;
  str1 : String;
begin
  Source := s;  //get s string sha256

  if Source <> '' then
  begin
    Hash := TDCP_sha256.Create(nil);  //create the hash
    Hash.Init;
    Hash.UpdateStr(Source);
    Hash.Final(Digest);
    str1 := '';
    for i:=0 to 31 do
      str1 := str1 + IntToHex(Digest[i],2);
  end;
  Result := str1;
end;

end.

u_dbset.pas 单元文件

unit u_dbset;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, Mask, Registry,ZAbstractConnection, ZConnection;

type
  TfrmDbSet = class(TForm)
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    Label5: TLabel;
    Bevel1: TBevel;
    Label6: TLabel;
    lblStatus: TLabel;
    edt_ServerName: TEdit;
    edt_DBName: TEdit;
    edt_ConnUser: TEdit;
    edt_Pwd: TEdit;
    BitBtn1: TBitBtn;
    BitBtn2: TBitBtn;
    BitBtn3: TBitBtn;
    Panel1: TPanel;
    Label1: TLabel;
    Image1: TImage;
    Panel2: TPanel;
    Label1: TLabel;
    edt_Port: TEdit;
    ZC: TZConnection;
    procedure FormKeyUp(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure BitBtn3Click(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure BitBtn1Click(Sender: TObject);
    procedure ZCAfterConnect(Sender: TObject);
    procedure ZCAfterDisconnect(Sender: TObject);
    procedure RzBitBtn2Click(Sender: TObject);
  private
    { Private declarations }
    DBConStr: string;
    FMacineName: string;
    FDBName: string;
    FPwd: string;
    FConnUser: string;
    procedure SetDBConStr;
  public
    { Public declarations }
  end;

var
  frmDbSet: TfrmDbSet;

implementation

uses u_databox,comobj;

{$R *.dfm}

{ TfrmDbSet }



procedure TfrmDbSet.SetDBConStr;
var
  myReg:TRegistry;
begin
  myReg:=TRegistry.Create;
  with myReg do
  try
    RootKey:=HKEY_CURRENT_USER;
    if OpenKey('SOFTWARE\ExampleProg\DBConParam\',True) then
    begin
      WriteString('DBIP',edt_ServerName.Text);
      WriteString('DBPort',edt_Port.Text);
      WriteString('DBName',edt_DBName.Text);
      WriteString('DBUser',edt_ConnUser.Text);
      WriteString('DBPwd', edt_Pwd.Text);
    end;
  finally
    myReg.CloseKey;
    myReg.Free;
  end;
end;

procedure TfrmDbSet.FormKeyUp(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if (Key = VK_F8) and (ssCtrl in Shift) then
  begin
    edt_ConnUser.Color := clWhite;
    edt_ConnUser.Enabled := True;
    edt_ConnUser.SetFocus;
  end;
end;

procedure TfrmDbSet.BitBtn3Click(Sender: TObject);
begin
  Close;
end;

procedure TfrmDbSet.FormShow(Sender: TObject);
begin
  edt_ServerName.SetFocus;
end;

procedure TfrmDbSet.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  ZC.Disconnect;
  Action:=caFree;
  frmDbSet:=nil;
end;

procedure TfrmDbSet.BitBtn1Click(Sender: TObject);
begin
  with ZC do
  begin
    Disconnect;
    Protocol := 'mysql';
    LibraryLocation := ExtractFilePath(Application.ExeName)+'libmysql.dll';
    HostName := edt_ServerName.Text;
    Port := StrToInt(edt_Port.Text);
    User := edt_ConnUser.Text;
    Password := edt_Pwd.Text;
    Database := edt_DBName.Text;
    Connect;
    RzBitBtn2.Enabled := True;
  end;
  lblStatus.Caption := '连接状态:测试连接成功,已连接!';
end;

procedure TfrmDbSet.ZCAfterConnect(Sender: TObject);
begin
  lblStatus.Caption := '连接状态:已连接!';
end;

procedure TfrmDbSet.ZCAfterDisconnect(Sender: TObject);
begin
  lblStatus.Caption := '连接状态:未连接!';
end;

procedure TfrmDbSet.BitBtn2Click(Sender: TObject);
begin
  SetDBConStr;
  Close;
end;

end.

u_login.pas 单元文件

unit u_login;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, DB, ZAbstractRODataset, ZAbstractDataset, ZDataset;

type
  Tfrm_login = class(TForm)
    Label1: TLabel;
    edt_pwd: TEdit;
    cbo_username: TComboBox;
    Label2: TLabel;
    btnLogin: TButton;
    ZQUser: TZQuery;
    procedure FormCreate(Sender: TObject);
    procedure btnLoginClick(Sender: TObject);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure cbo_usernameKeyPress(Sender: TObject; var Key: Char);
    procedure edt_pwdKeyPress(Sender: TObject; var Key: Char);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure ZQUserAfterOpen(DataSet: TDataSet);
    procedure edt_pwdEnter(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

function Show_FormLogin:Boolean;

implementation

uses u_databox;

var
  PasswordOK : Boolean;

{$R *.dfm}

function Show_FormLogin:Boolean;
var
  frm_login : Tfrm_login;
begin
  PasswordOK := False;
  frm_login := Tfrm_login.Create(Application);
  try
    frm_login.ShowModal;
  finally
    frm_login.Free;
  end;
  Result := PasswordOK;
end;

procedure Tfrm_login.FormCreate(Sender: TObject);
begin
  ZQUser.Close;
  ZQUser.Open;
end;

procedure Tfrm_login.btnLoginClick(Sender: TObject);
begin
  if ZQUser.Locate('loginname;pwd', 
	VarArrayOf([cbo_username.Text, DataBox.GetSha256(edt_pwd.Text)]), [loCaseInsensitive]) then
  begin
    OperID := ZQUser.FieldByName('id').AsInteger;
    Operator := cbo_username.Text;
    isAdmin := (ZQUser.FieldByName('isAdmin').AsInteger=1);
    Application.MessageBox(PChar(Operator + '登录成功'), '登录提示', MB_OK+MB_ICONINFORMATION);
    PasswordOK := True;
    WriteLogs(OperID,'登录');  
    if PasswordOK then
      Close;
  end
  else
    Application.MessageBox('用户名或密码不正确,登录失败,如忘记密码,请联系管理员!', '错误提示', MB_OK+MB_ICONWARNING);
end;

procedure Tfrm_login.FormCloseQuery(Sender: TObject;
  var CanClose: Boolean);
begin
  if not PasswordOK then
  begin
    CanClose := Application.MessageBox('你真的要退出该软件吗?', '信息提示', MB_YESNO+MB_ICONQUESTION)=IDYES;
    WriteLogs(OperID, '退出系统');
  end;
end;

procedure Tfrm_login.cbo_usernameKeyPress(Sender: TObject; var Key: Char);
begin
  if Key = #13 then
  begin
    edt_pwd.Text := '';
    edt_pwd.SetFocus;
  end;
end;

procedure Tfrm_login.edt_pwdKeyPress(Sender: TObject; var Key: Char);
begin
  if Key = #13 then
  begin
    btnLoginClick(Self);
  end;
end;

procedure Tfrm_login.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Action := caFree;
end;

procedure Tfrm_login.ZQUserAfterOpen(DataSet: TDataSet);
begin
  cbo_username.Items.Clear;
  with ZQUser do
  begin
    First;
    while not Eof do
    begin
      cbo_username.Items.Add(FieldByName('loginname').AsString);
      Next;
    end;
  end;
end;

procedure Tfrm_login.edt_pwdEnter(Sender: TObject);
begin
  edt_pwd.Text := '';
end;

end.

完成上述的步骤,这个小的 Demo 就算完成了,有三个地方需要注意一下:

第一,我看我的 IED 中安装了 DCPcrypt2 加密解密组件,随手引用了对密码做哈希处理,这部分如果用于练习的时候可以去掉。

第二,访问 MySQL 数据库需要动态链接库 libmysql.dll ,这个需要注意一下,不管你用 32 位的操作系统还是 64 位的操作系统,同时它也跟你安装的 MySQL 是 32 位的还是 64 位的也没有关系,在 Delphi 7 中使用 MySQL 的时候只能使用这个动态链接库的 32 位版本。

第三,编码问题,Delphi 7 会碰到编码问题,比如向 MySQL 数据库中写入中文会显示乱码,此时在 TZConnectionProperties 中添加 codepage=gbk 。另一种处理方式是在使用 TZQuery 等时,在运行 SQL 插入数据语句前,要先运行 set names gbk

begin
{这里的 zq1 是一个 TZQuery 控件}
	with zq1 do
	begin
		Close;
		SQL.Text := 'set names gbk';
		ExecSQL;
		SQL.Text := 'INSERT INTO Test(UName) VALUES("张三")';
		ExecSQL;
	end;
end;

因为是直接在项目中使用后,手工直接在 Notion 中码出来的,并没有写这个 Demo ,所以无法附上 Demo 的源码,对于现在还使用 Delphi 的人来说应该是比较简单的,我其实更多的是给自己做个笔记。但如果有任何问题,可直接通过「关于我」中提供的联系方式与我联系。

– EOF –

初识 Python(二):字符串

继续学习 Python 的基础。字符串是编程语言中经常处理的数据类型,Python 中的字符串可以使用单引号作为开始和结束的起止符表示,也可以使用双引号作为开始和结束的起止符表示,不少其他的编程语言也采用这样的方式来表示字符串。

对于一个字符串中本来就包含有单引号时,使用双引号会方便一些,不用使用转义符,其他两者并无不同,使用主要看个人喜好,我比较喜欢使用双引号。

转义符在各个编程语言中都有,反斜杠后面跟需要转义的字符,常见的需要转义的字符有单引号、双引号、制表符 「\t」、换行符 「\n」等等。

除此之外, Python 还有三引号作为字符串的起止符,三引号常用于多行字符串,三个单引号和三个双引号都可以,三引号起止符之间的字符串中的所有空格、引号、制表符和换行符都会被当作字符串的一部分。三引号也可以作为多行注释。

在字符串起始的引号前加小写字母「 r 」,将忽略该引号中字符串里所有的转义字符,也就是你引号中的字符串是什么它就是什么,同样的支持单引号、双引号、三引号。

对字符串的操作就像列表一样,可以用下标,同样也可以对字符串实行切片操作,切片操作使用「字符串[m:n]」来完成。

比较常见的字符串方法。

字符大小写转换相关的方法 upper()、lower()、isupper()、islower(),其中前两个是能够将字符串全部变成大写和全部变成小写的两个方法,需要注意的是调用该方法并不会改变原有的字符串,而是生成了新的字符串;后两个方法分别判断字符串是否全部为大写或全部为小写,返回 True 或 Flase。

isupper() 和 islower() 还有一些弟弟妹妹们:

isalpha() 如果字符串中只包含字母并且不为空,对字符串调用这个方法会返回 True 。

isalnum() 如果字符串中只包含字母和数字并且不为空,对字符串调用这个方法会返回 True 。

isdecimal() 如果字符串中只包含数字并且不为空,对字符串调用这个方法会返回 True 。

istitle() 如果字符串每个词的首字母大写后面为小写并且不为空,对字符串调用这个方法会返回 True 。

字符串的连接和拆分是字符串编程中常见的操作,Python 中的字符串拆分跟很多其他编程语言类似,都是使用split() 方法,使用 split() 可以将字符串拆分成列表。字符串连接的方法使用 join() ,这让我一下想起了 SQL 中的left join 和 right join ,对字符串使用 join() 可以连接字符串。

去除字符串前后的空格也是字符串编程中常见的,Python 中使用 strip() 、lstrip() 、 rstrip() 方法取出字符串前后的空格,strip 去除首尾空格,lstrip 去除字符串首部空格,rstrip 去除尾部空格。

Python 还提供了字符串对齐的方法,这个比较有意思,分别是用于左对齐的 ljust(),用于右对齐的 rjust() 何由居中对齐的 center()。 对齐方法有两个参数,第一个参数是对齐的长度,第二个参数是用于填充对齐中空白的字符。

还有比较字符串开头和结尾是否一致的方法,分别是 startswith() 和 endswith(),可以检查字符串的头尾是否和预期的一致。

Python 中提供了不少的字符串的方法,这门编程语言对于文本处理应该是比较强大的,再配合起第三方的模块应该比较强悍,继续学习。

相关阅读

初识 Python(一)

本文首发于我的微信公众账号「时间易逝」,欢迎订阅我的微信公众账号
在微信中搜索「doevents」或用微信扫描页面右上方二维码可订阅我的微信公众账号

初识 Python(一)

人生苦短,该学学 Python ,花时间看了看 Python 基础性的内容,做个记录。

Python 是一门解释性、面向对象、动态数据类型的高级程序设计语言。

代码块间无花括号,也没有诸如 begin…end 的开始结束分隔符,每条语句也不需要用分号作为结束,以自然换行为一条语句。同一层次的代码块用缩进来区分,简洁干净。

基础数据类型同其他程序设计语言,如整型、浮点型、字符串、布尔型。

操作符也很类似,其中 * 操作符比较有意思,除了作为乘法操作符外,在用于字符串和整型值时,这个操作符就变成了字符串复制符。

比如在命令行 >>> 后输入 China * 3 回车键后会复制 China 这个字符串 3 次,输出显示。

操作符 ** 可以用于求指数,操作符 // 用于整除。

流程控制跟其他程序设计语言差不多,if 、while 、for 这些都是有的。

用 def 关键字后跟函数名和参数完成函数的定义,比如定义一个函数 say_hello ,打印输出一行字符串。

def say_hello(name):
print('Hi, ' + name)

#调用 say_hello 函数
say_hello('Eric')

江湖中之所以有「人生苦短,我用 Python」的说法,是因为 Python 有大量的库可供使用,不必重复制造轮子,使用 import 关键字引入这些库的模块即可,好像 Java 也是这么干的 ,也有些类似 C# 的 using 。

异常处理使用 try…except 来完成。

有三个数据类型值得注意,它们分别是列表、元组、字典。

列表「list」是一个值的序列,有点类似其他程序设计语言中的数组,是一种以下标 0 开始的有序值的集合,这些有序值称为列表项。针对列表有一些操作,如以切片的方式获得子列表,增加列表项,插入列表项,删除列表项,排序等。可以视列表为一个「可变的」数据类型。列表使用方括号 []定义。

元组「tuple」跟列表很类似,所不同的是元组用圆括号()定义,另外比较重要的是元组初始化后是不可改变的。

字典「dict」也是一种多值的集合,与列表相比,字典的索引「下标」可以使用多种不同的数据类型,在字典中索引「下标」被称为键,字典是一种键值对的多值集合数据类型。另外,字典是非有序的。字典使用花括号{}定义。

以上是对 Python 的一些初步认识,学习一门程序设计语言唯一的方法就是将这门程序设计语言在实际项目中投入使用,通过使用中的刻意练习获得提高。把一门程序设计语言学到用它可以进行专职工作可能需要不短的时间,但是学会它并简单的写一点能够替代重复性工作的代码还是相对较容易的。

本文首发于我的微信公众账号「时间易逝」,欢迎订阅我的微信公众账号
在微信中搜索「doevents」或用微信扫描页面右上方二维码可订阅我的微信公众账号

怎样提高开发速度

软件产品想在市场上获得竞争力,开发速度是克敌制胜的重要因素之一。基于此,在产品开发的过程中,速度是每个团队都会重点关注的指标之一,每个团队都在极力想办法提升产品开发的速度,以便在市场的赛道上奔跑中领先那么一半个身位。从我自己的角度来看,我会尝试关注以下 6 方面的工作,这可能会对团队提高开发速度带来一些帮助。

及时清理技术债务,技术债务对团队生产率的影响是非常大的。技术债务很容易产生,产生后往往又不是一个能够快速修正的工作,当技术债务积累过多时,常常会花费数月乃至数年来偿还。避免产生技术债务需要优先考虑代码质量,实际中很多团队为追求开发速度往往并不注重代码的质量,或许开始能够在赛道上领先,但在整个赛程中,随着积累的技术债务,开发速度会越来越慢,直到花费巨大的成本偿还这些债务,这个阶段往往也是模仿者/追随者弯道超车的好时机。一开始就保持时时清理技术债务从整体是会提高开发速度的,毕竟开发是伴随着整个产品的生命周期而时刻在进行的一项工作。

技术债务并不是那么容易清理,依赖于技能水平与经验,但总可以从最简单的开始。比如,当发现有重复性代码的时候,可能就产生了技术债务,对这些重复的代码进行重构,既避免了技术债务的生成,又提高了自己的技能。伴随着勤快处理问题和系统演进中的即时重构除了能够降低技术债务的累积,也是提高个人竞争力的一条道路。

提高客户参与度,开发人员往往不具备客户领域内的知识,碰到需要客户解答的领域内问题时,要么等待,要么猜测,这么一来一往之中,会拖延开发的速度。提高客户参与度,有能够随时回答开发人员问题的客户,无疑会提高开发速度。

让客户参与进来往往并不那么容易,在这方面往常中采取的措施是引入行业领域内的专家,或者把整个团队进驻到客户所在的场地,通过这种方式来提高客户参与度往往会增加一定的成本,但相比缺乏领域知识造成的开发效率低下和不专业性无疑是值得的。另外一种做法是用专人往复于客户与团队之间传递这些领域内的知识,效果取决于这个专人横向的认知广度和纵向认知的深度,在以前可能由项目经理承担这个角色,现在更多会设置产品经理岗位。

精力充沛的工作,疲倦会带来成本高昂的错误,同时疲倦也会让人难以全力以赴地工作,长时间的加班是极不可取的。短暂的透支一下精力是可能的,长期透支则代表应该寻找问题的根源了。试试在单位时间的使用上投入更多的关注,这样或许会更好一些。加班普遍的现象有一部分原因是实际上投入工作的时间并没有那么长,拉长的时间线在补充了实际工作时间的同时很容易让人精力不济。

减少对开发人员的干扰,尽量将非开发的工作交给其他能完成的人来完成,减少不必要的会议,在产品开发进行中时,跟产品开发无关的事交由另外的人处理,比如行政事务类的事由专门的人负责。另一方面的干扰来自自我,面对众多的干扰源,要求我们自律一些是重要的,这方面一方面需要团队的文化制度塑造个人,另一方面选择合适的人可能是更合适的。

尽量提供优质的资源,一台电脑在手,天下我有。很大程度上开发人员的主要资源需求就是设备,不要让开发人员抱怨电脑慢、内存不足…,给他们提供优质的资源。在这些资源上省钱是毫无意义的。这方面我们可以简单的算笔账,如果每天因为设备耗去每个开发人员半小时,算算一年损耗的时间和因损耗减少的产出。

尽量谨慎地增加开发人员,除非团队人员严重不足,而且有经验的丰富员工可随时拿来用,否则开发人员的增加并不会带来速度的提升,项目往往还会进一步延期。假如开发一个产品需要 10 人月,那么并不能增加到 20 个人就能半月完成,这应该是产品开发中的常识。

将这些方法应用于开发过程中,随着时间推进,开发速度应该会有显著提高,从而使团队具有「小步快跑,试错迭代」的能力。当然有一个清晰的要达成的目标是最根本的,这就好比打仗,当团队知道为什么战斗时,具备这种能力的团队,往往每次迭代都会交付一个好结果。

本文首发于我的微信公众账号「时间易逝」,欢迎订阅我的微信公众账号
在微信中搜索「doevents」或用微信扫描页面右上方二维码可订阅我的微信公众账号