Жил — был древний код эпохи динозавров
Дано: адов кодярник работающий с 16ю разными версиями одного и того же «ах какого» продукта. COM, Interop, интерфейсы, реализации, сигнлтоны с факторями, паттерны с антипаттернами, модули и прочие ошметки крывавого ынтырпрайзу. Стандартный набор. Рос, мужал и матерел тот кодярник лет семь. Пока однажды очередной фикс не привел к исправлению массового копипаста в 16 модулях. Если кому интересно — foreach на for меняли.
Помучившись, провели исследование. Копипаст на 95% идентичен, различаются только имена пакетов из интеропов.
А можно ли как-то писать так чтобы не оборачивать сотни и сотни функций в свои врапперы, плюс ручками боксинг / анбоксинг этих врапперов?
Есть же ключевое слово dynamic!
И тогда адские макароны вот такого чудесного вида
public abstract class Application : IDisposable { public abstract void Close(); public abstract Document CreateDocument(); public abstract Document OpenDocument(string doc_path); // еще 200 методов // куча пропертей типа версий, путей и так далее void IDisposable.Dispose() { Close(); } } public class ClientApplication : Application { protected ClientApplication(){ string recovery_path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); recovery_path = Path.Combine( recovery_path, String.Format( @"...\Version {0}\en_GB\Caches\Recovery", Version)); try { foreach (string file in Directory.GetFiles(recovery_path)){ try { File.Delete(file); } catch { } } } catch {} // еще подпорок из палок и веревок } public override void Close() { if (Host != null) { Marshal.ReleaseComObject(Host); Host = null; } } } public class ClientApplication7_5 : ClientApplication { protected ClientApplication7_5() { Type type = Type.GetTypeFromProgID("....Application." + Version, true); _app = Activator.CreateInstance(type) as Interop75.Application; Host = app; // ... } public override Document CreateDocument() { return new ClientDocument7_5(this, _app.Documents.Add()); } public override Document OpenDocument(string doc_path) { return new ClientDocument7_5(this, _app.Open(doc_path, true, ...) as Interop75.Document); } // и еще 200 врапперов public override ComObject Host { get { return _app; } set { _app = value as Interop75.Application; } } private Interop75.Application _app; // и еще пропертей с версиями прог-айди и прочим } public class ServerApplication : Application { public ServerApplication() {} ... } // та же трава что и для клиент аппликейшен, еще 8 раз
становится ненужными, а код который это безобразие использовал
var app = Factory.GetApplication(); var doc = app.Documents.Add(); doc.DocumentPreferences.PreserveLayoutWhenShuffling = false; doc.DocumentPreferences.AllowPageShuffle = true; doc.DocumentPreferences.StartPageNumber = 1;
не меняется.
Профит? Ура, работает! Два десятка мегабайт полунагенеренного ужастика удачно выкидываем в мусорку. Поддержка новых версий радикально упрощается.
Литовский праздник «обломайтис»
Запускаем тесты. БАЦ!
Не, пока все вызовы того кома возвращают OK — то и работает тоже супер. Но стоило дождаться теста
try { var app = Factory.GetApplication(); var doc = app.Documents.Add(); doc.DocumentPreferences.PreserveLayoutWhenShuffling = false; doc.DocumentPreferences.AllowPageShuffle = true; doc.DocumentPreferences.StartPageNumber = -1; } catch (COMException ok) { .... // должны быть тут и красиво в лог записать "нишмагла" } catch(Exception bad) { ... // мы вот тут, а bad - это NullReferenceException БЕЗ StackTrace!!! }
Шок, скандалы, интриги, расследования. Если кому интересно — подтвержденный баг в микрософте, пофикшен будет не ранее 5.0. Грустно и скучно.
Пытливый ум не дает покоя — ведь если ходить через интеропы то там все как надо? Отладчик показывает тип нашего документа как System.__ComObject. А как же RCW? Просто не вычислило?
Меняем тест на
try { var app = Factory.GetApplication(); var doc = app.Documents.Add() as Interop75.Document; doc.DocumentPreferences.PreserveLayoutWhenShuffling = false; doc.DocumentPreferences.AllowPageShuffle = true; doc.DocumentPreferences.StartPageNumber = -1; } catch (COMException ok) { .... // и мы опять на своем месте } catch(Exception bad) { ... }
и… тест пройден.
Гипотеза интересна. Так может оно просто не может вычислить тип? Проверяем
var app = Factory.GetApplication(); var doc = app.Documents.Add(); var typeName = Microsoft.VisualBasic.Information.TypeName(doc);
Хм хм. Вполне себе.
Идеи закончились.
Но постойте — есть же сырцы? Смотрим, курим, восхищаемся мастерству запутывания. Начали отсюда: __ComObject. Плавно перетекли сюда: Type.cs. Закончили ildasm. В процессе курева пришло понимание — так там явно несколько мест обрабатывающих эти комы по разному. А что будет если заменить
doc.DocumentPreferences.StartPageNumber = -1;
на
Type type = doc.DocumentPreferences.GetType(); type.InvokeMember("StartPageNumber", BindingFlags.SetProperty, null, doc.DocumentPreferences, new object[] { -1 });
По идее — ничего?
Галантерейщик и кардинал — это сила
А вот и меняется. Тест снова пройден. И что делать? Превращать такой красивый код в макароны — не улыбается, да и много его.
Поздно, вечер, пытаюсь толсто потроллить и разрядить обстановку — так может свою реализацию динамиков подсунем — на рефлектах? Еще не закончив мысль понимаю — а это мысль!
Пробуем.
public class ComWrapper : DynamicObject { public ComWrapper(object comObject) { _comObject = comObject; _type = _comObject.GetType(); } public object WrappedObject { get { return _comObject; } } // вдруг кому будет надо // стандартно пропертя гет + сет public override bool TryGetMember(GetMemberBinder binder, out object result) { result = Wrap(_type.InvokeMember(binder.Name, BindingFlags.GetProperty, null, _comObject, null)); return true; } public override bool TrySetMember(SetMemberBinder binder, object value) { _type.InvokeMember( binder.Name, BindingFlags.SetProperty, null, _comObject, new object[] { Unwrap(value) } ); return true; } // та же трава про вызов метода public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { result = Wrap(_type.InvokeMember( binder.Name, BindingFlags.InvokeMethod, null, _comObject, args.Select(arg => Unwrap(arg)).ToArray() )); return true; } // наш ручной боксинг - анбоксинг private object Wrap(object obj) { return obj != null && obj.GetType().IsCOMObject ? new ComWrapper(obj) : obj; } private object Unwrap(object obj) { ComWrapper wrapper = obj as ComWrapper; return wrapper != null ? wrapper._comObject : obj; } // очевидно то что нам передали в конструкторе + тип переданного чтобы сто раз не считать private object _comObject; private Type _type; }
Прекрасно — все делает сам, работает как надо, все что нужно — это обернуть им результат Factory.GetApplication(). Прямо там и оборачиваем. Есть правда нюанс — забыли про коллекции. Так что чуть погодя добавили еще и такое:
// наш енумератор на коленке private IEnumerable Enumerate() { foreach (var item in (IEnumerable)_comObject) yield return Wrap(item); } // автоконвертация к enumerable public override bool TryConvert(ConvertBinder binder, out object result) { if (binder.Type.Equals(typeof(IEnumerable)) && _comObject is IEnumerable) { result = Enumerate(); return true; } result = null; return false; } // и поддержка работы как с массивом, по индексу. На всякий случай public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) { if (indexes.Length == 1) { dynamic indexer = _comObject; result = Wrap(indexer[indexes[0]]); return true; } result = null; return false; } public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) { if (indexes.Length == 1) { dynamic indexer = _comObject; indexer[indexes[0]] = Unwrap(value); return true; } return false; }
Вот теперь — победа.
Вдруг кому пригодится.
ссылка на оригинал статьи http://habrahabr.ru/post/252371/
Добавить комментарий