Обобщения и слабая связь: можно ли разделить класс‹T› без предположений о типах?

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

Есть ли способ получить класс, который поддерживает параметр универсального типа, чтобы он действительно был как развязанным, так и безопасным? Меня беспокоит то, что даже если вы пишете в общий интерфейс, если производный тип в будущем отличается от объектов, для которых вы изначально закодировали, вы можете столкнуться с некоторыми неприятными ошибками времени выполнения, которые компилятор может не поймать.

Проблема в том, что когда я пишу код для получения данных из базы данных и гидратации моего объекта, я сталкиваюсь с головоломкой при попытке реализовать его на следующем уровне. Если я передам класс, такой как Foo2 ниже, я могу сломать свой код, поскольку нет «неявного» преобразования. Я работал с отражением, чтобы попытаться ослабить ситуацию, но я постоянно возвращаюсь к проблеме, что при получении данных из базы данных мне нужно гидратировать конкретный тип. Затем этот тип создает проблемы с приведением и контролем типов. Кроме того, если бы я хотел абстрагировать все методы для всех многих типов в моей библиотеке сущностей, я мог бы сделать это с помощью отражения, но я все еще сталкиваюсь с проблемой, что общий тип может только обеспечивать гарантии типа для явных операторов «where T: ISomeInterface». . Наконец, эта модель не сработает, если в будущем появится больше производных типов или типов, ответвляющихся от интерфейсов для формирования новых типов. Думая, что мне нужно будет реализовать новые объекты доступа к данным для каждого отдельного типа, когда-либо созданного, потребовались бы изменения на уровне доступа к данным. Это ломает определение слабой связи в моем сознании.

Все это, кажется, заставляет меня вернуться к вопросу: можно ли использовать дженерики по-настоящему слабо связанным образом? Если да, то какие ссылки вы можете предложить?

Упрощенный пример:

public interface IEntity
{
    // stuff for state and methods ...
}
public interface IRepository<T> where T : IEntity
{
    void Save(out int id, T obj);
    T Load(int id);
}
public interface IFoo : IEntity
{
    int Id { get; set; }
    //All other IEntities goodness
    //Some new goodness specific to Foo
}
//Concrete Entities:
public class Foo : IFoo
{
    // blah blah
}
public class Foo2 : IFoo
{
    // new blah blah
}

public class FooRepository : IRepository<Foo> //OOPS, Looks like we have settled in on a concrete type!
{

    public void Save(out int id, Foo obj)
    {
        // ADO.NET code to access Sql ...
        id = 1;// this would actually be the result of the Sql insert output parameter
        return;
    }

    public Foo Load(int id)
    {
        Foo foo = new Foo();
        // ADO.Net code to access Sql
        return foo;
    }
}

Итак, как бы вы поступили с кодом, который может обрабатывать любой объект, производный от IFoo, и при этом иметь возможность возвращать полностью сформированный конкретный тип независимо от того, что программисты делают в будущем? Лучше ли отказаться от общей части и придерживаться внедрения зависимостей? Любые ссылки, рекомендации и т. д. были бы замечательными.


person Zack Jannsen    schedule 12.03.2013    source источник


Ответы (3)


С проблемой, с которой вы столкнулись здесь, вы всегда можете использовать typeof (или, возможно, это другой оператор/метод), чтобы получить динамический тип вашего объекта. Таким образом, вы всегда можете выполнить проверки, прежде чем столкнуться с какой-либо ошибкой приведения.

Вы можете установить конкретное ограничение типа для вашего типа верхнего уровня, но это действительно противоречит цели дженериков.

В противном случае я действительно не вижу, как преодолеть вашу проблему. Если вы используете дженерики, смысл в том, чтобы добавить слабую связь и универсальные операции, которые не зависят от типа. Единственный способ, которым вы можете получить его, — это ввести тип, а затем преобразовать его, улавливая пограничные случаи, чтобы предотвратить ошибки приведения. Что касается вашего вопроса:

Можно ли использовать дженерики по-настоящему слабосвязанным образом?

Я не совсем понимаю, что вы имеете в виду? Семантически из вашего вопроса, конечно, можно. На самом деле это одна из целей дженериков.

Итак, как бы вы поступили с кодом, который может обрабатывать любой объект, производный от IFoo, и при этом иметь возможность возвращать полностью сформированный конкретный тип независимо от того, что программисты делают в будущем?

Это тоже весьма расплывчато. Я мог бы написать:

public SomeType method(IFoo obj)
{
...
}

И это все равно справится с любым IFoo. Для этого не нужны дженерики. Если вы хотите вернуть тип того, что вы конкретно передали, то:

public T method<T>(IFoo obj) where T:IFoo
{
    return (T)obj;
}

Надеюсь это поможет. ЛМК, если я что-то не так понял.

