MVC 3 - как реализовать сервисный уровень, нужны ли репозитории?
В настоящее время я создаю свое первое приложение MVC 3, используя EF Code First, SQL CE и Ninject.
Я много читал об использовании репозиториев, подразделения работы и слоев обслуживания. Я думаю, что у меня есть основы, и я сделал свою собственную реализацию.
Это моя текущая настройка:
Объекты
public class Entity
{
public DateTime CreatedDate { get; set; }
public Entity()
{
CreatedDate = DateTime.Now;
}
}
public class Profile : Entity
{
[Key]
public Guid UserId { get; set; }
public string ProfileName { get; set; }
public virtual ICollection<Photo> Photos { get; set; }
public Profile()
{
Photos = new List<Photo>();
}
public class Photo : Entity
{
[Key]
public int Id { get; set; }
public Guid FileName { get; set; }
public string Description { get; set; }
public virtual Profile Profile { get; set; }
public Photo()
{
FileName = Guid.NewGuid();
}
}
SiteContext
public class SiteContext : DbContext
{
public DbSet<Profile> Profiles { get; set; }
public DbSet<Photo> Photos { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}
}
Интерфейс: IServices
public interface IServices : IDisposable
{
PhotoService PhotoService { get; }
ProfileService ProfileService { get; }
void Save();
}
Реализация: службы
public class Services : IServices, IDisposable
{
private SiteContext _context = new SiteContext();
private PhotoService _photoService;
private ProfileService _profileService;
public PhotoService PhotoService
{
get
{
if (_photoService == null)
_photoService = new PhotoService(_context);
return _photoService;
}
}
public ProfileService ProfileService
{
get
{
if (_profileService == null)
_profileService = new ProfileService(_context);
return _profileService;
}
}
public void Save()
{
_context.SaveChanges();
}
private bool disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
_context.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
Интерфейс
public interface IPhotoService
{
IQueryable<Photo> GetAll { get; }
Photo GetById(int photoId);
Guid AddPhoto(Guid profileId);
}
Реализация
public class PhotoService : IPhotoService
{
private SiteContext _siteContext;
public PhotoService(SiteContext siteContext)
{
_siteContext = siteContext;
}
public IQueryable<Photo> GetAll
{
get
{
return _siteContext.Photos;
}
}
public Photo GetById(int photoId)
{
return _siteContext.Photos.FirstOrDefault(p => p.Id == photoId);
}
public Guid AddPhoto(Guid profileId)
{
Photo photo = new Photo();
Profile profile = _siteContext.Profiles.FirstOrDefault(p => p.UserId == profileId);
photo.Profile = profile;
_siteContext.Photos.Add(photo);
return photo.FileName;
}
}
Global.asax
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());
Database.SetInitializer<SiteContext>(new SiteInitializer());
}
NinjectControllerFactory
public class NinjectControllerFactory : DefaultControllerFactory
{
private IKernel ninjectKernel;
public NinjectControllerFactory()
{
ninjectKernel = new StandardKernel();
AddBindings();
}
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
return controllerType == null
? null
: (IController)ninjectKernel.Get(controllerType);
}
private void AddBindings()
{
ninjectKernel.Bind<IServices>().To<Services>();
}
}
PhotoController
public class PhotoController : Controller
{
private IServices _services;
public PhotoController(IServices services)
{
_services = services;
}
public ActionResult Show(int photoId)
{
Photo photo = _services.PhotoService.GetById(photoId);
if (photo != null)
{
string currentProfile = "Profile1";
_services.PhotoService.AddHit(photo, currentProfile);
_services.Save();
return View(photo);
}
else
{
// Add error message to layout
TempData["message"] = "Photo not found!";
return RedirectToAction("List");
}
}
protected override void Dispose(bool disposing)
{
_services.Dispose();
base.Dispose(disposing);
}
}
Я могу построить свое решение и, похоже, работает правильно.
Мои вопросы:
- Есть ли какие-то явные недостатки в моей реализации, которые мне не хватает?
- Смогу ли я использовать это с TDD? Обычно я вижу насмешку над репозиториями, но я не использовал это в приведенном выше, это вызовет проблемы?
- Я использую DI (Ninject) правильно и достаточно?
Я программист по хобби, поэтому любые комментарии и/или предложения к моему коду приветствуются!
Ответы
Ответ 1
У вас есть общая идея, но для того, чтобы действительно привыкнуть к Injection Dependency, требуется некоторое время. Я вижу ряд возможных улучшений:
- Ваш
IServices
интерфейс кажется ненужным. Я бы предпочел, чтобы контроллер определял, какие службы он нуждается (IPhotoService и т.д.) Через свой конструктор, вместо использования интерфейса IServices
, как своего рода сильно типизированный локатор сервисов.
- Я видел там
DateTime.Now
? Как вы собираетесь проверить правильность установки даты в unit test? Что делать, если позже вы решите поддерживать несколько часовых поясов? Как насчет использования инъецируемой службы даты для создания этого CreatedDate
?
- Существует очень хорошее расширение Ninject, специально для MVC. Он заботится о подключении к различным точкам, поддерживаемым MVC 3 для инъекций. Он реализует такие вещи, как ваш NinjectControllerFactory. Все, что вам нужно сделать, это сделать ваш класс
Global
расширением определенного приложения на основе Ninject.
- Я бы предложил использовать NinjectModules для установки привязок вместо того, чтобы устанавливать их в ControllerFactory.
- Рассмотрите возможность использования привязки по Конвенции, чтобы вам не приходилось явно привязывать каждую службу к ее реализации.
Update
Расширение Ninject MVC можно найти здесь. См. Раздел README для примера того, как расширить NinjectHttpApplication
. В этом примере используются модули, которые вы можете прочитать здесь здесь. (Они в основном просто место для размещения вашего кода привязки, чтобы вы не нарушали принцип единой ответственности.)
Что касается привязок на основе условностей, общая идея заключается в том, чтобы ваш код привязки просматривал соответствующие сборки и автоматически связывал такие вещи, как IPhotoService
- PhotoService
на основе соглашения об именах. Существует еще одно расширение здесь, чтобы помочь в таких вещах. С его помощью вы можете вставить в свой модуль такой код:
Kernel.Scan(s =>
{
s.From(assembly);
s.BindWithDefaultConventions();
});
Вышеприведенный код автоматически привяжет каждый класс в данной сборке к любому интерфейсу, который он реализует, что следует за соглашениями "по умолчанию" (например, Bind<IPhotoService>().To<PhotoService>()
).
Обновление 2
Что касается использования одного и того же DbContext для всего запроса, вы можете сделать что-то вроде этого (используя библиотеку Ninject.Web.Common
, которая требуется расширением MVC):
Bind<SiteContext>().ToSelf().InRequestScope();
Тогда любые зависящие от контекста сервисы, созданные Ninject, будут совместно использовать один и тот же экземпляр для запроса. Обратите внимание, что я лично использовал контекст с более коротким проживанием, поэтому не знаю, как бы вы вынудили контекст быть удаленным в конце запроса, но я уверен, что это не будет слишком сложно.
Ответ 2
Типы IServices
и Services
кажутся мне лишними. Если вы отбросите их и смените конструктор контроллера на
public PhotoController(IPhotoService photoService, IProfileService profileService)
{
_photoService = photoService;
_profileService = profileService;
}
будет более очевидно, на что он в действительности зависит. Более того, когда вы создаете новый контроллер, которому действительно нужен IProfileService, вы можете просто передать IProfileService вместо полного IService, тем самым придав новому контроллеру более легкую зависимость.
Ответ 3
Я мог бы утверждать, что ваши сервисы очень похожи на репозиторий. Посмотрите внимательно на интерфейс:
IQueryable<Photo> GetAll { get; }
Photo GetById(int photoId);
Guid AddPhoto(Guid profileId);
Похож на репозиторий. Может быть, потому, что пример довольно прост, но я вижу смысл иметь сервис, если вы добавите на него логику использования. вместо этих довольно simpel операций CRUD.
И вы можете утверждать, что EFs DbSet и DbContext являются хранилищами и единицей работы приложения... и в этот момент мы вводим новую зону, которая несколько выходит за рамки вопроса.