使用数据结构重构复制/粘贴模式






4.50/5 (6投票s)
使用数据结构来消除样板代码。
引言
继续之前关于“使用委托重构复制/粘贴代码”的文章,我将继续讨论如何在实际项目中重构复制/粘贴代码。
背景
在很多情况下,代码块会被一遍又一遍地重复使用。如果只是少数几次,那还好。但是,最终会有一个极限,超过这个极限,就意味着你应该尝试另一种方法。
最常见的重复情况之一是在大量调用存储过程或查询的项目中,这在大多数 CRUD 应用程序中很常见。因此,任何具有数据库访问代码的类都会有很多方法,这些方法中只有一小部分代码行彼此不同。这使得代码难以更新,因为更改必须由所有方法共享。由于无聊或拙劣的搜索和替换编辑,会产生无数的错误机会。
解决这个问题的方法是,我们必须将主要流程与数据分离。也就是说,通过创建适应应用程序需求的自定义数据结构来重构这种模式。此外,人们可以在其中添加一些技巧,使其更容易开发处理代码。
用 ADO.NET 举例说明
我们从这个常见的模式开始;正如你所看到的,它主要是样板代码。而且,如果只有少数几次出现,这样使用它也没什么问题。
public DataSet GetPersonsInformationWithFilter(char filter_sex)
{
DataSet persons = new DataSet();
string connectionstring =
ConfigurationManager.ConnectionStrings["default"].ConnectionString;
SqlConnection connection = new SqlConnection(connectionstring);
string query = "SELECT * FROM PERSONS WHERE PERSONS.SEX = @sex ;";
SqlCommand command = new SqlCommand(query, connection);
SqlDataAdapter adapter = new SqlDataAdapter(command);
SqlParameter sex_parameter =
command.Parameters.Add("@sex", SqlDbType.Char);
sex_parameter.Direction = ParameterDirection.Input;
sex_parameter.Value = filter_sex;
try
{
connection.Open();
adapter.Fill(persons);
}
catch
{
// do some logging here ...
throw;
}
finally
{
connection.Close();
adapter.Dispose();
command.Dispose();
connection.Dispose();
}
return persons;
}
在这种情况下,通过使用对数据库调用操作建模的数据结构,相对容易地重构这些类型的方法。并且可以在保持方法签名的情况下完成此操作,以便应用程序代码的其余部分不会中断。
这里有一些简单的数据结构,用于对 SQL 命令和 SQL 参数建模,方便地称为 QueryDefinition
和 ParameterDefinition
。
QueryDefinition
类将负责定义数据库命令调用,无论是查询(select、update、insert、delete)还是存储过程调用。ParameterDefinition
类将定义输入/输出参数。
public class QueryDefinition
{
public string ConnectionSetting
{
get;
set;
}
public string CallText
{
get;
set;
}
public CommandType CallType
{
get;
set;
}
public List<ParameterDefinition> Parameters
{
get;
set;
}
}
public class ParameterDefinition
{
public ParameterDefinition(string name, SqlDbType dbType,
ParameterDirection direction)
{
this.Name = name;
this.DbType = dbType;
this.Direction = direction;
}
public ParameterDefinition(string name, SqlDbType dbType,
ParameterDirection direction, int size)
: this(name, dbType, direction)
{
this.Size = size;
}
public ParameterDefinition(string name, SqlDbType dbType,
ParameterDirection direction, object value)
: this(name, dbType, direction)
{
this.Value = value;
}
public ParameterDefinition(string name, SqlDbType dbType,
ParameterDirection direction, int size, object value)
: this(name, dbType, direction, size)
{
this.Value = value;
}
public string Name
{
get;
set;
}
public SqlDbType DbType
{
get;
set;
}
public int? Size
{
get;
set;
}
public ParameterDirection Direction
{
get;
set;
}
public object Value
{
get;
set;
}
}
这些类存储构建 ADO.NET 命令的最低要求;这些将作为参数传递并定义一个操作,例如获取收据列表或插入客户信息。
我还会添加一个巧妙的功能,添加一个委托方法来传递输出。这将使代码更通用,并且更容易重用于大多数类型的输出。
protected T AccessDataBase<T>(QueryDefinition definitions,
Func<SqlCommand, T> output_functor)
{
using (SqlConnection connection = new SqlConnection(
ConfigurationManager.ConnectionStrings[
definitions.ConnectionSetting].ConnectionString))
{
try
{
connection.Open();
using (SqlCommand command =
this.CreateCommand(definitions, connection))
{
return output_functor(command);
}
}
catch (Exception ex)
{
throw;
}
finally
{
connection.Close();
}
}
}
protected IEnumerable<T> AccessDataBase<T>(
QueryDefinition definitions, Func<SqlDataReader, T> map_functor)
{
using (SqlConnection connection = new SqlConnection(
ConfigurationManager.ConnectionStrings[
definitions.ConnectionSetting].ConnectionString))
{
using (SqlCommand command =
this.CreateCommand(definitions, connection))
{
connection.Open();
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.NextResult())
{
yield return map_functor(reader);
}
}
connection.Close();
}
}
}
private SqlCommand CreateCommand(QueryDefinition definitions,
SqlConnection connection)
{
SqlCommand ret_command = new SqlCommand(definitions.CallText,
connection) { CommandType = definitions.CallType };
foreach (SqlParameter parameter in
this.BuildParameters(definitions.Parameters))
ret_command.Parameters.Add(parameter);
return ret_command;
}
private IEnumerable<SqlParameter> BuildParameters(
IEnumerable<ParameterDefinition> definitions)
{
foreach (ParameterDefinition item in definitions)
yield return this.BuildParameter(item);
}
private SqlParameter BuildParameter(ParameterDefinition definition)
{
SqlParameter retParam = null;
if (definition.Size != null)
retParam = new SqlParameter(definition.Name,
definition.DbType,
Convert.ToInt32(definition.Size))
{ Direction = definition.Direction };
else
retParam = new SqlParameter(definition.Name,
definition.DbType)
{ Direction = definition.Direction };
if ((definition.Direction == ParameterDirection.Input ||
definition.Direction == ParameterDirection.InputOutput)
&& definition.Value != null)
retParam.Value = definition.Value;
return retParam;
}
正如你所看到的,有两个 AccessDataBase
方法,一个用于处理单个返回值,无论是对象还是值类型,另一个 IEnumerable
用于处理输出可以返回多行的查询。
返回枚举的方法也不同,因为它的匿名函数是一个映射操作,因此当它迭代数据适配器类时,它会将选定的数据转换为预期的返回类型。
现在,我将添加一些辅助方法来填充参数列表并设置一个清理后的主体方法,该方法负责调用存储过程或查询命令。 这些是现成的辅助方法,可以执行最常用的操作。
protected DataSet AccessDbDataSet(QueryDefinition definitions)
{
Func<SqlCommand, DataSet> out_functor =
(SqlCommand command) => this.GetDataSet(command);
return this.AccessDataBase<DataSet>(definitions, out_functor);
}
protected int AccessDbNonQuery(QueryDefinition definitions)
{
Func<SqlCommand, int> out_functor =
(SqlCommand command) => command.ExecuteNonQuery();
return this.AccessDataBase<int>(definitions, out_functor);
}
在这种情况下,我决定通过子类化或派生来使用此类。 但是,也可以将该类编写为实用程序类。
所以,这是简化的方法; 类方法签名可以保持不变,但现在,更容易对内部代码和结构进行更改。
public DataSet GetSomeProcedure()
{
QueryDefinition definition = new QueryDefinition()
{
ConnectionSetting = this._connection,
CallText = "ProcedureName",
CallType = CommandType.StoredProcedure,
Parameters = new List<ParameterDefinition>()
{
new ParameterDefinition("result",
SqlDbType.Structured, ParameterDirection.ReturnValue)
}
};
return this.AccessDbDataSet(definition);
}
public DataSet GetSomeQuery(int id)
{
QueryDefinition definition = new QueryDefinition()
{
ConnectionSetting = this._connection,
CallText = "select * from atable where ID=@id;",
CallType = CommandType.Text,
Parameters = new List<ParameterDefinition>()
{
new ParameterDefinition("@id",
SqlDbType.Int, ParameterDirection.Input) { Value = id}
}
};
return this.AccessDbDataSet(definition);
}
public IEnumerable<Person> GetPersons()
{
QueryDefinition definition = new QueryDefinition()
{
ConnectionSetting = this._connection,
CallText = "select name, address, age from persons;",
CallType = CommandType.Text,
Parameters = new List<ParameterDefinition>() { }
};
Func<SqlDataReader, Person> map_functor =
(reader) => new Person() {
Name = reader["name"].ToString(),
Address = reader["address"].ToString(),
Age = (int) reader["age"]
};
foreach (Person item in
this.AccessDataBase<Person>(definition, map_functor))
yield return item;
}
第一个结果是代码变得更加简洁,并且你只需编写所需的内容,例如定义输入和输出。 因此,出错的机会更少。 其次,你为更有趣的更改开辟了新的领域,这些更改可以进一步简化数据层的编写。 你可以编写一个 Fluent 接口来处理整个过程; 如果需要,你还可以开发查询的序列化策略,而无需编写太多新代码。
关注点
这种重构模式不仅对于简化数据库访问代码很有用,而且对于类方法中输入和输出参数是唯一变量代码段的任何情况也很有用。
这种重构还有其他影响,这意味着我们可以序列化数据结构,使整个过程更加动态,从而减少了在数据结构更改不破坏代码时编译新二进制文件的需要。
历史
- 首次提交 - 2011 年 1 月 6 日。
- 更新后的提交 - 2011 年 1 月 13 日。