最近一直都没有写blog,也没有翻译,几乎将所有的业余时间都放在了C# Code Analyser上。有好几个晚上,做梦都在思考设计中的递归问题,感觉前进的路十分难走。
开发这个程序的最初目标是分析出程序中对象的结构与关系,本以为能够分析出代码的语言结构就能够做到这一点,可是分析代码的语言结构根本就不像我所想象的那么简单。越往前走,感觉路越黑。而在黑暗中也感觉到了编程语言的精妙所在。
在这个过程中,(可能是偶然,但是对我帮助很大),读到了一本关于数理逻辑的书《不完备性--哥德尔的证明和悖论》,虽然只看了一半,但是里面的思想深深地影响了我:递归思想。这个思想让我彻底摆脱了代码的表面,而开始进入其一个新的世界。
实际上,在我的第一个Code Analyser的设计思想,就是找到代码表面的特征。这是一种很有限的工作,代码的表面特征有很多种,我们是没有办法将其穷举的。例如下面的代码:
bool CodeHelperFunctions::HasFunctionDefinition(wchar_t * const & buffer, wchar_t * & startFunction,wchar_t * & endFunction, wstring& functionName)
{
wchar_t* comp(buffer);
wchar_t * bracket1(0), * bracket2(0);
while(*comp != '/0')
{
wchar_t* typeName = Common::String::GetStartOfWord(comp);
if(typeName == '/0')
break;
wchar_t* endOfTypeName = GetEndOfTypeName(typeName);
if(Common::String::FindWord(typeName, endOfTypeName, L"namespace")
|| Common::String::FindWord(typeName, endOfTypeName, L"class")
|| Common::String::FindWord(typeName, endOfTypeName, L"struct")
|| Common::String::FindWord(typeName, endOfTypeName, L"enum"))
{
comp = Common::String::GetStartOfWord(++endOfTypeName);
continue;
}
wchar_t* funcName = Common::String::GetStartOfWord(endOfTypeName + 1);
if(funcName == '/0')
break;
wchar_t* endOfFuncName = GetEndOfFuncName(funcName);
if(Common::String::FindChar(funcName,endOfFuncName,';'))
{
comp = funcName;
continue;
}
if(!IsEmpty(endOfTypeName, funcName))
{
comp = funcName;
continue;
}
//get the function name
bracket1 = Common::String::GetSuccessiveOf(endOfFuncName,'(');
if(bracket1 > 0)
{
bracket2 = Common::String::GetEndPosOfTag(bracket1,'(',')');
if(Common::String::FindChar(bracket1, bracket2, ';'))
{
comp = funcName;
continue;
}
if(bracket2)
{
wchar_t* s = Common::String::GetSuccessiveOf(bracket2, ':');
if(s)
{
//调用基类构造函数
wchar_t* b1 = Common::String::GetFirstLetter(s,'(');
if(b1 == 0)
{
comp = funcName;
continue;
};
wchar_t* b2 = Common::String::GetEndPosOfTag(b1,'(',')');
if(b2 == 0)
{
comp = funcName;
continue;
}
startFunction = Common::String::GetSuccessiveOf(b2,'{');
if(startFunction == 0)
{
comp = funcName;
continue;
}
endFunction = Common::String::GetEndPosOfTag(startFunction, '{', '}');
if(endFunction == 0)
{
comp = funcName;
continue;
}
}
else
{
startFunction = Common::String::GetSuccessiveOf(bracket2, '{');
if(startFunction == 0)
{
comp = funcName;
continue;
}
endFunction = Common::String::GetEndPosOfTag(startFunction,'{', '}');
if(endFunction == 0)
{
comp = funcName;
continue;
}
}
//获取函数名称
functionName = wstring(funcName, Common::String::GetEndOfWord(funcName) + 1);
return true;
}
}
bracket1 = Common::String::GetSuccessiveOf(endOfFuncName,'{');
if(bracket1)
{
wchar_t* endOfPropertyBracket = Common::String::GetEndPosOfTag(bracket1,'{','}');
if(endOfPropertyBracket)
{
functionName = wstring(funcName, endOfFuncName + 1);
startFunction = bracket1;
endFunction = endOfPropertyBracket;
return true;
}
}
comp = funcName;
}
return false;
}
上面的代码你没有必要去读,其意图是分析出函数定义。说实话,这样的代码,我自己都不希望看第二遍,很糟糕,其原因是代码的处理逻辑和一些表面现象急急地粘合在一起,是代码的处理逻辑失去了弹性(严格意义说是可扩展性)和可维护性。
在这段代码写成之后,Code Analyser的最初几个版本发布期间,我在公司的项目中,也遇到了很多维护性的问题。这让我正式开始去思考我们该如何去做,才能提高系统的可维护性。
我实际上是不相信Code Analyser会能够100%地分析出所有的函数定义,只要能够大致分析出函数的类定义中的函数的调用关系我就很满足了。在一段时间里,Code Analyser确实在代码分析上给我不少的帮助。直到有一天,我发现用Code Analyser确实不能把某些函数定义分析出来,于是我决定从新设计其分析算法,力图彻底解决这个问题。
要想解决这个问题,必须得分析出问题具体出在什么地方。在上面的函数中,逻辑和数据被紧紧地粘在了一起,这一点本身就不利于代码的维护和代码的扩展。
例如下面这段代码:
if(Common::String::FindWord(typeName, endOfTypeName, L"namespace")
|| Common::String::FindWord(typeName, endOfTypeName, L"class")
|| Common::String::FindWord(typeName, endOfTypeName, L"struct")
|| Common::String::FindWord(typeName, endOfTypeName, L"enum"))
{
comp = Common::String::GetStartOfWord(++endOfTypeName);
continue;
}
如果分析的单词是namespace,class,struct,enum,那么将读取指针移动到单词的结尾,并进入下一个循环。这段代码的目的是跳过namespace,class,struct,enum类型定义,那么剩下的可能就是函数和属性的定义。如果我们再次提炼一下这个逻辑,就会得到这样的逻辑,如果x为真,那么就会得到y(很遗憾,关于逻辑的标准表达我也是在学习中)。这个函数的代码虽然这么长,但是实际上就只有这么一个逻辑。那反过来想,既然只有这么一个逻辑,那又是什么因素让这个函数提变得如此的长呢?我得到的答案是数据(如果你有不同的想法,欢迎讨论)。是数据的多样性造就了这段代码的复杂性。
在这段代码中,先分析的数据是namespace,class,struct,enum;然后分析的是(,),{,},即在逻辑上,后一个判断处理以前一个判断处理的结果为基础(这很正常),而在代码的分布上,后一个判断和前一个判断粘连在一起,这对代码的扩展性和维护性是及其不利的。
我们的代码,特别是处理业务逻辑的代码,几乎都是一个样子:如果什么,就什么(当然我是凭借自己的经验下的这个结论,目前为止,我还没有相关的证明)。数据的多样性,使我们不得不为各种数据类型编写专门的代码来根据这些数据的属性编写如果什么,就什么的代码。
这是一个有趣的思考,但是由于本人的能力不够,不能够继续深入,还望各位见谅。下面我直接把改进后的代码粘出来和大家分享。
void ParseMember(CSharpCodeReaderX & reader, CodeElement & codeElement,const wstring & code)
{
CSharpKeyword keyword;
switch(keyword.GetKeyword(code))
{
case Keyword::Class:
{
if(codeElement.GetAccessType() == AccessType::None)
codeElement.SetAccessType(AccessType::InternalAccess);//设置元素的访问类型
codeElement.SetCodeElementType(CodeElementType::ClassType);//设置元素的类型
codeElement.SetName(AssertReadName(reader,keyword));//设置与元素的名称
//设置元素的继承关系
::CSharpCodeReaderX subReader(Common::String::FindChar(reader.GetCurrentBuffer(),'{'),
Common::String::GetEndPosOfTag(Common::String::FindChar(reader.GetCurrentBuffer(),'{'),
'{','}'));
reader.SetCurrentBuffer(ParseClass(subReader, codeElement));
}
break;
case Keyword::Private:
{
codeElement.SetAccessType(AccessType::PrivateAccess);
wstring code1;
if(reader.Read(code1))
{
ParseMember(reader, codeElement,code1);
return;
}
else
throw new runtime_error("Invalid terminal for the code");
}
break;
case Keyword::Protected:
{
codeElement.SetAccessType(AccessType::ProtectedAccess);
wstring codeUnit;
if(reader.Read(codeUnit))
{
ParseMember(reader, codeElement, codeUnit);
return;
}
else
throw new runtime_error("Invalid terminal for the code");
}
break;
case Keyword::Public:
{
codeElement.SetAccessType(AccessType::PublicAccess);
wstring codeUnit;
if(reader.Read(codeUnit))
{
ParseMember(reader, codeElement, codeUnit);
return;
}
else
throw new runtime_error("Invalid terminal for the code");
}
break;
case Keyword::None:
{
wstring codeUnit;
if(reader.Read(codeUnit))
{
ParseMember(reader, codeElement,codeUnit);
if(codeElement.GetName().length() == 0)
{
codeElement.SetName(code);
return;
}
else if(codeElement.GetReturnTypeName().length() == 0)
{
codeElement.SetReturnTypeName(code);
return;
}
else
throw new runtime_error("Invalid terminal for the code");
}
else
throw new runtime_error("Invalid terminal for the code");
}
break;
case Keyword::LeftParenthesis:
{
codeElement.SetCodeElementType(CodeElementType::MethodType);
/*分析方法中的调用*/
codeElement.SetMemberStart(Common::String::FindChar(reader.GetCurrentBuffer(),'{'));
codeElement.SetMemberEnd(Common::String::GetEndPosOfTag(
Common::String::FindChar(reader.GetCurrentBuffer(),'{'),
'{','}'));
reader.SetCurrentBuffer(codeElement.GetMemberEnd());
return;
}
break;
case Keyword::LeftCurlyBracket:
{
codeElement.SetCodeElementType(CodeElementType::PropertyType);
/*分析属性中的调用*/
codeElement.SetMemberStart(reader.GetCurrentBuffer() - 1);
codeElement.SetMemberEnd(Common::String::GetEndPosOfTag(codeElement.GetMemberStart(),
'{','}'));
reader.SetCurrentBuffer(codeElement.GetMemberEnd());
return;
}
break;
case Keyword::Semicolon:
{
codeElement.SetCodeElementType(CodeElementType::FieldType);
return;
}
break;
case Keyword::Assignment:
{
codeElement.SetCodeElementType(CodeElementType::FieldType);
reader.SetCurrentBuffer(Common::String::FindChar(reader.GetCurrentBuffer(),';'));
return;
}
case Keyword::Region:
case Keyword::CommentBlock:
case Keyword::CommentLine:
case Keyword::Space:
{
wstring codeUnit;
if(reader.Read(codeUnit))
{
ParseMember(reader, codeElement, codeUnit);
return;
}
}
break;
default:
{
wstring codeUnit;
if(reader.Read(codeUnit))
{
ParseMember(reader,codeElement,codeUnit);
return;
}
else
throw new runtime_error("Invalid terminal for the code");
}
break;
}
}
这段新算法和第一个版本比起来,有了很大的区别。
在这段代码中,引入了关键字处理,代码单词读取器和代码结构对对象。回到最初的代码,这三个对象存在与被分散的执行代码中。这又会带来什么好处呢?
在这个函数中,明确定义了这三个对象之后,我们在分析代码时可以将更多的精力放在该函数的其它代码上。由于对象封装了实现,那么我们在优化对象内部实现时,不会影响到现有的代码。其实这点谁都知道,而我这里还想强调的是另外一个价值,就是我们可以放心地用这种方法用于迭代开发。大家很容易可以发现,在新算法中,关键字同真正的C#语言比起来少了很多,原因是我目的分析工作只需要这些关键字。我会根据分析工作的深入,逐渐增加关键字。这可以保证我当前阶段的工作可以顺利产出。
另外,正如我们前面分析的那样,代码是由很多个“如果…就…”逻辑组成,其它代码完成的就是数据的转换工作。在业务逻辑的层面上,我们应该尽量在处理“如果…就…”的同时还要完成数据的转换工作,这不利于业务逻辑的有效展示,同时也限制的数据本身的可移植性。我们还是拿这两个函数说事,
在前一个函数中,业务逻辑就和数据转换紧紧地粘合在一起,例如:
wchar_t* funcName = Common::String::GetStartOfWord(endOfTypeName + 1);
这段代码就是为了获取一个名称,这个名字根据业务逻辑的判断,来决定这个名字是否是一个函数名。那么很显然,这段代码和这个函数的业务逻辑没有什么必然的联系。它做的就是一个数据转换工作,既将当前读取字符流的指针位置开始取一个单词。类似的转换工作在之前的那个函数中还有很多。在新的算法中,所有的单词转换工作由CSharpCodeReaderX完成。CSharpCodeReaderX做为一种新的数据类型,在新算法中,为业务逻辑提供数据支持。而这一点也是我们作为系统构架师需要掌握的一把尺子:
1. 用它来评价系统的优化结果
2. 用它来评价要选择的目标开源框架所提供的业务逻辑设计上的支持
以上也算是我对目前开发的一点总结吧,对与最后两点分析结果,我已经在公司的项目中开始实践,收到了不错的效果。有一点很明确,尽量将数据转换和业务逻辑代码分开。
明天开始我又要开始接下来的Code Analyser开发,希望能够得到大家的支持。
更多内容,请参见C# Code Analyser 及其它的开发背景

本文分享了作者在开发C#代码分析器过程中的心得,包括如何通过重构提高代码的可维护性和扩展性,以及使用递归思想进行更深层次的代码分析。
3105

被折叠的 条评论
为什么被折叠?



