Как стать автором
Обновить

Комментарии 28

Могли бы Вы развернуть немного мысль? Я так понимаю, Вы предлагаете сделать шаблонный метод у IDevManager?
Я не совсем понял всю бизнес-логику, но ясно увидел, что мы куда-то свернули с «пути в светлое будущее». Если у нас некий dev-manager что-то общее имеет в поведении, но где-то частное в зависимости от девайса, то общее оставить в нём, а частное либо отнаследовать, либо инкапсулировать.
На меня сегодня тупость напала и я совесем не могу понять, чем не угодил COM? Все равно, клиент должен знать, что он хочет от девайса: сам по себе факт, что в SyncBoard_System2 добавили setPeriod ничего не даст. Поэтому, можно сделать: p = device->Query(IID_SetPeriod); if(p) p->SetPeriod(100500);
То же самое делается без всяких закрытых технологий на чистых плюсах через dynamic_cast, кстати.
Какие закрытые технологии? COM — это всего лишь концепт, который применяет не только Microsoft, но и Mozilla Firefox (XPCOM), и OpenOffice (XInterface) и многие-многие другие.
Не знаю как Вам ответить, т.к. я даже не рассматривал подобный вариант. Была полная уверенность, что задача решается средствами языка, без привлечения иных технологий.
Это можно же сделать средствами языка; как это сделано в COM — это просто подход, когда вы получаете указатель на интерфейс по его цифровому Id.

Вообще мне кажется что тут выбор архитектуры может зависить от какого рода обработка в клиентах. В примитивном виде это похоже на вектор из элементов типа IDevice, и клиент может перебирать их, приводить к тем интерфейсам которые он умеет обрабатывать и обрабатывать. Вопрос может быть в том что такой подход усложняеет разработку клиентов, однако не зная что там за обработка, трудно сказать это наверника.

Мне чем-то напомнило проблему о которой рассказывалось в этом видео: channel9.msdn.com/Events/GoingNative/2013/Inheritance-Is-The-Base-Class-of-Evil Там немного другая задача, но есть некоторые интересные мысли.
Я не знаю как с этим обстоят дела в С++ (со времен универа не писал на нем), но в большинстве случаев такая проблема легко решается через Dependency Injection.
В частости в дотнете я использую компонент контейнеры (Autofac например), который резолвит зависимости сам. Для вашего примера:
есть интерфейс IDevice. Есть классы для разных датчиков, реализущих этот интерфейс. Мы регистрируем все эти классы в контейнере.
Теперь о клиентах. Как вы сказали, клиенты могут требовать различные устройства, и различные их комбинации. Проще всего (и констистентнее) внедрять зависимость через конструктор. Причем внедрять можно различные агрегаты — как непостредственно типы, так и их коллекции, а также функторы от них — например фабрики. Пример:
class TemperatureDisplay 
{
  public TemperatureDisplay(TemperatureSensor sensor) 
  {
    mySensor = sensor;  
  }
}

class AllSensorInfoDisplay
{
  public  AllSensorInfoDisplay(IEnumerable<IDevice> allSensors) 
  {
    /// ...
   }
}

В первом случае мы запросили только температурный датчик, во втором мы запросили все датчики в системе. Клиенты при этом подхоже нужно тоже создавать через контейнер, чтобы он смог внедрить зависимости.
Будет лучше, если для даже конкретных реализаций вы сделаете по интерфейсу типа ITemperatureSensor со всеми нужными методами и будете инжектить уже интерфейс. Это как минимум признак хорошего тона, плюс ко всему повышает гибкость системы (хотя насчет С++ не знаю, в связи с отсутвствием интерфейсов и наличием множественного наследование там скорее всего другие практики).
Еще один пункт — бывают ситуации, когда нужно заинжектить набор объектов по какому либо признаку, для нашего примера — предположим, могут быть устройства, имеющие расположение, т.е. метод GetPosition(), и мы хотим сделать клиента, который сможет взять все такие устройства и отобразить их позиции. При обычном подходе, пришлось бы писать кучу кода с выборкой таких устройсв и т.д. При инъекции сделаем следующее: добавим интерфейс
interface IPositionedDevice : IDevice {
  Position GetPosition();
}

