virtual IM& clone(const IM&); virtual ~IM() = 0; // ..... Контекст (C++ style): matrix.hpp template <typename T> class M : public IM { T *contents; size_t x, y; public: M(M& rhs); M& clone(const IM&) override; // все реализации в том же файле 4
class M : public IM { T *contents; size_t x, y; public: M(M& rhs); M& clone(const IM&) override; // .... Инварианты • Указатель contents валиден если 𝑥 ≠ 0 • Если 𝑥 ≠ 0то всегда 𝑦 ≠ 0 • Для contents аллоцирована память размером 𝑥 ∗ 𝑦 ∗ 𝑠𝑖𝑧𝑒𝑜𝑓(𝑇) • После клонирования матрица равна исходной • Ещё? 5
Контекст реализует интерфейс (для типов в C++ через наследование интерфейса). • Производный контекст расширяет базовый (для типов в C++ через наследование реализации). • Если контексты это типы, производный контекст связан с базовым дополнительными отношениями (частное/общее, быть частью и подобн • Если несколько типов реализуют общий интерфейс, вызовы их методов этот интерфейс полиморфны. 6
• В каких отношениях могут быть друг с другом классы в C++? • Примеры отношений: "A наследует от B" или "C является полем в D" • Назовите все какие сможете вообразить 8
через своё имя, поля и методы • По традиции имя идёт в первом квадрате, поля во втором а методы в третьем • Формат полей "поле : тип" (несколько контринтуитивно для C++) • UML поддерживает также тонны других атрибутов, например шаблонные параметры 9 https://www.uml-diagrams.org • UML это специальный язык, который моделирует классы и отношения м классами (отношения будут далее)
связаны друг с другом • Например появляются вместе внутри одной функции) • Здесь также видно, что у каждой связи можно указать роли и множественность. • Генерализация: отношение частное/общее (для C++ это открытое наследование) 10
B является частью сущности A • Здесь файл принадлежит только одной папке и связан с ней временем жизни • Аггрегация: сущность A владеет сущностью B, но кроме A у B может быть много владельцев • Здесь треугольник состоит из отрезков, но каждый из отрезков может участвовать во многих треугольниках 11
систему, в т числе сколь угодно плохую. • Software имеет английский корень soft, означающий нечто, что легко изменять. • Но часто вместо куска пластилина у нас под руками оказывается засохшая субстанция с обломками гвоздей и лезвий внутри. • Первый шаг к хорошему коду это легко изменяемый код. 12
контекст должен иметь одну ответственность • OCP – open-close principle • каждый контекст должен быть закрыт для изменения и открыт для расшире • LSP – Liskov substitution principle • частный класс должен иметь возможность свободно заменять общий • ISP – interface segregation principle • Тип не должен зависеть от тех интерфейсов, которые он не использует • DIP – dependency inversion principle • Высокоуровневые классы не должны зависеть от низкоуровневых 14
тут должны будем изменять полигон? • Что в этом плохого? • Есть ли нечто плохое в зависимости от вектора и от кватернионов? • "A class should have only one reaso to change" (Robert C. Martin) srp-bad.cc
Для вывода есть итераторы • В итоге внешние функции могут обращаться к элементам но не к состоянию полигона • "We want to design components that are self-contained: independent and with single well-defined purpose" (Andrew Hunt, David Thomas) 16 srp-good.cc
и внешне разделены. • Разделяйте всё, что может быть разделено без создания жёстких внешн связей. Пример: отделение алгоритмов от контейнеров. • "Cohesion is a measure of the strength of association of the elements inside a module. A highly cohesive module is a collection of statements and data items that should be treated as a whole because they are so closely related." (Tom DeMarco) 17
противоречит SRP. • Мы добавили виртуальную функцию draw в полигон, но мы несколькими слайдами раньше договорились этого не делать. • "Inheritance is the base class of Evil" (Sean Parent) • Посмотрите на код справа. • Чего мы хотели бы? using document_t = std::vector<int>; // документ хранит объекты // семантика значения // no incidental data structures document.push_back(1); document.push_back(2); document.push_back(3); draw(document, std::cout); 20
противоречит SRP. • Мы добавили виртуальную функцию draw в полигон, но мы несколькими слайдами раньше договорились этого не делать. • "Inheritance is the base class of Evil" (Sean Parent) • Посмотрите на код справа. • Чего мы хотели бы? using document_t = std::vector<???>; // документ хранит объекты // семантика значения // no incidental data structures document.push_back(circle); document.push_back(polygon); document.push_back(vector); draw(document, std::cout); // мы хотели бы хранить и полиморфно // отображать разнородные объекты 21
SRP • Теперь мы расширяем добавляя свободные функции, полиморфные, как множество перегрузки. • Динамический полиморфизм при этом остаётся деталью реализации. • Шаблонный полиморфизм используется чтобы позволить обобщённое программирование 24
общими и по составу и по поведению. class Polygon3D : public Polygon2D; • Это читается как: трёхмерный полигон может быть использован во всех контекстах, где нам нужен двумерный полигон. Если это некорректно, наследовать нельзя. • Предусловия алгоритмов не могут быть усилены производным классом. • Постусловия алгоритмов не могут быть ослаблены производным классо • Важной концепцией для LSP является ковариантность. 26
если выполняется условие: если A обобщает B, то A' обобщает B' • Собственно указатели ковариантны к генерализации если трактовать A class Rectangle : public Shape { /* ... */ }; void draw(Shape* shapes, size_t size); Rectangle rects[5]; draw(rects, 5); // ok, Rectangle* is Shape* 27
если выполняется условие: если A обобщает B, то A' обобщает B' • При этом шаблоны вообще-то инвариантны к генерализации class Rectangle : public Shape { /* ... */ }; void draw(std::vector<Shape> shapes); std::vector<Rectangle> rects(5); draw(rects); // fail, vector<Rectangle> is not vector<Shape> 29
не инвариантны? template <typename T> using Pointer = T*; // казалось бы void draw(Pointer<Shape> shapes, size_t size); Pointer<Rectangle> rects = new Rectangle[5]; draw(rects, 5); // ok, но чем Pointer<Rectangle> // лучше чем std::vector<Rectangle>? • Подсказка: ковариантны только одинарные указатели • Таким образом, ковариантность указателей и ссылок к обобщению это приятное исключение для LSP, а не правило. 30
подверженность срезке делают их отличными кандидатами в C++ • Но их использование приводит к неявным (incidental) структурам данных и убивает value-семантику. 32
= 0; virtual void eat() = 0; // ..... }; class Robot : public IWorker { void work() override; void eat() override { // do nothing } }; class Manager { IWorker *subdue; public: void manage () { subdue->work(); } }; • Здесь менеджер зависит от интерфейса eat. В итоге его должны реализовать роботы 33
в среднем занимается не научной или технической деятельностью, а деятельностью социальной • Это на сто процентов верно для бухгалтерии, веб-программирования и т • Но даже для компиляторостроения, высоконагруженных систем и всего такого интересного соотношение ~ 80/20 в пользу гуманитарных задач • Программный код больше похож на чертёж здания, чем на доказательс теоремы. Поэтому говорят о "качестве", "архитектуре", "проекте" • Поговорим о качестве. Что такое хороший код? 40
не о том • скорость работы • время до поставки пользователю • количество найденных дефектов на строчку • искусственные критерии вроде цикломатической сложности и т.д. требования может легко выполнить чудовищная адская индусская лапша) • Субъективные критерии ("когда я лично назову код хорошим") • читаемость • расширяемость • разумный выбор алгоритмов и абстракций • Любой человек защищается. Главное свойство плохого кода: его написал не я 41
спорны, но они формировались годами и написаны кровью • Таковы принципы SOLID для ООП • Таковы ещё два важных принципа которые применимы вообще везде • Law of Demeter или Principle of least information • Контекст не должен давать пользователю заглядывать в более низкие уровн абстракции напрямую • Principle of least astonishment • То что программист видит в коде не должно его удивлять и запутывать 42
ногами лошади • Было бы странно, если бы всадник получил интерфейс к нервам, позволяющим двигать ногами лошади напрямую • Но именно это регулярно происходит в плохо спроектированных системах 45
перегрузки так // parses "010" as 8, "0x10" as 16, "10" as 10 int strtoint(string s); // respects user radix int strtoint(string s, int radix); • На какие проблемы может наткнуться программист невнимательно чит документацию? • Всегда ли программисты внимательно читают документацию? 46
функцию так // radix = 10 if not specified int strtoint(string s, int radix = 10); • Теперь при неправильном использовании будет разумная ошибка • Вторую можно оставить как // parses "010" as 8, "0x10" as 16, "10" as 10 int smart_strtoint(string s); 47