c# - patrones - Patrón de repositorio y Data Mapper
patrones de diseño repositorio c# (1)
Después de leer un montón acerca de Repository y Data Mapper, decidí implementar esos patrones en un proyecto de prueba. Como soy nuevo en esto, me gustaría obtener su opinión sobre cómo implementé esos en un proyecto simple.
Jeremy Miller dice:
Realice algún tipo de proyecto de codificación personal no trivial donde pueda experimentar libremente con patrones de diseño.
Pero no sé, hice todas estas cosas bien o no.
Aquí está mi estructura de proyecto:
Como puede ver, hay muchas carpetas que voy a describir en detalle a continuación.
Dominio: las entidades de dominio del proyecto van aquí Tengo una clase de personal simple que se hereda de la clase EntityBase, la clase EntityBase tiene una sola propiedad llamada Id.
public int Id { get; set; }
Infraestructura: Aquí hay una capa de acceso a datos simple con dos clases. SqlDataLayer es una clase simple que se hereda de una clase abstracta llamada DataLayer. Aquí proporciono algunas funcionalidades como el siguiente código:
public SQLDataLayer() { const string connString = "ConnectionString goes here"; _connection = new SqlConnection(connString); _command = _connection.CreateCommand(); }
agregar parámetro a la colección de parámetros de comandos:
public override void AddParameter(string key, string value) {
var parameter = _command.CreateParameter();
parameter.Value = value;
parameter.ParameterName = key;
_command.Parameters.Add(parameter);
}
ejecutando DataReader:
public override IDataReader ExecuteReader() {
if (_connection.State == ConnectionState.Closed)
_connection.Open();
return _command.ExecuteReader();
}
y así.
- Repositorio: Aquí traté de implementar un patrón de repositorio. IRepository es una interfaz genérica
IRepository.cs:
public interface IRepository<TEntity> where TEntity : EntityBase
{
DataLayer Context { get; }
TEntity FindOne(int id);
ICollection<TEntity> FindAll();
void Delete(TEntity entity);
void Insert(TEntity entity);
void Update(TEntity entity);
}
Repository.cs:
public class Repository<TEntity> : IRepository<TEntity> where TEntity : EntityBase, new() {
private readonly DataLayer _domainContext;
private readonly DataMapper<TEntity> _dataMapper;
public Repository(DataLayer domainContext, DataMapper<TEntity> dataMapper) {
_domainContext = domainContext;
_dataMapper = dataMapper;
}
public DataLayer Context {
get { return _domainContext; }
}
public TEntity FindOne(int id)
{
var commandText = AutoCommand.CommandTextBuilder<TEntity>(CommandType.StoredProcedure, MethodType.FindOne);
// Initialize parameter and their types
Context.AddParameter("Id", id.ToString(CultureInfo.InvariantCulture));
Context.SetCommandType(CommandType.StoredProcedure);
Context.SetCommandText(commandText);
var dbReader = Context.ExecuteReader();
return dbReader.Read() ? _dataMapper.Map(dbReader) : null;
}
No expuse métodos no implementados de IRepository.
Aquí, en la clase de Repositorio genérico, espero que dos parámetros en el constructor primero sean una referencia a mi clase SqlDataLayer y el segundo sea una referencia a Entity DataMapper. Aquellos parámetros enviados por cada clase de Repositorio de Entidades que heredó de la clase Repositorio. por ejemplo :
public class PersonnelRepository : Repository<Personnel>, IPersonnelRepository {
public PersonnelRepository(DataLayer domainContext, PersonnelDataMapper dataMapper)
: base(domainContext, dataMapper) {
}
}
Como puede ver aquí en el método FindOne, traté de automatizar algunas operaciones, como crear CommandText, luego aproveché la ventaja de mi clase DataLayer para configurar el comando y finalmente ejecutar el comando para obtener IDataReader. Paso IDataReader a mi clase DataMapper para mapear a la Entidad.
DomainMapper: Finalmente, aquí asigné el resultado de IDataReader a Entities, a continuación se muestra una muestra de cómo mapeo Personnel entity:
public class PersonnelDataMapper : DataMapper<Personnel> { public override Personnel Map(IDataRecord record) { return new Personnel { FirstName = record["FirstName"].ToString(), LastName = record["LastName"].ToString(), Address = record["Address"].ToString(), Id = Convert.ToInt32(record["Id"]) }; }}
Uso:
using (var context = new SQLDataLayer()) {
_personnelRepository = new PersonnelRepository(context, new PersonnelDataMapper());
var personnel = _personnelRepository.FindOne(1);
}
Sé que cometí muchos errores aquí, por eso estoy aquí. Necesito su consejo para saber qué hice mal o cuáles son los puntos buenos en este proyecto de prueba simple.
Gracias por adelantado.
Algunos puntos:
Me parece que, en general, tienes un buen diseño. Eso se evidencia, en parte, por el hecho de que puede hacer cambios en él con poco impacto en cualquier clase fuera de las que se cambian (bajo acoplamiento). Dicho esto, es muy similar a lo que hace Entity Framework, por lo tanto, si bien es un buen proyecto personal, consideraría usar EF primero antes de implementarlo en un proyecto de producción.
Su clase DataMapper podría hacerse genérica (digamos,
GenericDataMapper<T>
) utilizando la reflexión. Se itera sobre las propiedades del tipo T mediante reflexión y se obtienen de la fila de datos de forma dinámica.Suponiendo que crea un DataMapper genérico, podría considerar crear un
CreateRepository<T>()
en DataLayer, de modo que los usuarios no tengan que preocuparse por los detalles de qué tipo de Mapper elegir.Una crítica menor: usted asume que todas las entidades tendrán un único ID entero llamado "Id", y que los procedimientos almacenados se configurarán para recuperarlos por tal. Puede mejorar su diseño aquí al permitir claves primarias de diferentes tipos, de nuevo quizás mediante el uso de genéricos.
Probablemente no desee volver a utilizar los objetos de Conexión y Comando de la manera en que lo hace. Eso no es seguro para subprocesos, e incluso si lo fuera, terminarías con unas condiciones de carrera sorprendentes y difíciles de depurar alrededor de Transacciones DB. Debería crear nuevos objetos de Conexión y Comando para cada llamada de función (asegurándose de eliminarlos una vez que haya terminado), o implementar alguna sincronización alrededor de los métodos que acceden a la base de datos.
Por ejemplo, sugeriría esta versión alternativa de ExecuteReader:
public override IDataReader ExecuteReader(Command command) {
var connection = new SqlConnection(connString);
command.Connection = connection;
return command.ExecuteReader();
}
Su antiguo reutilizó el objeto de comando, lo que podría conducir a condiciones de carrera entre los llamadores multiproceso. También desea crear una nueva conexión, porque la conexión anterior podría estar involucrada en una transacción iniciada por un interlocutor diferente. Si desea reutilizar las transacciones, debe crear una conexión, comenzar una transacción y volver a usar esa transacción hasta que haya ejecutado todos los comandos que desea asociar con la transacción. Como ejemplo, puede crear sobrecargas de sus métodos ExecuteXXX como este:
public override IDataReader ExecuteReader(Command command, ref SqlTransaction transaction) {
SqlConnection connection = null;
if (transaction == null) {
connection = new SqlConnection(connString);
transaction = connection.BeginTransaction();
} else {
connection = transaction.Connection;
}
command.Connection = connection;
return command.ExecuteReader();
}
// When you call this, you can pass along a transaction by reference. If it is null, a new one will be created for you, and returned via the ref parameter for re-use in your next call:
SqlTransaction transaction = null;
// This line sets up the transaction and executes the first command
var myFirstReader = mySqlDataLayer.ExecuteReader(someCommandObject, ref transaction);
// This next line gets executed on the same transaction as the previous one.
var myOtherReader = mySqlDataLayer.ExecuteReader(someOtherCommandObject, ref transaction);
// Be sure to commit the transaction afterward!
transaction.Commit();
// Be a good kid and clean up after yourself
transaction.Connection.Dispose();
transaction.Dispose();
- Por último, pero no por eso menos importante, después de haber trabajado con Jeremy, estoy seguro de que diría que debería tener pruebas unitarias para todas estas clases.