И реализуем его во всех нужных классах устройств, имеющих расположение. Тогда в классе клиенте достаточно заинжектить список IEnumerable<IPositionedDevice> — и мы получим все устройства с позицией.

Как я уже сказал — я использую Autofac, при этом я написал свой регистратор типов, которых работает на атрибутах. Т.е. я не регистрирую новые классы руками, а просто добавляю аттрибут Component у класса. Как такое делает в С++ опять же не скажу.
Ну хорошо. Передали мы клиентам список IEnumerable<IDevice> или IEnumerable<IPositionedDevice>. В списке присутствуют классы, реализующие эти интерфейсы и дополняющие их своими специфическими методами(читай — новыми). Как Вы получите в C# доступ к этим специфическим методам, если они даже не объявлены в интерфейсе? В C++ — это dynamic_cast/static_cast к нужному типу (приведение к типу). Я как раз от этого и пытался уйти-)
Погодите. Мне кажется, вы не до конца осознаете принцип DI.
> В списке присутствуют классы, реализующие эти интерфейсы и дополняющие их своими специфическими методами(читай — новыми)
Если есть такой класс, значит для него есть специфический клиент, который явно этот метод должен вызывать, верно? Значит сделайте в этом клиенте инжект именно этого специфического типа, в чем проблема?
class ClientToCallSomeSpecificMethod 
{
	public ClientToCallSomeSpecificMethod(DeviceWithVerySpecificMethod device)
	{
		device.SpecificMethod();
	}
}
Вы все верно говорите, но не замечаете пока саму проблему, о которой я тут писал.
Давайте за основу возьмем Ваш последний пример. Сейчас передается(инжектится) пока только одно устройство. А если необходимо передать несколько разных
DeviceWithVerySpecificMethod? Как изменится Ваш код? Будем передавать в конструктор сразу несколько DeviceWithVerySpecificMethod? Или воспользуемся коллекцией?
Если у вас есть РАЗНЫЕ с точки зрения типа устройства, т.е. у вас есть
— устройство DeviceWithSpecificMethod1 с методом Method1()
— устройство DeviceWithSpecificMethod2 с методом Method2()
и у вас есть клиент, которому нужны ОБА этих устройства, причем нужны для вызова именно этих СПЕЦИФИЧЕСКИХ методов Method1 и Method2
то да, вы просто инжектите 2 параметра в конструктор указывая явно эти 2 типа
public ClientFor2SpecificDevices(DeviceWithSpecificMethod1  dev1, DeviceWithSpecificMethod2 dev2) {
  dev1.Method1();
  dev2.Method2();
}

Не вижу в этом ничего плохого. Более того, если у вас есть на каждый из этих типов по несколько экземпляров устройств, например 2 устройства DeviceWithSpecificMethod1 и 3 устройства DeviceWithSpecificMethod2, то просто берем и инжектим коллекции:
public ClientFor2SpecificDevices(IEnumerable<DeviceWithSpecificMethod1>  devList1, IEnumerable<DeviceWithSpecificMethod2> devList2) {
  foreach(var dev1 in devList1) {
    dev1.Method1();
  }
  foreach(var dev2 in devList2) {  
    dev2.Method2();
  }
}


Более того, это не отменяет возможности получить оба этих устройства (или 5, если брать второй пример) в одном списке, если вам не требуются эти самые специфические методы, то мы просто инжексим IEnumerable<IDevice>
К этому я Вас и вел:) Поймите меня правильно, все, что Вы сейчас предложили имеет право на жизнь. Я рассматривал этот вариант в разных вариациях, просто не стал в и так длинную статью помещать этот и другие варианты. Только те, что отображают плавный переход мысли.

