Подтвердить что ты не робот

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) правильно и достаточно?

Я программист по хобби, поэтому любые комментарии и/или предложения к моему коду приветствуются!

4b9b3361

Ответ 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 являются хранилищами и единицей работы приложения... и в этот момент мы вводим новую зону, которая несколько выходит за рамки вопроса.