项目迭代开发手记--文件分割存储用例的实现过程(3)
上午的迭代2完成后,我们获得了一个有完整压缩流功能的实现代码,这次迭代完成的代码是可用的,我们在迭代2中完成了我们既定的任务。在下午的小组讨论中,我们继续考虑下一阶段的迭代目标,由于没有决定图档文件的格式,我们决定先不考虑图片格式的问题,先实现文件的分割功能。文件的分割主要是考虑当图档文件太大的时,数据库提交性能会变得非常慢,分割的目的就是改进提交的性能。
迭代3:
对向数据库提交的二进制流进行分割压缩;那么从数据库提取的时候要进行解压和拼接操作,以获得原始图档数据。
在分割功能的设计和编码前,我们重新审视了上午的代码——那个压缩类TLoadBinaryDataToDB,发觉该类似乎职责太多,它要负责把文件装载成流,然后才对流进行压缩和解压缩,我们发现UnCompressStream函数有更好的通用性,只要是压缩的流就可以对其进行解压。而压缩功能在这个类里似乎只能对通过文件装载的流进行压缩,如果流是以另一种形式获得的,不是以文件装载的形式,那么我们不知道该如何对该流进行压缩。这里似乎违背了功能单一的职责,类既负责了流的装载,又负责流的压缩;于是我们对该类进行了重构已获得结构更好的的类,以增加类的重用性。
重构后的类只有两个公用的方法 CompressStream 和 UnCompressStream 它们都已流为参数,通过对传入流的处理来实现压缩和解压缩功能。
procedure TCompressStream.CompressStream(var stream: TMemoryStream);
var
iSize: Integer;
lDestStream: TMemoryStream;
lCompressionStream: TCompressionStream;
begin
lDestStream := TMemoryStream.Create;
lCompressionStream := TCompressionStream.Create(clMax, lDestStream);
try
iSize := stream.Size; //获得图像流的原始尺寸
stream.SaveToStream(lCompressionStream); //将原始图像流进行压缩,
// lDestStream中保存着压缩后的图像流
lCompressionStream.Free;
stream.Clear;
stream.WriteBuffer(iSize, SizeOf(iSize)); //写入原始图像的尺寸
stream.CopyFrom(lDestStream, 0); //写入经过压缩的图像流
finally
lDestStream.Free
end;
end;
解压缩函数
procedure TCompressStream.UnCompressStream(var stream: TMemoryStream);
var
DecompressionStream: TDecompressionStream;
Buffer: PChar;
Count: Integer;
begin
stream.ReadBuffer(Count, SizeOf(Count));
GetMem(Buffer, Count); //根据图像尺寸大小为将要读入的原始图像流分配内存块
DecompressionStream := TDecompressionStream.Create(stream);
try
DecompressionStream.ReadBuffer(Buffer^, Count); //将被压缩的图像流解压缩,
//然后存入 Buffer内存块中
stream.Clear;
stream.WriteBuffer(Buffer^, Count); //将原始图像流保存至 stream流中
stream.Position := 0;
finally
FreeMem(Buffer); // 释放内存
end;
end;
经过重构后,类TCompressStream无疑提高了重用性,同时有更好的结构。除去了把文件装载成流的功能后,TCompressStream职责变得更单一了。它对已任何形式获得得的流都可以进行压缩和解压缩。完成TLoadBinaryDataToDB重构我们开始考虑对流进行分割功能的实现。
在假定一个流被分割成5份,那么拼接时就要有一个顺序我们考虑在数据库增加一个顺序的字段来保存流各个块之间的分割顺序。
字段名
字段类型
字段长度
字段说明
FID
Number
主键
F_NAME
VarChar2
50
文件名称
F_SERIAL
Number
文件分割顺序号
F_BINARY_DATA
Long Row
二进制数据
同样我们考虑把这个功能封装在一个类里面。我们实现了一个叫TStreamIncise的类,在设计这个类时,我们为了更好的增加对这类要设计成什么样子进行了很好的讨论,首先我们模拟了如何使用该类。
for I := 0 to IncisedCount - 1 do
begin
StreamIncise.GetInciseStream(lStream); //获得分割流
ClientDataSet2.Append;
ClientDataSet2.FieldByName('F_ID').Value := I; //取序列号
ClientDataSet2.FieldByName('F_NAME').Value := FFileFullName;
ClientDataSet2.FieldByName('F_SERIAL').Value := I; // 取每次分割的序列号
lCompressionStream.CompressStream(lStream);
(ClientDataSet2.FieldByName('F_BINARY_DATA')
as TBlobField).LoadFromStream(lStream);
ClientDataSet2.Post;
end;
我们用代码估计了类的调用方式,通过这样的模拟代码我们获得了以下信息
1) 要获得文件的被分割数,就是说如果使用上面的模拟代码,我们必须先获得流的分割数。
2) TStreamIncise流在执行前先获得要处理流,同时设定分割块的大小。
如图:
我们用FInciseSize 来保存分割快的大小值,FStreamSize 保存流的大小值,FRemainSize保存每次分割后的剩余值。FInciseSize 在初始化函数 Create 中初始化。
FInciseSize := 50000; //设置分割的大小
LoadFromStream 把原始的流装载过来。
procedure TStreamIncise.LoadFromStream(stream: TMemoryStream);
begin
FMemoryStream := stream; // 保存一个流的引用
FStreamSize := stream.Size;
FRemainSize := FStreamSize;
end;
GetIncisedCount 获得装载的原始流要被分割的数量。
function TStreamIncise.GetIncisedCount: Integer;
begin
Result := FStreamSize div FInciseSize + 1;
end;
SetStreamDefault 用来把获得流设置到初始位置。
procedure TStreamIncise.SetStreamDefault;
begin
if Assigned(FMemoryStream) then FMemoryStream.Position :=0;
end;
核心的函数是GetInciseStream 通过调用它用户获得分割好后的流。
procedure TStreamIncise.GetInciseStream(inciseStream: TMemoryStream);
var
iMaxError: Integer;
Count: Integer;
Buffer: PChar;
begin
Count := GetBufferCount;
GetMem(Buffer, Count);
try
FMemoryStream.ReadBuffer(Buffer^, Count);
InciseStream.Clear;
inciseStream.WriteBuffer(Buffer^, Count);
InciseStream.Position := 0;
FRemainSize := FRemainSize - Count;
finally
FreeMem(Buffer);
end;
end;
这里GetBufferCount 每次返回分割块的大小,当剩余的流大小不够5000 时它返回剩下流的长度。
function TStreamIncise.GetBufferCount: Integer;
begin
Result := FInciseSize;
if FRemainSize < FInciseSize then
Result := FRemainSize;
end;
最终我们获得了一个可以这样调用的分割类:
procedure TForm1.Button8Click(Sender: TObject);
var
StreamIncise: TStreamIncise;
I: Integer;
lStream: TMemoryStream;
lCompressionStream: TCompressStream;
begin
StreamIncise := TStreamIncise.Create;
lStream := TMemoryStream.Create;
lCompressionStream := TCompressStream.Create;
StreamIncise.LoadFromStream(FStream);
StreamIncise.SetStreamDefault;
try
for I := 0 to StreamIncise.IncisedCount - 1 do
begin
StreamIncise.GetInciseStream(lStream); //获得分割流
ClientDataSet2.Append;
ClientDataSet2.FieldByName('F_ID').Value := I; //取序列号
ClientDataSet2.FieldByName('F_NAME').Value := FFileFullName;
ClientDataSet2.FieldByName('F_SERIAL').Value := I; // 取每次分割的序列号
lCompressionStream.CompressStream(lStream);
(ClientDataSet2.FieldByName('F_BINARY_DATA')
as TBlobField).LoadFromStream(lStream);
ClientDataSet2.Post;
end;
finally
StreamIncise.Free;
lStream.Free;
lCompressionStream.Free;
end;
end;
最后我们增加了InciseSize 属性,让程序员在创建类以后可以自己修改分割块的大小。
通过这样的调用,我们就可以把分割类具体的保存业务的耦合解开,从而增加了分割类下次被重用的可能性。在查阅资料过程中我们也找到一些分割的例子,只是都跟具体的业务耦合得很紧密,要重用该代码除了粘贴复制以外基本上没有他法。
这样当迭代3完成的时候我们实现了对了流的分割压缩,文件分割存储用例到这里获得一个好的解决方案,通过小步的迭代前进我们可以在每一次迭代结束的时候获得可以使用的功能代码,剩下来就使考虑图档文件的格式问题了。其实更主要的通过这次开发我们让新加入的组员获得了一次很好的编程培训,更容易理解要实现一个功能的具体思路和步骤。