Теперь внимание, вопрос:
Получается, что от системы к системе, у клиентов разный конструктор. Передаваемые параметры ведь разные, верно?
Значит, код, который создает клиентов, тоже нужно менять. Тут, само-собой, мы пользуемся паттерном фабрика. Но как передавать параметры конструкторам? Придется придумывать, что нибудь вот такое .
Секундочку, Вам не показалось, что уже как-то много сущностей стало появляться? Еще нам наверняка придется сделать фабрику и для IConfiguration (из приведенной ссылки). Вам не кажется, что какой-то замкнутый круг?:)
не не не. Я вам в первом комменте указал, что при таком подходе, все создается через IoC контейнер! В том числе и ваши клиенты. Как раз этот подход избавляет от написания кучи фабрик у прочего шлакокода, который нужен только для инфраструктуры.
При использовании IoC все классы, которым требуются зависимости, создаются через контейнер, при этом он сам занимается разруливанием того, что нужно передать в конструктор — именно для этого и существует IoC. Выглядит примерно так:
//некоторый entry point вашего приложения:
var container = new Container();
container.Register<DeviceWithSpecificMethod1>().AsSelf().AsImplementedInterfaces().SingleInstance();
container.Register<DeviceWithSpecificMethod2>().AsSelf().AsImplementedInterfaces().SingleInstance();
container.Register<ClientFor2SpecificDevices>().AsSelf().AsImplementedInterfaces().SingleInstance();
container.Build();

// гдето в другом месте
ClientFor2SpecificDevices client = container.Resolve<ClientFor2SpecificDevices>();
// здесь вы получили ваш клиент, в который автоматически попали нужные устройства, заметьте, вы нигде не написали new


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

Если вы не верите в такой подход — то могу сказать, что именно так написан Решарпер, а в нем не один миллион строк кода. Правда мы используем свой компонент контейнер, заточенный под наши нужды.

Более того такой подход позволяет реализовывать полиморфизм еще одним более гибким методом, не используя виртуальные методы языка. Точнее мы не заменяем полиморфизм языка, а дополняем его в тех местах, где использовать его по некоторым причинам нельзя или проблемно. В общем тема отдельной одной статьи, в который раз оправдываюсь на Хабре, что нет времени ее написать)
Теперь я Вас понял. В C++ таких плюшек — нет:) Как минимум в готовом виде.
В плюсах вроде бы тоже все это есть:
вот несколько фреймворков, которые я нагуглил:
code.google.com/p/wallaroo/ — выглядит достаточно ужасно
code.google.com/p/hypodermic/ — вполне неплохо
github.com/phs/sauce — тоже вполне ок

Конечно, по сравнению с дотнетом для меня это «вырвиглаз», но все это изза остутствия рефлексии. Однако, оно имеет право на жизнь. Я думаю, вам стоит попробовать заюзать какой нибудь из этих контейнеров и попробовать «принудить» себя рассуждать в терминах зависимостей, исключив мысли о явном создании компонентов приложения и протаскивании их через множество слоев. Через некоторое время вы осознаете, что это очень мощный подход, который позволяет писать очень чистый и минимально избыточный код (опять же с поправкой на возможности С++).

Вот тут вроде вполне годный цикл статей про разные контейнеры в С++
ledentsov.de/2014/01/01/quest-for-dependency-injection-library-part2-some-sauce/
Благодарю, за ссылки и науку.
Да не за что) Рад помочь.
Столько потуг, а в итоге все свелось к вызовам:
v4.callMethod<1, int>(0, 1);
v4.callMethod<0, int>(10, 31);

То есть диспетчеризация происходит по первому типу-параметру. Так как это все происходит в compile-time, то есть никакого runtime полиморфизма, то чем это лучше, чем:
v4.callMethod1(0, 1);
v4.callMethod0(10, 31);

?

При этом callMethodX можно заменить на нормальные имена, а внутри будет банальный pimpl, возможно с typetest, и не нужны будут все эти пляски с кучей шаблонов.

Или я что-то не понимаю?
Если вкратце, то Вы предлагаете вариант №1. О нем я расписал в статье.
Возможно, я не читал внимательно все варианты. Вопрос с том, чем последний вариант лучше.
Опять же, вкратце. Тем, что вызывающему коду нужно знать минимум информации об используемом объекте, а это позволяет повторно использовать код, с минимальными переделками. Тем, что если допущена ошибка, то так же как и с именованными (не шаблонными) методами, будет генерироваться ошибка компиляции.
Ну если использовать отдельные методы, то будет тоже самое:
1) Минимум информации — не нужно знать какой селектор что обозначает.
2) При ошибке — ошибка компиляции
3) Код также используется с минимальными переделками, ибо нет разницы callMethod<X,...>(...) или callMethodX(...)
Я не могу в комментарии снова написать всю статью, простите:) Ответы на Ваши вопросы изложены полностью в варианте 1.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории