重构以避免重复代码

时间:2010-12-15 19:00:12

标签: c# oop refactoring

我正在尝试分解一些重复的代码,但它现在开始闻起来很时髦。 假设我从这开始不太正确,但你抓住了我的漂移:

    public virtual OrganisationEntity Get(int id)
    {
        SqlCommand command = new SqlCommand();
        command.CommandText = @"SELECT t.Id, t.Description FROM Organisation t Where t.Id = @Id";
        command.Parameters.Add("@id", SqlDbType.Int).Value = id;

        List<OrganisationEntity> entities = new List<OrganisationEntity>();

        SqlDataReader reader = Database.ExecuteQuery(command, ConnectionName.Dev);

        while (reader.Read())
        {
            OrganisationEntityMapper mapper = Mapper;
            entities = mapper.MapAll(reader);
        }

        return entities.First<OrganisationEntity>();
    }

很明显,除了查询相同的形式之外,每个其他Get(int id)方法都有,所以我的下一步是创建一个基类,RepositoryBase看起来像:

public abstract class RepositoryBase<T> where T : new()
{
    /// <summary>
    /// 
    /// </summary>
    public abstract EntityMapperBase<T> Mapper { get; }

    public virtual T Get(int id)
    {

        List<T> entities = new List<T>();

        SqlDataReader reader = Database.ExecuteQuery(Command, ConnectionName);

        while (reader.Read())
        {
            EntityMapperBase<T> mapper = Mapper;
            entities = mapper.MapAll(reader);
        }

        return entities.First<T>();
    }
}

添加一些通用的时髦,但这也是它变得难看的地方。 首先,Database.ExecuteQuery需要一个SqlCommand和一个枚举,所以我可以,然后我会添加2个属性,我将用一些东西启动它。 我已经意识到我不再需要int id参数了,因为我在子类中构造查询,所以我不妨将命令和connectionName作为参数传递,我希望connectionName无论如何都要依赖于OrganisationRepository(其他需要另一个字符串):

public class OrganisationRepository : RepositoryBase<OrganisationEntity>
{
    protected override EntityMapperBase<OrganisationEntity> Mapper
    {
        get
        {
            return new OrganisationMapper();
        }            
    }

    public override OrganisationEntity Get(int id)
    {
        SqlCommand command = new SqlCommand();
        command.CommandText = @"SELECT t.Id, t.Description FROM Organisation t Where t.Id = @Id";
        command.Parameters.Add("@id", SqlDbType.Int).Value = id;
        return base.Get(command, ConnectionName.Dev);
    }
}

但是,oops,当然,现在方法签名不再同步了......哎呀! 所以,基本上我在想。它只是感觉很讨厌,但不知道究竟为什么。 一方面,我想尽可能地分解重复代码,但现在它让我留下了这个!

如何将此重构为(更多)适当的OO?我应该忘记分解查询字符串并编写大量重复内容吗?

5 个答案:

答案 0 :(得分:1)

你的“下一步”与我的不一样。

我的下一步是找到你试图重构的这个“常用代码”的另一个例子。也许是一个“CustomerEntity.Get(int id)'”方法。

现在,让我们假设CustomerEntity和OrganisationEntity版本之间的唯一区别是查询字符串以及用“Customer”替换术语“Organization”。我的下一步是尝试使这两种方法越来越相同。假设此方法是OrganisationEntityRepository类的一部分,我会将其重构为EntityRepository1类,并将CustomerEntityRepository重构为EntityRepository2。

步骤1是为实体类型引入通用参数。您必须对OrganisationEntityMapper和CustomerEntityMapper类执行相同的操作。

接下来,回过头来看看还有什么不同。我看到他们使用不同的映射器类,所以让我们使映射器类型通用。为了做到这一点并仍然引用MapAll方法,我将介绍一个带有MapAll方法的IMapper接口,并让我的两个具体映射器类实现它。

现在,下一个重大区别是查询。我将它放入虚拟的“CommandText”属性中。

现在我觉得我已经准备好了一个基类,也许是EntityRepositoryBase<TEntity,TMapper>。通过适当的假设,我结束了以下几点:

public abstract class EntityRepositoryBase<TEntity, TMapper>
    where TMapper : IMapper<TEntity>
{
    public virtual TEntity Get(int id)
    {
        List<TEntity> entities;
        using (var command = new SqlCommand {CommandText = CommandText})
        {
            command.Parameters.Add("@id", SqlDbType.Int).Value = id;

            entities = new List<TEntity>();

            using (var reader = Database.ExecuteQuery(command, ConnectionName.Dev))
            {
                while (reader.Read())
                {
                    var mapper = Mapper;
                    entities = mapper.MapAll(reader);
                }
            }
        }

        return entities.First();
    }

    protected abstract string CommandText { get; }
    protected abstract TMapper Mapper { get; }
}

public class OrganisationEntityRepository :
    EntityRepositoryBase<OrganisationEntity, OrganisationEntityMapper<OrganisationEntity>>
{
    protected override string CommandText
    {
        get { return @"SELECT t.Id, t.Description FROM Organisation t Where t.Id = @Id"; }
    }

    protected override OrganisationEntityMapper<OrganisationEntity> Mapper
    {
        get { throw new NotImplementedException(); }
    }
}

