Yuri Trukhin - Software developement best practices
1. Лучшие практики разработки ПО Принципы гибкого проектирования Юрий Трухин senior developer, CNIPGIS LLC Microsoft Student Partner Guru
2. беларуская версія Лепшыя практыкі распрацоўкі праграмнага забеспячэння прынцыпы гнуткага праектавання Юрий Трухин senior developer, CNIPGIS LLC Microsoft Student Partner Guru
4. Признаки плохого дизайна ПО Жесткость: дизайн трудно поддается изменению Хрупкость: дизайн легко разрушается Косность: дизайн трудно использовать повторно Вязкость: трудно добиться желаемого Ненужная сложность: избыточное проектирование Ненужные повторения: чрезмерное использование копирования и вставки Непрозрачность: плохо выраженная цель Часто эти признаки появляются в результате нарушения одного из принципов проектирования.
6. public partial class Copier { public static void Copy() { int c; while ((c = Keyboard.Read()) != -1) { Printer.Write(c); } } }
7. public class CopierWithPerfocards { /// <summary> /// не забудьте сбросить этот флаг /// </summary> private static boolptFlag = false; public static void Copy() { int c; while ((c = (ptFlag ? PaperTape.Read() : Keyboard.Read())) != -1) Printer.Write(c); } }
8. public class CopierWithPerfocardsReadAndWrite { //не забудьте сбросить этот флаг public static boolptFlag = false; public static boolpunchFlag = false; public static void Copy() { int c; while ((c = (ptFlag ? PaperTape.Read() : Keyboard.Read())) != -1) punchFlag ? PaperTape.punchFlag(c) : Printer.Write(c); } }
9. public interface Reader { int Read(); } public class KeyboardReader : Reader { public int Read() { return Keyboard.Read(); }; } public partial class Copier { public static Reader reader = new KeyboardReader(); public static void Copy() { var c; while ((c = (reader.Read())) != -1 ) { Printer.Write(c); } } }
11. Принципы гибкого проектирования Принцип единственной обязанности Принцип открытости/закрытости Принцип подстановки Лисков Принцип инверсии зависимости Принцип разделения интерфейсов Принцип эквивалентности повторного использования и выпуска Принцип общей закрытости Принцип совместного повторного использования Принцип ацикличности зависимостей Принцип устойчивых зависимостей Принцип устойчивых абстракций
12. Принцип единственной обязанности (SRP) «Никто кроме Будды не должен брать на себя ответственность за сокровенные знания» Э.Кобхэм Брюэр, 1897
13. Принцип единственной обязанности (SRP) У класса должна быть только одна причина для изменения. Любое изменение требований проявляется в изменении распределения обязанностей между классами. Если класс берет на себя несколько обязанностей – у него появляется несколько причин для изменения. Если класс отвечает за несколько действий, его обязанности оказываются связанными. Это является причиной хрупкого дизайна приложений.
14. Принцип единственной обязанности (SRP) Более одной обязанности Rectangle Приложение «Вычислительная геометрия» +draw() +area() : double Графическое приложение GUI
16. Принцип единственной обязанности (SRP) Проблемы: В приложение «Вычислительная геометрия» попадает не нужная логика вместе с классом Rectangle. Если изменение графического приложения потребует изменить класс Rectangle, придется заново собирать, тестировать и развертывать приложение «Вычислительная геометрия». … или приложение «Вычислительная геометрия» неожиданно может перестать работать
17. Принцип единственной обязанности (SRP) Обязанности разделены Rectangle GeometricRectangle Приложение «Вычислительная геометрия» Графическое приложение +draw() +area() : double GUI
18. Принцип единственной обязанности (SRP) Обязанность – причина изменения Если вы можете найти несколько причин для изменения – у класса несколько обязанностей.
19. Принцип единственной обязанности (SRP) Пример: Public interface Modem { public void Dial(string pno); public void Hangup(); public void Send (char c); public char Recv(); } Сколько обязанностей у интерфейса Modem?
20. Принцип единственной обязанности (SRP) Сколько обязанностей?: Public interface Modem { public void Dial(string pno); //управление соединением public void Hangup();//управление соединением public void Send (char c);//передача данных public char Recv(); //передача данных } Нужно ли разделить интерфейс?
21. Принцип единственной обязанности (SRP) Нужно ли разделять интерфейс?: ДА! Если может потребоваться изменение сигнатуры методов управления соединением, класс, вызывающий send и receive придется повторно компилировать и развертывать чаще, чем хотелось бы. Это приводит к жесткости дизайна. Следует разделить интерфейс на DataChannelи Connection
22. Принцип единственной обязанности (SRP) Нужно ли разделять интерфейс?: НЕТ! Если приложение не модифицируют таким образом, что обязанности изменяются порознь, то разделять нет необходимости. Это приводит к излишней сложности. Изменения интерфейса не требуются.
23. Принцип единственной обязанности (SRP) Нужно ли разделять интерфейс?: НЕТ! Если приложение не модифицируют таким образом, что обязанности изменяются порознь, то разделять нет необходимости. Это приводит к излишней сложности. Изменения интерфейса не требуются. Вывод: ось изменения становится таковой, если изменение имеет место. Если на то нет причин, неразумно применять ЛЮБОЙ принцип проектирования.
24. Принцип единственной обязанности (SRP) Разделение связанных обязанностей. Обеспечение сохранности. Employee +CalculatePay +Store Persistense System Связь бизнес-правил и подсистемы сохраниния -> неприятности! Обычно тесты заставляют их разделять, если нет – в случае если появляется жесткость и хрупкость – нужен рефакторинг! (Для данного случая применимы паттерны Фасад, Объект доступа к данным, Прокси для разделения обязанностей).
25. Принцип открытости-закрытости «Голланская дверь: существительное. Дверь, разделенная на две части по горизонтали так, что каждая створка может открываться и закрываться независимо» The American Heritage Dictionary of English language, 2000
26. Принцип открытости-закрытости Программные сущности (классы, модули, функции) должны быть открыты для расширения и закрыты для модификации. Если единственное изменение в каком-то месте программы приводит к каскаду изменений в зависимых модулях – это признак жесткого дизайна.
27. Принцип открытости-закрытости Основные храктеристики модулей, разработанных в соответствии с принципом открытости-закрытости: Они открыты для расширения (поведение модуля можно расширить, можно менять состав функций модуля). Они закрыты для модификации (расширение поведение модуля не сопряжено с изменением в исходном или двоичном коде модуля). Как это сделать?
28. Принцип открытости-закрытости Как это сделать? С ПОМОЩЬЮ АБСТРАКЦИИ! Абстракция – абстрактный базовый класс, поведение – производный класс. Модуль, манипулирующий абстракцией можно сделать закрытым для модификации – он зависит от ФИКСИРОВАННОЙ абстракции. Модуль при этом остается расширяемым.
30. Принцип открытости-закрытости Применив паттерн «Стратегия» получим: “interface” Client Interface Client Server Класс Client одновременно является открытым и закрытым.
31. Принцип открытости-закрытости Альтернатива: Паттерн «Шаблонный метод» Открытые методы в policy реализуют некую политику, они аналогичны методам класса Client (описывают определенные функции в терминах абстрактных интерфейсов, часть класса policy, в C# это абстрактные методы, реализуются в подтипах policy). Поведения, описанные внутри policy, можно расширять и модифицировать. Policy Implementation +PolicyFunction() #ServiceFunction() #ServiceFunctiom
33. Принцип открытости-закрытости --shape.h-------------------------------------------------------------------- enumShareType (circle, square); struct Shape { ShapeTypeitsType; } --circle.h------------------------------------------------------------------- struct Circle { ShapeTypeitsType; double itsRadius; Point itsCenter; }; void DrawCircle(struct Circle*); --square.h------------------------------------------------------------------- struct Square { ShapeTypeitsType; double itsSide; Point itsTopLeft; }; void DrawSquare(struct Square*);
34. Принцип открытости-закрытости --drawAllShapes.cc----------------------------------------------------------- typedefstruct Shape *ShapePointer; //функция не может быть закрытой для добавления других фигур, //не удовлетворяет принципам OCP void DrawAllShapes(ShapePointer list[], int n) { int i; for (i=0; i<n; i++) { struct Shape* s = list[i]; switch (s->itsType) { case square: DrawSquare((struct Square*)s); break; case circle: DrawCircle((struct Circle*)s); break; } } } ///Резюме: ///это решение жесткое, ///тк после добавление фигуры Triange(треугольник) ///необходимо заново откомпилировать и развернуть файлы , содержащие опредлеление ///Shape, Square,Circle и DrawAllShapes. /// ///это решение хрупкое, ///т.к. есть много других switch/case и if/else, которые сложно отыскать и понять. /// ///это решение костное, потому что всякий, кто попытается использовать ///функцию DrawAllShapes в другой программе, вынужден будет тащить за собой ///определения Square и Circle, даже если в этой программе они не используются.
35. Принцип открытости-закрытости public interface Shape { void Draw(); } public class Square : Shape { public void Draw() { //нарисовать квадрат } } public class Circle : Shape { public void Draw() { //нарисовать круг } } public void DrawAllShape(IList shapes) { foreach (Shape shape in shapes) shape.Draw(); }
38. Принцип открытости-закрытости Вывод 1 Модель Shape не является естественной в системе, где упорядоченность связана с типом фигуры. Каким бы закрытым не был модуль, всегда найдется такое изменение, для которых он не закрыт. Не существует моделей, естественных во всех контекстах! Проектировщик должен решить, от каких изменений закрыть дизайн. OCP применим только с вероятными изменениями.
39. Принцип открытости-закрытости Расстановка «точек подключения» «Обманул меня раз». Первоначально код пишется без учета возможных изменений. Если изменение происходят – реализуется абстракция, в будущем защищающие от подобного рода изменений. «Стимулирование изменений» Сначала пишем тесты. Разработку ведем очень короткими циклами, измеряемыми в днях, а не в неделях Разрабатываем содержательные функции до инфраструктуры и часто демонстрируем их заинтересованным сторонам Первую версию ПО выпускаем быстро, а последующие часто.
40. Принцип открытости-закрытости Заключение Следование этому принципу позволит получить от ООП максимум обещанного: гибкость, возможность повторного использования и удобство сопровождения. Отказ от преждевременного абстрагирования столь же важен, как и само абстрагирование.
42. Принцип инверсии зависимостей Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те и другие должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
44. Принцип эквивалентности повторного использования и выпуска Единица повторного использования равна единице выпуска. (либо все классы, включенные в компонент можно повторно использовать, либо ни один!)
45. Принцип общей закрытости Все классы внутри компонента должны быть закрыты относительно изменений одного и того же вида. Изменение, затрагивающее компонент, должно затрагивать все классы в этом компоненте и только в нем.
46. Принцип совместного повторного использования Все классы внутри компонента используются совместно. Если вы можете повторно использовать один класс, то можете использовать и остальные.
48. Принцип устойчивых зависимостей Зависимости должны быть направлены в сторону устойчивости (изменяемые компоненты должны быть сверху графа, и из них собирается неизменяемый)
49. Принцип устойчивых абстракций Компонент должен быть столь же абстрактным, сколь и устойчивым (иначе устойчивость будет препятствовать его расширению).
В приложение «Вычислительная геометрия» попадает не нужная логика вместе с классом Rectangle.Если изменение графического приложения потребует изменить класс Rectangle, придется заново собирать, тестировать и развертывать приложение «Вычислительная геометрия».… или приложение «Вычислительная геометрия» неожиданно может перестать работать
ДА!Если может потребоваться изменение сигнатуры методов управления соединением, класс, вызывающий send и receive придется повторно компилировать и развертывать чаще, чем хотелось бы.Это приводит к жесткости дизайна. Следует разделить интерфейс на DataChannelи ConnectionНЕТ!Если приложение не модифицируют таким образом, что обязанности изменяются порознь, то разделять нет необходимости.Это приводит к излишней сложности. Изменения интерфейса не требуются.
///Резюме:///это решение жесткое,///тк после добавление фигуры Triange(треугольник)///необходимо заново откомпилировать и развернуть файлы , содержащие опредлеление///Shape, Square,Circle и DrawAllShapes.//////это решение хрупкое,///т.к. есть много других switch/case и if/else, которые сложно отыскать и понять.//////это решение костное, потому что всякий, кто попытается использовать ///функцию DrawAllShapes в другой программе, вынужден будет тащить за собой ///определения Square и Circle, даже если в этой программе они не используются.
///Резюме:///это решение жесткое,///тк после добавление фигуры Triange(треугольник)///необходимо заново откомпилировать и развернуть файлы , содержащие опредлеление///Shape, Square,Circle и DrawAllShapes.//////это решение хрупкое,///т.к. есть много других switch/case и if/else, которые сложно отыскать и понять.//////это решение костное, потому что всякий, кто попытается использовать ///функцию DrawAllShapes в другой программе, вынужден будет тащить за собой ///определения Square и Circle, даже если в этой программе они не используются.
///Резюме:///это решение жесткое,///тк после добавление фигуры Triange(треугольник)///необходимо заново откомпилировать и развернуть файлы , содержащие опредлеление///Shape, Square,Circle и DrawAllShapes.//////это решение хрупкое,///т.к. есть много других switch/case и if/else, которые сложно отыскать и понять.//////это решение костное, потому что всякий, кто попытается использовать ///функцию DrawAllShapes в другой программе, вынужден будет тащить за собой ///определения Square и Circle, даже если в этой программе они не используются.