Как сломать циклические зависимости между репозиториями - PullRequest
9 голосов
/ 09 марта 2012

Для начала, нет, я не использую ORM, и мне не разрешено. Я должен свернуть свои репозитории вручную, используя ADO.NET.

У меня есть два объекта:

public class Firm
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public virtual IEnumerable<User> Users { get; set; }
}

public class User
{
    public Guid Id { get; set; }
    public string Username { get; set; }
    public Firm Firm { get; set; }
}

обратите внимание на ссылки друг на друга, у Фирмы есть список пользователей, у каждого пользователя есть только одна фирма.

Теперь я хочу создать свои репозитории:

public interface IFirmRepository
{
    IEnumerable<Firm> FindAll();
    Firm FindById(Guid id);
}

public interface IUserRepository
{
    IEnumerable<User> FindAll();
    IEnumerable<User> FindByFirmId(Guid firmId);
    User FindById(Guid id);
}

Пока все хорошо. Я хочу загрузить Фирму для каждого пользователя из моего UserRepository. FirmRepository знает, как создать фирму из постоянства, поэтому я не хотел бы хранить эти знания в FirmRepository.

public class UserRepository : IUserRepository
{
    private IFirmRepository _firmRepository;

    public UserRepository(IFirmRepository firmRepository)
    {
        _firmRepository = firmRepository;
    }

    public User FindById(Guid id)
    {
        User user = null;
        using (SqlConnection connection = new SqlConnection(_connectionString))
        {
            SqlCommand command = connection.CreateCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = "select id, username, firm_id from users where u.id = @ID";
            SqlParameter userIDParam = new SqlParameter("@ID", id);
            command.Parameters.Add(userIDParam);
            connection.Open();
            using (SqlDataReader reader = command.ExecuteReader())
            {
                if (reader.HasRows)
                {
                    user = CreateListOfUsersFrom(reader)[0];
                }
            }
        }
        return user;
    }

    private IList<User> CreateListOfUsersFrom(SqlDataReader dr)
    {
       IList<User> users = new List<User>();
       while (dr.Read())
       {
           User user = new User();
           user.Id = (Guid)dr["id"];
           user.Username = (string)dr["username"];
           //use the injected FirmRepository to create the Firm for each instance of a User being created
           user.Firm = _firmRepository.FindById((Guid)dr["firm_id"]);
       }
       dr.Close();
       return users;
    }

}

теперь, когда я иду, чтобы загрузить любого пользователя через UserRepository, я могу попросить FirmRepository создать для меня фирму User. Пока что здесь нет ничего сумасшедшего.

Теперь проблема.

Я хочу загрузить список пользователей из моего FirmRepository. UserRepository знает, как создать пользователя из постоянства, поэтому я хотел бы сохранить эти знания в UserRepository. Итак, я передаю ссылку на IUserRepository в FirmRepository:

public class FirmRepository : IFirmRepository
{
    private IUserRepository
    public FirmRepository(IUserRepository userRepository)
    {

    }
}

Но теперь у нас проблема. FirmRepository зависит от экземпляра IUserRepository, а UserRepository теперь зависит от экземпляра IFirmRepository. Поэтому один репозиторий нельзя создать без экземпляра другого.

Если я оставлю контейнеры IoC вне этого уравнения (и мне следует, чтобы в модульных тестах B / C не использовались контейнеры IoC), у меня не было бы возможности выполнить то, что я пытаюсь сделать.

Нет проблем, но я просто создам FirmProxy для отложенной загрузки коллекции Users из Фирмы! Это лучшая идея, потому что я не хочу загружать ВСЕХ пользователей все время, когда я иду за Фирмой или списком Фирм.

public class FirmProxy : Firm
{
    private IUserRepository _userRepository;
    private bool _haveLoadedUsers = false;
    private IEnumerable<User> _users = new List<User>();

    public FirmProxy(IUserRepository userRepository)
        : base()
    {
        _userRepository = userRepository;
    }

    public bool HaveLoadedUser()
    {
        return _haveLoadedUsers;
    }

    public override IEnumerable<User> Users
    {
        get
        {
            if (!HaveLoadedUser())
            {
                _users = _userRepository.FindByFirmId(base.Id);
                _haveLoadedUsers = true;
            }
            return _users;
        }
    }

}

Итак, теперь у меня есть хороший прокси-объект для облегчения отложенной загрузки. Поэтому, когда я продолжаю создавать фирму в FirmRepository из постоянства, я вместо этого возвращаю FirmProxy.