person Kevin Wang    schedule 12.03.2013
comment
Я знаю, что вопрос не сформирован на 100%. Я не хотел слишком углубляться в сорняки по этому поводу, поскольку общие комментарии, которые вы сделали, совпадают с некоторыми из того, с чем я играл. Ваша последняя структура очень интересна. Однако, если я правильно помню, если вы применяете ограничение типа для класса, вам не разрешено применять условия к методам. Часть проблемы заключается в том, что я играл с более сложным отражающим классом, который позволял бы мне передавать любой универсальный тип, а затем возвращать методы сохранения и загрузки из базовых объектов доступа к данным. У меня конфликт. - person Zack Jannsen; 13.03.2013
comment
... конфликт заключается в том, что DataAccessClass‹T›, где T : IBaseObject. IEntity: IBaseObject и IFoo: IEntity. Это создает правильную цепочку, которая мне нужна для иерархии, но если я попытаюсь затем вызвать методы Load и Save для производных классов IFoo из DataAccessClass‹T› и использовать отражение, чтобы решить, какие классы DAO вызывать методы, нет никакого способа обеспечить безопасность типов, кроме исключения исключений, если, как вы сказали, typeof() не равен true. Возможно, мне было бы лучше не агрегировать их так сильно и держать их разделенными на свои собственные классы/методы для доступа к данным. - person Zack Jannsen; 13.03.2013
comment
Я думаю, мне следует добавить, что использование этого кода на уровне выше будет следующим: DataAccessClass‹Foo›.Load(int) и выбор между всеми типами Entity затем определяется в DataAccessClass DAL. Конечно, все имена были упрощены, но Сущностями могут быть самые разные товары (например, книга, музыка, компьютер и т. д.). - person Zack Jannsen; 13.03.2013
comment
Если у вас есть мысли о правильных шаблонах или абстракциях, я был бы признателен. Возможно, мне придется переосмыслить дизайн (например, вставить абстрактный заводской шаблон или что-то в этом роде). - person Zack Jannsen; 13.03.2013
comment
Существует разница между ограничением типа и аргументами метода. Вещи, которые находятся между ‹›, являются ограничениями типов (например, типы, которые вы передаете, чтобы делать все, что хотите). Ваши методы - это любая фактическая переменная. Вы можете воспринимать ‹T› как еще один приемщик аргументов, но только для типов. т.е. метод‹A,B,C,D›() - person Kevin Wang; 15.03.2013
comment
В этом случае вы говорите, что у меня может быть ограничение на уровне класса, а затем определить отдельный общий метод для метода, который накладывает ограничение на локальном уровне метода, которое имеет конкретное ограничение, которое мне нужно. Это точно? class SomeClass‹T1›, где T1: IProduct… public T2 DoSomething‹T2›, где T2: class, T1, IKeyBoard, new()… Это справедливое утверждение? - person Zack Jannsen; 19.03.2013
comment
Верно, ограничения уровня класса и ограничения уровня метода работают очень похоже. Это просто разница в том, куда идет переменная. - person Kevin Wang; 19.03.2013
comment
Ах. Таким образом, незаконно определять ограничение дважды, но у вас может быть второй дженерик, который наследует первый на уровне метода. Интересный. Это то, что я пропустил. Спасибо. Мне придется поэкспериментировать с этим. - person Zack Jannsen; 19.03.2013

Я должен признать, что был немного сбит с толку вашей загадкой; Я не уверен, что это может быть.

Репозиторий, производящий конкретные типы, очевидно, должен знать, как их создавать. Почему бы не создать репозиторий для каждого типа, чтобы каждый из них реализовывал IRepository<IFoo>? Таким образом, вы можете оставить конкретную реализацию конфигурации вашего контейнера DI.

Например, в ninject я бы написал что-то вроде

Bind<IRepository<IFoo>>.To<Foo2Repository>()

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

Я что-то неправильно понял, или это относится к вашей проблеме?

person Ben    schedule 12.03.2013
comment
Интересно... Посмотрю на Bind и поковыряюсь поглубже. Все, что я делал в этом проекте, было с нуля, поэтому может быть полезно изучить больше. Я немного избегал контейнеров IOC, чтобы посмотреть, могу ли я ошибиться в производительности, но быстро перешел к рассмотрению этих инструментов. - person Zack Jannsen; 13.03.2013
comment
Я думаю, что одним из больших преимуществ использования шаблона внедрения зависимостей, как вы, является то, что вы можете использовать контейнер IoC, а тестируемость, очевидно, является вторым. По моему опыту, производительность незначительна. - person Ben; 13.03.2013
comment
Возможно, мне придется это обдумать. Сложность системы — это одна вещь, которую мне нужно абстрагировать, чтобы обеспечить простоту использования. Одна только библиотека типов будет иметь несколько категорий. Каждая категория может иметь несколько конкретных типов. Каждый конкретный тип будет иметь ровно два других производных. Вот почему я попытался упростить этот вопрос. Архитектура будет трехуровневым приложением со службой WCF, соединяющей уровни предметной области с уровнями представления. На данный момент серверная часть — это SQL, но я хочу сохранить возможность выбора. Его нужно будет масштабировать, так как я ожидаю много применений... - person Zack Jannsen; 14.03.2013
comment
... Я полагаю, что тот факт, что я развертываю уровень домена как службу, позволяет распределять нагрузку между экземплярами для масштабируемости. В каком контексте вы использовали контейнеры IoC? Что вы считаете лучшим? (производительность сбалансирована с простотой использования/обслуживаемости). Любые слова предупреждения? PS - Под масштабируемостью я имею в виду миллионы пользователей - person Zack Jannsen; 14.03.2013

Вы можете определить общие вещи в

class BaseFooRepository<TFoo> : IRepository<TFoo> where TFoo : IFoo

а затем поместите более конкретный код в

class Foo2Repository : BaseFooRepository<Foo2>
person default.kramer    schedule 12.03.2013