public class CustomerEntityRepository : EntityRepositoryBase<CustomerEntity, CustomerEntityMapper<CustomerEntity>>
{
    protected override string CommandText
    {
        get { return @"SELECT t.Id, t.Description FROM Customer t Where t.Id = @Id"; }
    }

    protected override CustomerEntityMapper<CustomerEntity> Mapper
    {
        get { throw new NotImplementedException(); }
    }
}

而且,不用说,不管怎样我会说出来:支持JetBrains ReSharper 5.1做所有动作的事情,所以我没有必要。

答案 1 :(得分:0)

我相信像Entity FrameworknHibernate这样的ORM会更适合这种情况。他们可以照顾你自己想要建造的所有管道。

答案 2 :(得分:0)

如果您可以在项目中使用ORM框架,我认为最好不要手工编写所有这些内容。

但是,你可以做的一种重构方法是在RepositoryBase中有一个具有这个签名的抽象方法:

public abstract T Get(int id);

以及带有此签名的受保护方法:

protected T Get(SqlCommand command, SqlConnection connection)

与您之前在RepositoryBase中显示的代码相同。

这样,在派生类中,您只需要实现构造命令的那个,并从执行实际数据库调用的基类中调用该命令。

答案 3 :(得分:0)

我将从以下开始。在单独的接口中定义数据库和映射功能并将它们注入存储库,这样存储库将更容易测试。使用此方法,您应该能够扩展存储库以包含其他CRUD操作。

这种方法的一个问题是映射器和SqlCommand的创建之间的分离,通过select语句返回哪些列可能不是很明显。

// The concrete implementation of this interface will handle connections to the
// database
public interface IDatabase
{
    SqlDataReader ExecuteQuery(SqlCommand command);
}

public interface IEntityMapper<T>
{
    T MapAll(SqlDataReader reader);
}

public abstract class EntityRepository<T>
{
    private readonly IDatabase _database;
    private readonly IEntityMapper<T> _mapper;

    protected EntityRepository(IEntityMapper<T> mapper, IDatabase database)
    {
        _mapper = mapper;
        _database = database;
    }

    public T Get(int id)
    {
        return this.Get(_mapper, _database, id);
    }

    protected virtual T Get(IEntityMapper<T> mapper, IDatabase database, int id)
    {
        // Create a command can be used to fetch the entity, remember to dispose when complete
        using (var cmd = this.CreateGetCommand(id))
        {
            using (var reader = database.ExecuteQuery(cmd))
            {
                // No need to read all the rows, just the first...
                return reader.Read() ? mapper.MapAll(reader) : default(T);
            }
        }
    }

    protected abstract SqlCommand CreateGetCommand(int id);
}

并实施以下

public class OrganisationEntityRepository : EntityRepository<OrganisationEntity>
{
    public OrganisationEntityRepository(IEntityMapper<OrganisationEntity> mapper, IDatabase database) : base(mapper, database)
    {
    }

    protected override SqlCommand CreateGetCommand(int id)
    {
        var command = new SqlCommand(@"SELECT t.Id, t.Description FROM Organisation t Where t.Id = @Id");
        command.Parameters.Add("@id", SqlDbType.Int).Value = id;

        return command;
    }
}

public class OrganisationEntityMapper : IEntityMapper<OrganisationEntity>
{
    public OrganisationEntity MapAll(SqlDataReader reader)
    {
        return new OrganisationEntity();  // Populate using the reader...
    }
}

答案 4 :(得分:0)

以下是我将如何重构它:

public class Repository<T> : IRepository<T> where T : class, new()
{
    private IEntityMapper<T> _mapper;

    public Repository(IEntityMapper<T> mapper)
    {
        _mapper = mapper;
    }

    public virtual T Find(string value)
    {
        SqlCommand command = new SqlCommand();
        command.CommandText = @"SELECT t.Id, t.Description FROM Organisation t Where t.Description LIKE @value";
        command.Parameters.Add("@value").Value = value + "%";

        SqlDataReader reader = Database.ExecuteQuery(command, ConnectionName.Dev);
        return FillCollection(reader);
    }

    public void T Get(int id)
    {
        SqlCommand command = new SqlCommand();
        command.CommandText = @"SELECT t.Id, t.Description FROM Organisation t Where t.id = @value";
        command.Parameters.Add("@value").Value = id;

        SqlDataReader reader = Database.ExecuteQuery(command, ConnectionName.Dev);
        if (!reader.Read())
            return null;

        T entity = new T();
        _mapper.Map(entity, reader);
        return entity;
    }

    protected IList<T> FillCollection(IDataReader reader)
    {
        List<T> items = new List<T>();
        while (reader.Read())
        {
            T entity = new T();
            _mapper.Map(entity, reader);
            _items.Add(entity);
        }
        return items;
    }
}



public interface IEntityMapper<T>
{
    //row is the most generic part of a DataReader
    void Map(T entity, IDataRow row);
}

关键点:

  1. 使用FillCollection作为受保护的方法创建了一个基类。
  2. 强制实体是一个具有默认构造函数的类,以加速对象的创建。
  3. 使用mapper的接口并将其置于构造函数中。
  4. 我不会填写整个集合来获取第一个项目。浪费cpu。
  5. 尽量使用IDataRecord代替DbDataReader
  6. 作为通用接口