分层与接口
Posted 炼金士
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分层与接口相关的知识,希望对你有一定的参考价值。
最近浏览了一些网上对分层 、接口等概念的讨论。
想想这个问题也是我所接触到的一些团队,他们所一直困扰的问题,我觉得有必要整理一下思路。
这里所说的分层,是逻辑上的概念,和物理分层不搭界,也就是说,如果你的程序不做分布式,按照分层的设计思路,你也应该在你的代码级别上体现出逻辑分层的思想。
在CSLA里,一个业务对象自身就包含了持久层和逻辑层(CSLA里要细分层次的话,数下来蛮多的:UI、UI与客户端业务逻辑层的工作间层、客户端业务逻辑层、客户端与服务端的物理协议层、服务端业务逻辑层、服务端业务逻辑层与持久层的协议层、持久层、数据库,当然,开发人员的工作是写其中的客户端业务逻辑层、服务端业务逻辑层、持久层,其余CSLA基本已经帮你做好了)。在这些业务对象里,CSLA里是用模板的方式提供给你,使得你方便地在这些对象里实现了除UI之外其余的层。
如果你将物理分层和逻辑上的分层相混搅的话,会感觉CSLA实在是瞎胡闹,怎么能将几层的代码写在一个类里面呢。
如果你搞懂并遵循了CSLA给你提供的模板和框架,那么,设计、开发和后期的维护都是比较方便的,特别是维护,你知道问题会处在哪里,可以在哪个类的哪个代码块里找到。
说具体点,如果数据库某个表增加了一个字段,那么,系统要用到这个字段的话,业务逻辑层必然要做改动,相关的业务类要增加属性,以及对这些属性的操作函数,然后体现到UI上。这就是我们所说的,业务的改变,会从UI到后台的整个的修改,这是无法规避的事实,没有一个现成的方法能解决掉这个问题,你必须从UI改到后台。
这个问题的关键是,接口改变了!
分层的好处,是建立在接口稳定的基础之上的。在接口不变的状况下,每个层可以单独去修改自己的代码,这方便了团队的合作开发,只要大家确定好接口。
但如果是上面说的要加一个字段,并且要体现到UI层,你肯定要到每个层里去修改相应的代码,而对于接口,或者增加、或者修改。不管如何,接口是动了。
有人会想到这样的办法来解决问题:函数名和参数都是固定的,然后,用一个自定义格式的合并字符串来提交数据和返回数据。例如像下面的案例:
存储过程这么写:
CREATE or REPLACE PROCEDURE TEST_P(
I_PARM IN LONG,
I_VALUE IN LONG,
O_PARM OUT LONG,
O_VALUE OUT LONG)
IS
BEGIN
O_PARM := I_PARM;
O_VALUE := I_VALUE;
END TEST_P;
I_PARM和I_VALUE是输入的参数集合,I_PARM里用#2分隔参数名,I_VALUE里用#2分隔参数值;
O_PARM和O_VALUE是输出的参数集合,O_PARM里用#2分隔参数名,O_VALUE里用#2分隔参数值;
那么调用端,怎么应付这种协议呢?(注意,我这里说的是协议,不是接口)
procedure ThcRenovateSocketConnection.ExecProc(const StoredProcName: string;
const InParamNames: Variant; const InParamValues: Variant;
var OutParamNames: Variant; var OutParamValues: Variant);
var
Params: TParams;
S: string;
I: Integer;
begin
if VarIsArray(InParamNames) and VarIsArray(InParamValues) and
(VarArrayDimCount(InParamNames) <> VarArrayDimCount(InParamValues)) then
raise Exception.Create('参数 InParamNames 和 InParamValues 的数据项不对称!');
Params := TParams.Create;
try
S := '';
if VarIsArray(InParamNames) and (VarArrayDimCount(InParamNames) = 1) then
for I := VarArrayLowBound(InParamNames, 1) to VarArrayHighBound(InParamNames, 1) do
S := S + InParamNames[I] + #2
else
S := VarToStr(InParamNames) + #2;
Params.CreateParam(ftString, ’I_PARM‘, ptInput).AsString := S;
S := '';
if VarIsArray(InParamValues) and (VarArrayDimCount(InParamValues) = 1) then
for I := VarArrayLowBound(InParamValues, 1) to VarArrayHighBound(InParamValues, 1) do
S := S + InParamValues[I] + #2
else
S := VarToStr(InParamValues) + #2;
Params.CreateParam(ftString, 'I_VALUE', ptInput).AsString := S;
Params.CreateParam(ftString, 'O_PARM‘, ptOutput).AsString := ' ';
Params.CreateParam(ftString, 'O_VALUE', ptOutput).AsString := ' ';
ExecProc(StoredProcName, Params);
with TStringList.Create do
try
S := StringReplace(StringReplace(VarToStr(Params.ParamValues['O_PARM’]), #$D, #$1D, [rfReplaceAll]), #$A, #$1A, [rfReplaceAll]);
Text := StringReplace(S, #2, #$D#$A, [rfReplaceAll]);
if Count = 1 then
OutParamNames := StringReplace(StringReplace(Strings[0], #$1D, #$D, [rfReplaceAll]), #$1A, #$A, [rfReplaceAll])
else
begin
OutParamNames := VarArrayCreate([0, Count - 1], varVariant);
for I := 0 to Count - 1 do
OutParamNames[I] := StringReplace(StringReplace(Strings[I], #$1D, #$D, [rfReplaceAll]), #$1A, #$A, [rfReplaceAll]);
end;
S := StringReplace(StringReplace(VarToStr(Params.ParamValues['O_VALUE']), #$D, #$1D, [rfReplaceAll]), #$A, #$1A, [rfReplaceAll]);
Text := StringReplace(S, #2, #$D#$A, [rfReplaceAll]);
if Count = 1 then
OutParamValues := StringReplace(StringReplace(Strings[0], #$1D, #$D, [rfReplaceAll]), #$1A, #$A, [rfReplaceAll])
else
begin
OutParamValues := VarArrayCreate([0, Count - 1], varVariant);
for I := 0 to Count - 1 do
OutParamValues[I] := StringReplace(StringReplace(Strings[I], #$1D, #$D, [rfReplaceAll]), #$1A, #$A, [rfReplaceAll]);
end;
finally
Free;
end;
finally
Params.Free;
end;
if VarIsArray(OutParamNames) and VarIsArray(OutParamValues) and
(VarArrayDimCount(OutParamNames) <> VarArrayDimCount(OutParamValues)) then
raise Exception.Create('参数 OutParamNames 和 OutParamValue 的数据项不对称!');
end;
这种方法,在存储过程和调动端,都得写一个解析和打包的协议层,但能否解决上面所说的问题呢?
不能。
因为,增加了一个字段,必然要在服务和调用者双方的代码里修改对应的代码,你不改服务代码或者不改调用者代码,试试看,系统还是旧的。
上面的方法,是混搅了协议和接口的概念,不管你怎么改变协议和调用方法,但接口还是接口。
接口定义是逻辑上的,它隐藏在了代码里。
所以,我更倾向于将接口做成显式的,也就是强类型的,一旦有误,在编译期就通不过。
所以上面的方法,解决不了接口变化的情况,干脆,按照正规的方法写存储过程的参数,一个个地罗列出来。当然,为了升级方便,服务方可以提供新的接口(新的存储过程,原代码拷进来进行修改),然后,当调用方也升级成功了,再删除旧的存储过程。
所以,不管层之间是传递数据包、对象、还是其他自定义的东西,那都是协议,不是接口。接口是函数的参数、类的属性。。。,怎样的千变万化,只是形式(协议)上不同而已,它仍然存在在你的代码里,它动了,所串起来的东西都得动。
所以,我们要解决的是:如何在接口变化的情况下,能很快的处理问题。
最不好的情况是,一个点动了,拎起无数条线索,那就有得忙乎了,这就是所谓的松耦合的概念。
特别是一些业务需求经常变化的项目尤其重要,接口设计的很差的系统是没法维护下去的,甚至都没法上线。
除了系统设计,遵循松耦合的思想和准则外(达到目的是,一个点动了,拎起来的线索是有限和可控的),需要一个好的框架来配合。
一个好的框架,就是让你方便地管理接口,你可以很快找到地方,去修改相应的代码,而且在编译期就可以把问题暴露出来。
CSLA所谓在层间传输对象,主要是指它在物理层上进行传输所带来的优点(这个优点有点保留)。
CSLA反而是在逻辑的层上,进行了统一管理,你可以在一个类里面完成各个逻辑层的修改,这不是显而易见的好处吗!
这是非常值得借鉴的。
以上是关于分层与接口的主要内容,如果未能解决你的问题,请参考以下文章