Методические рекомендации по выполнению практических работ по междисциплинарному курсу
Скачать 2.6 Mb.
|
Синглтон и многопоточность При применении паттерна синглтон в многопоточным программах мы можем столкнуться с проблемой, которую можно описать следующим образом: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static void Main(string[] args) { (new Thread(() => { Computer comp2 = new Computer(); comp2.OS = OS.getInstance("Windows 10"); Console.WriteLine(comp2.OS.Name); })).Start(); Computer comp = new Computer(); comp.Launch("Windows 8.1"); Console.WriteLine(comp.OS.Name); Console.ReadLine(); } Здесь запускается дополнительный поток, который получает доступ к синглтону. Параллельно выполняется тот код, который идет запуска потока и кторый также обращается к синглтону. Таким образом, и главный, и дополнительный поток пытаются инициализровать синглтон нужным значением - "Windows 10", либо "Windows 8.1". Какое значение сиглтон получит в итоге, пресказать в данном случае невозможно. Вывод программы может быть такой: Windows 8.1 Windows 10 Или такой: Windows 8.1 Windows 8.1 В итоге мы сталкиваемся с проблемой инициализации синглтона, когда оба потока одновременно обращаются к коду: 1 2 if (instance == null) instance = new OS(name); Чтобы решить эту проблему, перепишем класс синглтона следующим образом: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class OS { private static OS instance; public string Name { get; private set; } private static object syncRoot = new Object(); protected OS(string name) { this.Name = name; } public static OS getInstance(string name) { if (instance == null) 16 17 18 19 20 21 22 23 24 25 { lock (syncRoot) { if (instance == null) instance = new OS(name); } } return instance; } } Чтобы избежать одновременного доступа к коду из разных потоков критическая секция заключается в блок lock. Другие реализации синглтона Выше были рассмотрены общие стандартные реализации: потоконебезопасная и потокобезопасная реализации паттерна. Но есть еще ряд дополнительных реализаций, которые можно рассмотреть. Потокобезопасная реализация без использования lock 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Singleton { private static readonly Singleton instance = new Singleton(); public string Date { get; private set; } private Singleton() { Date = System.DateTime.Now.TimeOfDay.ToString(); } public static Singleton GetInstance() { return instance; } } Данная реализация также потокобезопасная, то есть мы можем использовать ее в потоках так: 1 2 3 4 5 6 7 8 (new Thread(() => { Singleton singleton1 = Singleton.GetInstance(); Console.WriteLine(singleton1.Date); })).Start(); Singleton singleton2 = Singleton.GetInstance(); Console.WriteLine(singleton2.Date); Lazy-реализация Определение объекта синглтона в виде статического поля класса открывает нам дорогу к созданию Lazy-реализации паттерна Синглтон, то есть такой реализации, где данные будут инициализироваться только перед непосредственным использованием. Поскольку статические поля инициализируются перед первым доступом к статическому членам класса и перед вызовом статического конструктора (при его наличии). Однако здесь мы можем столкнуться с двумя трудностями. Во-первых, класс синглтона может иметь множество статических переменных. Возможно, мы вообще не будем обращаться к объекту синглтона, а будем использовать какие-то другие статические переменные: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class Singleton { private static readonly Singleton instance = new Singleton(); public static string text = "hello"; public string Date { get; private set; } private Singleton() { Console.WriteLine($"Singleton ctor {DateTime.Now.TimeOfDay}"); Date = System.DateTime.Now.TimeOfDay.ToString(); } public static Singleton GetInstance() { Console.WriteLine($"GetInstance {DateTime.Now.TimeOfDay}"); Thread.Sleep(500); return instance; } } class Program { static void Main(string[] args) { Console.WriteLine($"Main {DateTime.Now.TimeOfDay}"); Console.WriteLine(Singleton.text); Console.Read(); } } В данном случае идет только обращение к переменной text, однако статическое поле instance также будет инициализировано. Например, консольный вывод в данном случае мог бы выглядеть следующим образом: Singleton ctor 16:05:54.1469982 Main 16:05:54.2920316 hello В данном случае мы видим, что статическое поле instance инициализировано. Для решения этой проблемы выделим отдельный внутренний класс в рамках класса синглтона: 1 2 3 4 5 6 7 8 9 10 11 12 public class Singleton { public string Date { get; private set; } public static string text = "hello"; private Singleton() { Console.WriteLine($"Singleton ctor {DateTime.Now.TimeOfDay}"); Date = DateTime.Now.TimeOfDay.ToString(); } public static Singleton GetInstance() { 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Console.WriteLine($"GetInstance {DateTime.Now.TimeOfDay}"); return Nested.instance; } private class Nested { internal static readonly Singleton instance = new Singleton(); } } class Program { static void Main(string[] args) { Console.WriteLine($"Main {DateTime.Now.TimeOfDay}"); Console.WriteLine(Singleton.text); Console.Read(); } } Теперь статическая переменная, которая представляет объект синглтона, определена во вложенном классе Nested. Чтобы к этой переменной можно было обращаться из класса синглтона, она имеет модификатор internal, в то же время сам класс Nested имеет модификатор private, что позволяет гарантировать, что данный класс будет доступен только из класса Singleton. Консольный вывод в данном случае мог бы выглядеть следующим образом: Main 16:11:40.1320873 hello Далее мы сталкиваемся со второй проблемой: статические поля инициализируются перед первым доступом к статическому членам класса и перед вызовом статического конструктора (при его наличии). Но когда именно? Если класс содержит статические поля, не содержит статического конструктора, то время инициализации статических полей зависит от реализации платформы. Нередко это непосредственно перед первым использованием, но тем не менее момент точно не определен - это может быть происходить и чуть раньше. Однако если класс содержит статический конструктор, то статические поля будут инициализироваться непосредственно либо при создании первого экземпляра класса, либо при первом обращении к статическим членам класса. Например, рассмотрим выполнение следующей программы: 1 2 3 4 5 6 7 8 9 static void Main(string[] args) { Console.WriteLine($"Main {DateTime.Now.TimeOfDay}"); Console.WriteLine(Singleton.text); Singleton singleton1 = Singleton.GetInstance(); Console.WriteLine(singleton1.Date); Console.Read(); } Ее возможный консольный вывод: Main 16:33:33.1404818 hello Singleton ctor 16:33:33.1564802 GetInstance 16:33:33.1574824 16:33:33.1564802 Мы видим, что код метода GetInstance, который идет до вызова конструктора класса Singleton, выполняется после выполнения этого конструктора. Поэтому добавим в выше определенный класс Nested статический конструктор: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Singleton { public string Date { get; private set; } public static string text = "hello"; private Singleton() { Console.WriteLine($"Singleton ctor {DateTime.Now.TimeOfDay}"); Date = DateTime.Now.TimeOfDay.ToString(); } public static Singleton GetInstance() { Console.WriteLine($"GetInstance {DateTime.Now.TimeOfDay}"); Thread.Sleep(500); return Nested.instance; } private class Nested { static Nested() { } internal static readonly Singleton instance = new Singleton(); } } Теперь при выполнении той же программы мы получим полноценную Lazy-реализацию: Main 16:37:18.4108064 hello GetInstance 16:37:18.4208062 Singleton ctor 16:37:18.4218065 16:37:18.4228061 Реализация через класс Lazy Еще один способ создания синглтона представляет использование класса Lazy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Singleton { private static readonly Lazy { Name = System.Guid.NewGuid().ToString(); } public static Singleton GetInstance() { return lazy.Value; } } Практическая работа №17 Использование структурных шаблонов Декоратор (Decorator) Декоратор (Decorator) представляет структурный шаблон проектирования, который позволяет динамически подключать к объекту дополнительную функциональность. Для определения нового функционала в классах нередко используется наследование. Декораторы же предоставляет наследованию более гибкую альтернативу, поскольку позволяют динамически в процессе выполнения определять новые возможности у объектов. Когда следует использовать декораторы? Когда надо динамически добавлять к объекту новые функциональные возможности. При этом данные возможности могут быть сняты с объекта Когда применение наследования неприемлемо. Например, если нам надо определить множество различных функциональностей и для каждой функциональности наследовать отдельный класс, то структура классов может очень сильно разрастись. Еще больше она может разрастись, если нам необходимо создать классы, реализующие все возможные сочетания добавляемых функциональностей. Схематически шаблон "Декоратор" можно выразить следующим образом: Формальная организация паттерна в C# могла бы выглядеть следующим образом: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 abstract class Component { public abstract void Operation(); } class ConcreteComponent : Component { public override void Operation() {} } abstract class Decorator : Component { protected Component component; public void SetComponent(Component component) { this.component = component; } public override void Operation() { if (component != null) component.Operation(); } } class ConcreteDecoratorA : Decorator { public override void Operation() { base.Operation(); } } class ConcreteDecoratorB : Decorator 33 34 35 36 37 38 { public override void Operation() { base.Operation(); } } Участники Component: абстрактный класс, который определяет интерфейс для наследуемых объектов ConcreteComponent: конкретная реализация компонента, в которую с помощью декоратора добавляется новая функциональность Decorator: собственно декоратор, реализуется в виде абстрактного класса и имеет тот же базовый класс, что и декорируемые объекты. Поэтому базовый класс Component должен быть по возможности легким и определять только базовый интерфейс. Класс декоратора также хранит ссылку на декорируемый объект в виде объекта базового класса Component и реализует связь с базовым классом как через наследование, так и через отношение агрегации. Классы ConcreteDecoratorA и ConcreteDecoratorB представляют дополнительные функциональности, которыми должен быть расширен объект ConcreteComponent. Рассмотрим пример. Допустим, у нас есть пиццерия, которая готовит различные типы пицц с различными добавками. Есть итальянская, болгарская пиццы. К ним могут добавляться помидоры, сыр и т.д. И в зависимости от типа пицц и комбинаций добавок пицца может иметь разную стоимость. Теперь посмотрим, как это изобразить в программе на C#: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Program { static void Main(string[] args) { Pizza pizza1 = new ItalianPizza(); pizza1 = new TomatoPizza(pizza1); // итальянская пицца с томатами Console.WriteLine("Название: {0}", pizza1.Name); Console.WriteLine("Цена: {0}", pizza1.GetCost()); Pizza pizza2 = new ItalianPizza(); pizza2 = new CheesePizza(pizza2);// итальянская пиццы с сыром Console.WriteLine("Название: {0}", pizza2.Name); Console.WriteLine("Цена: {0}", pizza2.GetCost()); Pizza pizza3 = new BulgerianPizza(); pizza3 = new TomatoPizza(pizza3); pizza3 = new CheesePizza(pizza3);// болгарская пиццы с томатами и сыром Console.WriteLine("Название: {0}", pizza3.Name); Console.WriteLine("Цена: {0}", pizza3.GetCost()); Console.ReadLine(); } } abstract class Pizza { public Pizza(string n) { 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 this.Name = n; } public string Name {get; protected set;} public abstract int GetCost(); } class ItalianPizza : Pizza { public ItalianPizza() : base("Итальянская пицца") { } public override int GetCost() { return 10; } } class BulgerianPizza : Pizza { public BulgerianPizza() : base("Болгарская пицца") { } public override int GetCost() { return 8; } } abstract class PizzaDecorator : Pizza { protected Pizza pizza; public PizzaDecorator(string n, Pizza pizza) : base(n) { this.pizza = pizza; } } class TomatoPizza : PizzaDecorator { public TomatoPizza(Pizza p) : base(p.Name + ", с томатами", p) { } public override int GetCost() { return pizza.GetCost() + 3; } } class CheesePizza : PizzaDecorator { public CheesePizza(Pizza p) : base(p.Name + ", с сыром", p) { } 81 82 83 84 85 86 public override int GetCost() { return pizza.GetCost() + 5; } } В качестве компонента здесь выступает абстрактный класс Pizza, который определяет базовую функциональность в виде свойства Name и метода GetCost(). Эта функциональность реализуется двумя подклассами ItalianPizza и BulgerianPizza, в которых жестко закодированы название пиццы и ее цена. Декоратором является абстрактный класс PizzaDecorator, который унаследован от класса Pizza и содержит ссылку на декорируемый объект Pizza. В отличие от формальной схемы здесь установка декорируемого объекта происходит не в методе SetComponent, а в конструкторе. Отдельные функциональности - добавление томатов и сыры к пиццам реализованы через классы TomatoPizza и CheesePizza, которые обертывают объект Pizza и добавляют к его имени название добавки, а к цене - стоимость добавки, то есть переопределяя метод GetCost и изменяя значение свойства Name. Благодаря этому при создании пиццы с добавками произойдет ее обертывание декоратором: 1 2 3 Pizza pizza3 = new BulgerianPizza(); pizza3 = new TomatoPizza(pizza3); pizza3 = new CheesePizza(pizza3); Сначала объект BulgerianPizza обертывается декоратором TomatoPizza, а затем CheesePizza. И таких обертываний мы можем сделать множество. Просто достаточно унаследовать от декоратора класс, который будет определять дополнительный функционал. А если бы мы использовали наследование, то в данном случае только для двух видов пицц с двумя добавками нам бы пришлось создать восемь различных классов, которые бы описывали все возможные комбинации. Поэтому декораторы являются более предпочтительным в данном случае методом. Адаптер (Adapter) Паттерн Адаптер (Adapter) предназначен для преобразования интерфейса одного класса в интерфейс другого. Благодаря реализации данного паттерна мы можем использовать вместе классы с несовместимыми интерфейсами. Когда надо использовать Адаптер? Когда необходимо использовать имеющийся класс, но его интерфейс не соответствует потребностям Когда надо использовать уже существующий класс совместно с другими классами, интерфейсы которых не совместимы Формальное определение паттерна на UML выглядит следующим образом: Формальное описание адаптера объектов на C# выглядит таким образом: 1 2 3 4 5 6 7 8 9 class Client { public void Request(Target target) { target.Request(); } } // класс, к которому надо адаптировать другой класс class Target 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 { public virtual void Request() {} } // Адаптер class Adapter : Target { private Adaptee adaptee = new Adaptee(); public override void Request() { adaptee.SpecificRequest(); } } // Адаптируемый класс class Adaptee { public void SpecificRequest() {} } Участники Target: представляет объекты, которые используются клиентом Client: использует объекты Target для реализации своих задач Adaptee: представляет адаптируемый класс, который мы хотели бы использовать у клиента вместо объектов Target Adapter: собственно адаптер, который позволяет работать с объектами Adaptee как с объектами Target. То есть клиент ничего не знает об Adaptee, он знает и использует только объекты Target. И благодаря адаптеру мы можем на клиенте использовать объекты Adaptee как Target Теперь разберем реальный пример. Допустим, у нас есть путешественник, который путешествует на машине. Но в какой-то момент ему приходится передвигаться по пескам пустыни, где он не может ехать на машине. Зато он может использовать для передвижения верблюда. Однако в классе путешественника использование класса верблюда не предусмотрено, поэтому нам надо использовать адаптер: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Program { static void Main(string[] args) { // путешественник Driver driver = new Driver(); // машина Auto auto = new Auto(); // отправляемся в путешествие driver.Travel(auto); // встретились пески, надо использовать верблюда Camel camel = new Camel(); // используем адаптер ITransport camelTransport = new CamelToTransportAdapter(camel); // продолжаем путь по пескам пустыни driver.Travel(camelTransport); 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 Console.Read(); } } interface ITransport { void Drive(); } // класс машины class Auto : ITransport { public void Drive() { Console.WriteLine("Машина едет по дороге"); } } class Driver { public void Travel(ITransport transport) { transport.Drive(); } } // интерфейс животного interface IAnimal { void Move(); } // класс верблюда class Camel : IAnimal { public void Move() { Console.WriteLine("Верблюд идет по пескам пустыни"); } } // Адаптер от Camel к ITransport class CamelToTransportAdapter : ITransport { Camel camel; public CamelToTransportAdapter(Camel c) { camel = c; } public void Drive() { camel.Move(); } } И консоль выведет: Машина едет по дороге Верблюд идет по пескам пустыни В данном случае в качестве клиента применяется класс Driver, который использует объект ITransport. Адаптируемым является класс верблюда Camel, который нужно использовать в качестве объекта ITransport. И адптером служит класс CamelToTransportAdapter. Возможно, кому-то покажется надуманной проблема использования адаптеров особенно в данном случае, так как мы могли бы применить интерфейс ITransport к классу Camel и реализовать его метод Drive(). Однако, в данном случае может случиться дублирование функциональностей: интерфейс IAnimal имеет метод Move(), реализация которого в классе верблюда могла бы быть похожей на реализацию метода Drive() из интерфейса ITransport. Кроме того, нередко бывает, что классы спроектированы кем-то другим, и мы никак не можем на них повлиять. Мы только используем их. В результате чего адаптеры довольно широко распространены в .NET. В частности, многочисленные встроенные классы, которые используются для подключения к различным системам баз данных, как раз и реализуют паттерн адаптер (например, класс System.Data.SqlClient.SqlDataAdapter). |