public class FirmRepository : IFirmRepository
{

    public Firm FindById(Guid id)
    {
        Firm firm = null;
        using (SqlConnection connection = new SqlConnection(_connectionString))
        {
            SqlCommand command = connection.CreateCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = "select id, name from firm where id = @ID";
            SqlParameter firmIDParam = new SqlParameter("@ID", id);
            command.Parameters.Add(firmIDParam);
            connection.Open();
            using (SqlDataReader reader = command.ExecuteReader())
            {
                if (reader.HasRows)
                {
                    firm = CreateListOfFirmsFrom(reader)[0];
                }
            }
        }
        return firm;
    }

private IList<Firm> CreateListOfFirmsFrom(SqlDataReader dr)
{
    IList<FirmProxy> firms = new List<FirmProxy>([need an IUserRepository instance here!!!!]);
    while (dr.Read())
    {

    }
    dr.Close();
    return firms;
}

Но это все еще не работает !!!

Чтобы вернуть FirmProxy вместо Firm, мне нужно иметь возможность создать FirmProxy в своем классе FirmRepository. Ну, FirmProxy принимает экземпляр IUserRepository, так как UserRepository содержит сведения о том, как создать объект User из персистентности. В результате, когда FirmProxy требуется IUserRepository, мой FirmRepository теперь также нуждается в IUserRepository, и я возвращаюсь к исходной точке!

Итак, учитывая это длинное подробное объяснение / исходный код, как я могу создать экземпляр пользователя из FirmRepository и экземпляр Firm из UserRepository без:

  1. положить код создания пользователя в FirmRepository. Мне это не нравится Почему FirmRepository должен знать что-либо о создании экземпляра пользователя? Для меня это нарушение SoC.
  2. Не используется шаблон Service Locator. Если я иду по этому пути, я чувствую, что это очень сложно проверить. Кроме того, конструкторы объектов, которые принимают явные зависимости, делают эти зависимости очевидными.
  3. внедрение свойства вместо внедрения конструктора. Это ничего не меняет, мне все еще нужен экземпляр IUserRepository при создании нового FirmProxy независимо от того, как зависимость вводится в FirmProxy.
  4. Необходимость «заглушить» либо объект «Фирма», либо объект «Пользователь» и указать идентификатор «Фирма» для пользователя, например, вместо «Фирма». Если я просто делаю идентификаторы, то требование загрузки Фирмы из UserRepository исчезает, но вместе с этим уходит богатство возможности запрашивать объект Фирмы для чего-либо в контексте данного экземпляра Пользователя.
  5. Обращение к ОРМ. Я опять хочу это сделать, но не могу. Нет ОРМ. Это правило (и да, это дерьмовое правило)
  6. сохранить все мои зависимости для инъекций как зависимости, внедряемые из самого нижнего уровня приложения, а именно пользовательского интерфейса (в моем случае, веб-проекта .NET). Нет мошенничества и использования кода IoC в FirmProxy, чтобы создать для меня соответствующую зависимость. Во всяком случае, это в основном использует шаблон Service Locator.

Я думаю о NHiberante и Enitity Framework, и, похоже, у них нет проблем с выяснением того, как создать sql для простого примера, который я представил.

Есть ли у кого-нибудь еще какие-либо идеи / методы /и т.д. ... это поможет мне достичь того, что я хочу сделать без ORM?

Или, может быть, есть другой / лучший способ подойти к этому?Хочу, чтобы я не хотел потерять возможность доступа к Фирме от Пользователя или получить список Пользователей для данной Фирмы

1 Ответ

5 голосов
/ 09 марта 2012

Вам нужно более четко подумать о ваших совокупных корневых объектах, то есть о фокусе каждого репозитория.Вы не создаете репозиторий для каждой таблицы в вашей базе данных, это не является целью.Цель состоит в том, чтобы идентифицировать совокупный корневой объект, с которым вам нужно работать, включая дочерние таблицы / записи, то есть пользователя (фирма, адрес, права доступа).Это то, что ваш репозиторий вернет в ваше приложение.

Трудность, которую вы испытываете, показывает вам, что ваши репозитории неправильно структурированы.Каждый раз, когда я нахожу, что мой код становится слишком сложным, у меня появляется предупреждение, что я, вероятно, делаю это неправильно, я этим живу;)

...