Даб работа по методам программирования. обновлено пронько. 1. Классы в C#. Модификаторы доступа. Поля, свойства, индексаторы
Скачать 0.78 Mb.
|
9. Применение технологии LINQ to Objects для работы с массивами и коллекциями. LINQ (Language-Integrated Query) представляет простой и удобный язык запросов к источнику данных. Термин "LINQ to Objects" означает использование запросов LINQ с любой коллекцией IEnumerable или IEnumerable В общем смысле LINQ to Objects представляет собой новый подход к коллекциям. Раньше нужно было написать сложные циклы foreach, определяющие порядок извлечения данных из коллекции. При использовании LINQ пишется декларативный код, описывающий, какие данные необходимо извлечь. Кроме того, запросы LINQ предлагают три основных преимущества по сравнению с традиционными циклами foreach: Они более краткие и удобочитаемые, особенно при фильтрации нескольких условий. Они предоставляют широкие возможности фильтрации, упорядочивания и группировки с минимумом кода приложения. Они могут переноситься в другие источники данных практически без изменений. В общем, чем сложнее операция, которую нужно выполнить с данными, тем больше преимуществ вы получаете при использовании LINQ вместо традиционных способов итерации. В следующем примере показан полный пример использования LINQ to Objects. Сначала создается источник данных, затем определяется выражение запроса и выполняется этот запрос в инструкции foreach
LINQ предоставляет множество операторов, подробнее с которыми можно ознакомиться в документации по C#. 10. Рефлексия. Рефлексия представляет собой процесс выявления типов во время выполнения приложения. Каждое приложение содержит набор используемых классов, интерфейсов, а также их методов, свойств и прочих кирпичиков, из которых складывается приложение. И рефлексия как раз и позволяет определить все эти составные элементы приложения. То есть основная задача рефлексии - это исследование типов. Основной функционал рефлексии сосредоточен в пространстве имен System.Reflection. В нем мы можем выделить следующие основные классы: Assembly: класс, представляющий сборку и позволяющий манипулировать этой сборкой AssemblyName: класс, хранящий информацию о сборке MemberInfo: базовый абстрактный класс, определяющий общий функционал для классов EventInfo, FieldInfo, MethodInfo и PropertyInfo EventInfo: класс, хранящий информацию о событии FieldInfo: хранит информацию об определенном поле типа MethodInfo: хранит информацию об определенном методе PropertyInfo: хранит информацию о свойстве ConstructorInfo: класс, представляющий конструктор Module: класс, позволяющий получить доступ к определенному модулю внутри сборки ParameterInfo: класс, хранящий информацию о параметре метода Эти классы представляют составные блоки типа и приложения: методы, свойства и т.д. Но чтобы получить информацию о членах типа, нам надо воспользоваться классом System.Type. Класс Type представляет изучаемый тип, инкапсулируя всю информацию о нем. С помощью его свойств и методов можно получить эту информацию. Некоторые из его свойств и методов: Метод FindMembers() возвращает массив объектов MemberInfo данного типа Метод GetConstructors() возвращает все конструкторы данного типа в виде набора объектов ConstructorInfo Метод GetEvents() возвращает все события данного типа в виде массива объектов EventInfo Метод GetFields() возвращает все поля данного типа в виде массива объектов FieldInfo Метод GetInterfaces() получает все реализуемые данным типом интерфейсы в виде массива объектов Type Метод GetMembers() возвращает все члены типа в виде массива объектов MemberInfo Метод GetMethods() получает все методы типа в виде массива объектов MethodInfo Метод GetProperties() получает все свойства в виде массива объектов PropertyInfo Свойство Name возвращает имя типа Свойство Assembly возвращает название сборки, где определен тип Свойство Namespace возвращает название пространства имен, где определен тип Свойство IsArray возвращает true, если тип является массивом Свойство IsClass возвращает true, если тип представляет класс Свойство IsEnum возвращает true, если тип является перечислением Свойство IsInterface возвращает true, если тип представляет интерфейс Получение типа Чтобы управлять типом и получать всю информацию о нем, нам надо сперва получить данный тип. Это можно сделать тремя способами: с помощью оператора typeof, с помощью метода GetType() класса Object и применяя статический метод Type.GetType(). Получение типа через typeof: Type myType = typeof(Person); Console.WriteLine(myType); // Person public class Person { public string Name { get;} public Person(string name) => Name = name; } Здесь определен класс Person с некоторой функциональностью. И чтобы получить его тип, используется выражение Type myType = typeof(Person); Получение типа с помощью метода GetType, унаследованного от класса Object: Person tom = new Person("Tom"); Type myType = tom.GetType(); В отличие от предыдущего примера здесь, чтобы получить тип, надо создавать объект класса. И третий способ получения типа - статический метод Type.GetType(): Type? myType = Type.GetType("Person", false, true); Первый параметр указывает на полное имя класса с пространством имен. Второй параметр указывает, будет ли генерироваться исключение, если класс не удастся найти. В данном случае значение false означает, что исключение не будет генерироваться. И третий параметр указывает, надо ли учитывать регистр символов в первом параметре. Значение true означает, что регистр игнорируется. Поскольку указанный тип может отсутствовать, то метод возвращает объект nullable-типа В данном случае класс основной программы и класс Person находятся в глобальном пространстве имен. Однако если тип располагается в другом пространстве имен, то его также надо указать: Type? myType = Type.GetType("PeopleTypes.Person", false, true); Console.WriteLine(myType); // PeopleTypes.Person namespace PeopleTypes { public class Person { public string Name { get;} public Person(string name) => Name = name; } } В качестве альтернативы можно применять оператор typeof, передавая в него имя типа с указанием пространства имен: Type myType = typeof(PeopleTypes.Person); Если нужный нам тип находится в другой сборке dll, то после полного имени класса через запятую указывается имя сборки: Type myType = Type.GetType("PeopleTypes.Person, MyLibrary", false, true); Теперь исследуем тип и получим некоторую информацию о нем. Type myType = typeof(PeopleTypes.Person); Console.WriteLine($"Name: {myType.Name}"); // получаем краткое имя типа Console.WriteLine($"Full Name: {myType.FullName}"); // получаем полное имя типа Console.WriteLine($"Namespace: {myType.Namespace}"); // получаем пространство имен типа Console.WriteLine($"Is struct: {myType.IsValueType}"); // является ли тип структурой Console.WriteLine($"Is class: {myType.IsClass}"); // является ли тип классом namespace PeopleTypes { class Person { public string Name { get; } public Person(string name) => Name = name; } } Поиск реализованных интерфейсов Чтобы получить все реализованные типом интерфейсы, надо использовать метод GetInterfaces(), который возвращает массив объектов Type: Type myType = typeof(Person); Console.WriteLine("Реализованные интерфейсы:"); foreach (Type i in myType.GetInterfaces()) { Console.WriteLine(i.Name); } public class Person : IEater, IMovable { public string Name { get;} public Person(string name) => Name = name; public void Eat() => Console.WriteLine($"{Name} eats"); public void Move()=> Console.WriteLine($"{Name} moves"); } interface IEater { void Eat(); } interface IMovable { void Move(); } Так как каждый интерфейс представляет объект Type, то для каждого полученного интерфейса можно также применить выше рассмотренные методы для извлечения информации о свойствах и методах Но пока все примеры выше никак не использовали рефлексию. В следующих темах рассмотрим, как можно с помощью рефлексии получать компоненты типа и обращаться к ним, например, изменять значения приватных полей класса. 11. Ограничения наследования реализации. Примеры привносимых проблем. Наследование реализации (implementation inheritance) означает, что тип происходит от базового типа, получая от него все поля-члены и функции-члены. При наследовании реализации производный тип адаптирует реализацию каждой функции базового типа, если только в его определении не указано, что реализация функции должна быть переопределена. Наследование не учитывает будущие изменения родительского класса. Ведь когда вы наследуете новый класс от существующего, вы подписываете контракт о том, что новый класс всегда будет вести себя как существующий, возможно расширяя его поведение в некоторых местах. Для простоты понимания представьте себе, что вы разработчик и встретили своего друга, тоже разработчика. Поболтали, обсудили работу и поняли, что занимаетесь одним и тем же, только вы еще немного управляете командой. И тут вы можете сделать вывод – вы такой же как друг (вот оно наследование), но только делаете немного больше (расширение в дочернем классе). Прошло время, и ваш друг немного забросил разработку и начал заниматься больше менеджментом (но по-прежнему время от времени пишет код, да и писать не разучился). Получается, у родительского класса появились новые функции, которые все дочерние классы по умолчанию наследуют. И вы нежданно-негаданно начинаете тоже заниматься менеджментом (тут может ничего плохого и нет), по крайней мере так следует из вашего “отношения наследования”. Использование наследование для повторного использования кода ломает инкапсуляцию. Для демонстрации очередной проблемы наследования реализации можно привести классический пример о проблеме квадрата/прямоугольника: В этом примере класс Square (представляющий квадрат) неправильно определен как подтип класса Rectangle (представляющего прямоугольник), потому что высоту и ширину прямоугольника можно изменять независимо; а высоту и ширину квадрата можно изменять только вместе. Поскольку класс User полагает, что взаимодействует с экземпляром Rectangle, его легко можно ввести в заблуждение. 12. Делегирование (композиция) вместо наследования реализации. Мотивация. Примеры использования. Композиция вместо наследования реализации применяется в случаях, когда необходимо получить только часть от объекта. Наследовав объект, мы получим весь его функционал, включая поля, свойства, методы, но, если нам необходимо получить только часть данного функционала? Нам следует использовать композицию интерфейса с необходимыми нам методами и свойствами. Делегирование (композиция) — это техника в объектно-ориентированном программировании, когда объект делегирует часть своих обязанностей другому объекту. Делегирование может использоваться в качестве альтернативы наследованию реализации и может обеспечить многие из тех же преимуществ без ряда ограничений, присущих наследованию. Основная мотивация для использования делегирования вместо наследования реализации заключается в том, чтобы избежать тесной связи и зависимостей, которые могут возникнуть при наследовании. Делегируя обязанности другому объекту, делегирующий объект не зависит от деталей реализации делегируемого объекта и может легче изменить или расширить его поведение. Делегирование также может быть более гибким, чем наследование, поскольку делегирующий объект может выбирать, какие обязанности делегировать и какому объекту, и может в любой момент изменить объект-делегат.
В этом примере класс MyClass делегирует ответственность за ведение журнала объекту ILogger. Класс MyClass не зависит от деталей реализации регистратора и может использовать любой объект, реализующий интерфейс ILogger. Класс MyClass также может в любой момент сменить регистратор, просто передав конструктору другой объект регистратора. 13. Принцип единственной ответственности (SRP). Формулировка и мотивация. Связность и зацепленность (cohesion and coupling). Пример нарушения и устранение. Принцип Каждый объект должен иметь одну обязанность и эта обязанность должна быть полностью инкапсулирована в класс. Все его сервисы должны быть направлены исключительно на обеспечение этой обязанности. Или «умными словами»: Модуль должен иметь одну и только одну причину для изменения. Более правильным выглядит понятие группы(вместо причины изменения), состоящей из одного или нескольких лиц, желающих данного изменения. Мы будем называть такие группы акторами. Модуль должен отвечать за одного и только за одного актора. Модуль — это просто связный набор функций и структур данных. Слово «связный» подразумевает принцип единственной ответственности. Связность — это сила, которая связывает код, ответственный за единственного актора. Связность, или прочность (cohesion) - мера силы взаимосвязанности элементов внутри модуля; способ и степень, в которой задачи, выполняемые некоторым программным модулем, связаны друг с другом. Объект (подсистема) обладает высокой связностью (High cohesion), если его обязанности хорошо согласованы между собой, и он не выполняет огромных объемов работы. Класс с низкой связностью (Low cohesion) выполняет много разнородных функций или несвязанных между собой обязанностей. Такие классы создавать нежелательно, поскольку они приводят к возникновению следующих проблем: Трудность понимания Сложность при повторном использовании Сложность поддержки Ненадежность, постоянная подверженность изменениям Классы с низкой степенью связности, как правило, являются слишком «абстрактными» или выполняют обязанности, которые можно легко распределить между другими объектами. Связанность, сопряжение (coupling) - способ и степень взаимозависимости между программными модулями; сила взаимосвязей между модулями; мера того, насколько взаимозависимы разные подпрограммы или модули. Сильная связанность (High coupling) рассматривается как серьёзный недостаток, поскольку затрудняет понимание логики модулей, их модификацию, автономное тестирование, а также пере использование по отдельности. Слабая связанность (Low coupling), напротив, является признаком хорошо структурированной и хорошо спроектированной системы, и, когда она комбинируется с сильной связностью(high cohesion), соответствует общим показателям хорошей читаемости и сопровождаемости. Пример нарушения Из книжки(могут быть сложности с пониманием, после есть пример попроще) Класс Employee из приложения платежной ведомости. Он имеет три метода: calculatePay(), reportHours() и save() Этот класс нарушает принцип единственной ответственности, потому что три его метода отвечают за три разных актора. Реализация метода calculatePay() определяется бухгалтерией. Реализация метода reportHours() определяется и используется отделом по работе с персоналом. Реализация метода save() определяется администраторами баз данных. Поместив исходный код этих трех методов в общий класс Employee, разработчики объединили перечисленных акторов. В результате такого объединения действия сотрудников бухгалтерии могут затронуть что-то, что требуется сотрудникам отдела по работе с персоналом. Например, представьте, что функции calculatePay() и reportHours() используют общий алгоритм расчета не сверхурочных часов. Представьте также, что разработчики, старающиеся не дублировать код, поместили реализацию этого алгоритма в функцию с именем regularHours() Теперь вообразите, что сотрудники бухгалтерии решили немного изменить алгоритм расчета не сверхурочных часов. Сотрудники отдела по работе с персоналом были бы против такого изменения, потому что вычисленное время они используют для других целей. Разработчик, которому было поручено внести изменение, заметил, что функция regularHours() вызывается методом calculatePay(), но, к сожалению, не заметил, что она также вызывается методом reportHours(). Разработчик внес требуемые изменения и тщательно протестировал результат. Сотрудники бухгалтерии проверили и подтвердили, что обновленная функция действует в соответствии с их пожеланиями, после чего измененная версия системы была развернута. Сотрудники отдела по работе с персоналом не знали о произошедшем и продолжали использовать отчеты, генерируемые функцией reportHours(), но теперь содержащие неправильные цифры. В какой-то момент проблема вскрылась, и сотрудники отдела по работе с персоналом разом побледнели от ужаса, потому что ошибочные данные обошлись их бюджету в несколько миллионов долларов. Эти проблемы возникают из-за того, что мы вводим в работу код, от которого зависят разные акторы. Принцип единственной ответственности требует разделять код, от которого зависят разные акторы. Пример попроще Написан класс Продукта: 1. public class Product 2. { 3. public int Price { get; set; } 4. 5. public bool IsValid() 6. { 7. return Price > 0; 8. } 9. } Такой подход является вполне оправданным в данном случае. Код простой, тестированию поддается, дублирования логики нет. Tеперь наш объект Product начал использовать в некоем CustomerService, который считает валидным продукт с ценой больше 100 тыс. рублей. 1. public class Product 2. { 3. public int Price { get; set; } 4. 5. public bool IsValid(bool isCustomerService) 6. { 7. if (isCustomerService == true) 8. return Price > 100000; 9. 10. return Price > 0; 11. } 12. } Затем логика валидации стала усложняться и меняться, появилась необходимость вынести ответственность за валидацию данных другому объекту. Причем надо сделать так, чтобы класс не зависел от конкретной реализации логики, что логично, ведь у разных продуктов может быть разная логика валидации данных(не будем углубляться). Решение: 1. public interface IProductValidator 2. { 3. bool IsValid(Product product); 4. } 5. 6. public class ProductDefaultValidator : IProductValidator 7. { 8. public bool IsValid(Product product) 9. { 10. return product.Price > 0; 11. } 12. } 13. 14. public class CustomerServiceProductValidator : IProductValidator 15. { 16. public bool IsValid(Product product) 17. { 18. return product.Price > 100000; 19. } 20. } 21. 22. public class Product 23. { 24. private readonly IProductValidator validator; 25. 26. public Product() : this(new ProductDefaultValidator()) 27. { 28. } 29. 30. public Product(IProductValidator validator) 31. { 32. this.validator = validator; 33. } 34. 35. public int Price { get; set; } 36. 37. public bool IsValid() 38. { 39. return validator.IsValid(this); 40. } 41. } Также примерами нарушения ответствнности могут быть классы-боги (God-object) и прочее, эти принципы важны для Попова, так что рекомендую хорошенько ознакомиться с ними. |