Статьи

Эволюционирование к постоянному слою

Одним из самых запутанных шаблонов дизайна является настойчивость. Необходимость сохранения приложением своего внутреннего состояния и данных настолько велика, что, вероятно, существуют десятки, если не сотни, различных технологий для решения этой единственной проблемы. К сожалению, ни одна технология не является волшебной пулей. Каждое приложение, а иногда и каждый компонент приложения, по-своему уникально, поэтому требует уникального решения.

В этом уроке я научу вас некоторым рекомендациям, которые помогут вам определить, какой подход использовать при работе с будущими приложениями. Я кратко расскажу о некоторых проблемах и принципах проектирования высокого уровня, а затем более детально рассмотрим шаблон проектирования Active Record в сочетании с несколькими словами о шаблоне проектирования Table Data Gateway.

Конечно, я не просто научу вас теории, лежащей в основе проекта, но я также покажу вам пример, который начинается как случайный код и трансформируется в структурированное постоянное решение.


Сегодня ни один программист не может понять эту архаичную систему.

Самый старый проект, над которым я должен работать, начался в 2000 году. В то время команда программистов начала новый проект, оценив различные требования, подумав о рабочих нагрузках, с которыми придется работать приложению, проверив различные технологии и пришла к выводу: все PHP-код приложения, кроме файла index.php , должен находиться в базе данных MySQL. Их решение может звучать возмутительно сегодня, но оно было приемлемым двенадцать лет назад (хорошо … может и нет).

Они начали с создания своих базовых таблиц, а затем других таблиц для каждой веб-страницы. Решение сработало … какое-то время. Первоначальные авторы знали, как его поддерживать, но затем каждый автор оставлял одного за другим — оставляя кодовую базу в руках других новичков.

Сегодня ни один программист не может понять эту архаичную систему. Все начинается с запроса MySQL из index.php . Результат этого запроса возвращает некоторый код PHP, который выполняет еще больше запросов. Самый простой сценарий включает как минимум пять таблиц базы данных. Естественно, нет тестов или спецификаций. Изменение чего-либо не требуется, и мы просто должны переписать весь модуль, если что-то пойдет не так.

Разработчики оригинала игнорировали тот факт, что база данных должна содержать только данные, а не бизнес-логику или представление. Они смешали PHP и HTML-код с MySQL и проигнорировали концепции проектирования высокого уровня.

Все приложения должны быть сосредоточены на соблюдении чистого, высокого уровня дизайна.

Со временем новые программисты должны были добавить дополнительные функции в систему, в то же время исправляя старые ошибки. Не было никакого способа продолжать использовать таблицы MySQL для всего, и все, кто занимался поддержкой кода, согласились с тем, что его дизайн ужасно испорчен. Таким образом, новые программисты оценили различные требования, подумали о рабочих нагрузках, которые приложению придется обрабатывать, протестировали разные технологии и пришли к выводу: они решили перенести как можно больше кода в окончательную презентацию. Опять же, это решение может звучать сегодня возмутительно, но это были световые годы от предыдущего возмутительного замысла.

Разработчики приняли шаблонную среду и основали приложение на ней, запуская каждую новую функцию и модуль с новым шаблоном. Это было легко; шаблон был описательным, и они знали, где найти код, который выполняет определенную задачу. Но именно так они и получили файлы шаблонов, содержащие домен-специфический язык движка (DSL), HTML, PHP и, конечно же, запросы MySQL.

Сегодня моя команда просто смотрит и чудеса. Это чудо, что многие взгляды действительно работают. Может потребоваться немало времени, чтобы определить, как информация попадает из базы данных в представление. Как и его предшественник, это все большой беспорядок!

Эти разработчики игнорировали тот факт, что представление не должно содержать бизнес-логики или логики постоянства. Они смешали PHP и HTML-код с MySQL и проигнорировали концепции проектирования высокого уровня.


Схема высокого уровня

Макет — это объект, который действует как его реальный аналог, но не выполняет настоящий код.

Все приложения должны быть сосредоточены на соблюдении чистого, высокого уровня дизайна. Это не всегда достижимо, но это должно быть высоким приоритетом. Хороший дизайн высокого уровня имеет хорошо изолированную бизнес-логику. Создание, сохранение и доставка объектов находятся за пределами ядра, а зависимости указывают только на бизнес-логику.

Изоляция бизнес-логики открывает двери для больших возможностей, и все становится чем-то вроде плагина, если внешние зависимости всегда указывают на бизнес-логику. Например, вы можете поменять тяжелую базу данных MySQL на облегченную базу данных SQLite3.

  • Представьте себе возможность отбросить вашу текущую инфраструктуру MVC и заменить ее другой, не затрагивая бизнес-логику.
  • Представьте себе, что вы предоставляете результаты своего приложения через сторонний API, а не через HTTP, или меняете любую стороннюю технологию, которую вы используете сегодня (кроме, конечно, языка программирования), не затрагивая бизнес-логику (или без особых проблем).
  • Представьте, что вы делаете все эти изменения, и ваши тесты все равно пройдут.

Чтобы лучше определить проблемы с плохим, хотя и работающим дизайном, я начну с простого примера, как вы уже догадались, блога. В этом руководстве я буду следовать некоторым принципам разработки через тестирование (TDD) и сделаю тесты понятными, даже если у вас нет опыта работы с TDD. Давайте представим, что вы используете MVC Framework. При сохранении записи в блоге контроллер с именем BlogPost выполняет метод save() . Этот метод подключается к базе данных SQLite для хранения записи блога в базе данных.

Давайте создадим папку с именем Data в папке нашего кода и перейдем к этому каталогу в консоли. Создайте базу данных и таблицу, например:

1
2
3
4
5
6
7
8
$ sqlite3 MyBlog
SQLite version 3.7.13 2012-06-11 02:05:22
Enter «.help» for instructions
Enter SQL statements terminated with a «;»
sqlite> create table BlogPosts (
    title varchar(120) primary key,
    content text,
    published_timestamp timestamp);

Наш метод save() получает значения из формы в виде массива, который называется $data :

01
02
03
04
05
06
07
08
09
10
class BlogPostController {
 
    function save($data) {
        $dbhandle = new SQLite3(‘Data/MyBlog’);
 
        $query = ‘INSERT INTO BlogPosts VALUES(«‘ . $data[‘title’] . ‘»,»‘ . $data[‘content’] . ‘»,»‘ . time(). ‘»)’;
        $dbhandle->exec($query);
    }
 
}

Этот код работает, и вы можете проверить его, вызвав его из другого класса и передав предопределенный массив $data , например так:

1
2
3
4
5
6
7
$this->object = new BlogPostController;
 
$data[‘title’] = ‘First Post Title’;
$data[‘content’] = ‘Some cool content for the first post’;
$data[‘published_timestamp’] = time();
 
$this->object->save($data);

Содержимое переменной $data действительно было сохранено в базе данных:

1
2
sqlite> select * from BlogPosts;
First Post Title|Some cool content for the first post|1345665216

Наследование — это самый сильный тип зависимости.

Характеристический тест описывает и проверяет текущее поведение существующего кода. Он чаще всего используется для характеристики унаследованного кода и значительно упрощает рефакторинг этого кода.

Тест характеристики может протестировать модуль, модуль или пройти весь путь от интерфейса пользователя до базы данных; все зависит от того, что мы хотим проверить. В нашем случае такой тест должен осуществлять контроллер и проверять содержимое базы данных. Это не типичный модульный, функциональный или интеграционный тест, и его обычно нельзя связать ни с одним из этих уровней тестирования.

Тесты характеристик — это временная сеть безопасности, и мы обычно удаляем их после правильного рефакторинга и модульного тестирования. Вот реализация теста, помещенная в папку Test :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
require_once ‘../BlogPostController.php’;
 
class BlogPostControllerTest extends PHPUnit_Framework_TestCase {
    private $object;
    private $dbhandle;
 
    function setUp() {
        $this->object = new BlogPostController;
        $this->dbhandle = new SQLite3(‘../Data/MyBlog’);
    }
 
 
    function testSave() {
        $this->cleanUPDatabase();
 
        $data[‘title’] = ‘First Post Title’;
        $data[‘content’] = ‘Some cool content for the first post’;
        $data[‘published_timestamp’] = time();
        $this->object->save($data);
 
        $this->assertEquals($data, $this->getPostsFromDB());
 
    }
 
    private function cleanUPDatabase() {
        $this->dbhandle->exec(‘DELETE FROM BlogPosts’);
    }
 
    private function getPostsFromDB() {
        $result = $this->dbhandle->query(‘SELECT * FROM BlogPosts’);
        return $result->fetchArray(SQLITE3_ASSOC);
    }
 
}

Этот тест создает новый объект контроллера и выполняет его метод save() . Затем тест считывает информацию из базы данных и сравнивает ее с предопределенным массивом $data[] . Мы проводим это сравнение, используя метод $this->assertEquals() , утверждение, которое предполагает, что его параметры равны. Если они разные, тест не пройден. Кроме того, мы BlogPosts таблицу базы данных BlogPosts каждом запуске теста.

Устаревший код — это непроверенный код. — Майкл Перья

Выполнив тест, давайте немного очистим код. Откройте базу данных с полным именем каталога и используйте sprintf() для составления строки запроса. Это приводит к гораздо более простому коду:

01
02
03
04
05
06
07
08
09
10
11
class BlogPostController {
 
    function save($data) {
        $dbhandle = new SQLite3(__DIR__ . ‘/Data/MyBlog’);
 
        $query = sprintf(‘INSERT INTO BlogPosts VALUES («%s»,»%s»,»%s»)’, $data[‘title’], $data[‘content’], time());
 
        $dbhandle->exec($query);
    }
 
}

Шаблон шлюза

Мы понимаем, что наш код необходимо переместить с контроллера на уровень бизнес-логики и персистентности, и шаблон шлюза может помочь нам начать этот путь. Вот исправленный testSave() :

01
02
03
04
05
06
07
08
09
10
function testItCanPersistABlogPost() {
       $data = array(‘title’ => ‘First Post Title’, ‘content’ => ‘Some content.’, ‘timestamp’ => time());
       $blogPost = new BlogPost($data[‘title’], $data[‘content’], $data[‘timestamp’]);
 
       $mockedPersistence = $this->getMock(‘SqlitePost’);
       $mockedPersistence->expects($this->once())->method(‘persist’)->with($blogPost);
 
       $controller = new BlogPostController($mockedPersistence);
       $controller->save($data);
   }

Это представляет, как мы хотим использовать метод save() на контроллере. Мы ожидаем, что контроллер вызовет метод с именем persist($blogPostObject) для объекта шлюза. Давайте изменим наш BlogPostController чтобы сделать это:

01
02
03
04
05
06
07
08
09
10
11
12
class BlogPostController {
    private $gateway;
 
    function __construct(Gateway $gateway = null) {
        $this->gateway = $gateway ?
    }
 
    function save($data) {
        $this->gateway->persist(new BlogPost($data[‘title’], $data[‘content’], $data[‘timestamp’]));
    }
 
}

Хороший дизайн высокого уровня имеет хорошо изолированную бизнес-логику.

Ницца! Наш BlogPostController стал намного проще. Он использует шлюз (предоставленный или созданный) для сохранения данных путем вызова его метода persist() . Нет абсолютно никаких знаний о том, как данные сохраняются; логика постоянства стала модульной.

В предыдущем тесте мы создали контроллер с фиктивным объектом персистентности, гарантирующий, что данные никогда не будут записаны в базу данных при запуске теста. В SqlitePost коде контроллер создает свой собственный постоянный объект для сохранения данных с использованием объекта SqlitePost . Макет — это объект, который действует как его реальный аналог, но он не выполняет настоящий код.

Теперь давайте возьмем сообщение в блоге из хранилища данных. Это так же просто, как сохранить данные, но учтите, что я немного реорганизовал тест.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
require_once ‘../BlogPostController.php’;
require_once ‘../BlogPost.php’;
 
require_once ‘../SqlitePost.php’;
 
class BlogPostControllerTest extends PHPUnit_Framework_TestCase {
        private $mockedPersistence;
        private $controller;
        private $data;
 
    function setUp() {
        $this->mockedPersistence = $this->getMock(‘SqlitePost’);
        $this->controller = new BlogPostController($this->mockedPersistence);
        $this->data = array(‘title’ => ‘First Post Title’, ‘content’ => ‘Some content.’, ‘timestamp’ => time());
    }
 
    function testItCanPersistABlogPost() {
        $blogPost = $this->aBlogPost();
        $this->mockedPersistence->expects($this->once())->method(‘persist’)->with($blogPost);
 
        $this->controller->save($this->data);
    }
 
    function testItCanRetrievABlogPostByTitle() {
        $expectedBlogpost = $this->aBlogPost();
        $this->mockedPersistence->expects($this->once())
                ->method(‘findByTitle’)->with($this->data[‘title’])
                ->will($this->returnValue($expectedBlogpost));
 
        $this->assertEquals($expectedBlogpost, $this->controller->findByTitle($this->data[‘title’]));
    }
 
    public function aBlogPost() {
        return new BlogPost($this->data[‘title’], $this->data[‘content’], $this->data[‘timestamp’]);
    }
 
}

А реализация в BlogPostController — это просто метод с одним оператором:

1
2
3
function findByTitle($title) {
       return $this->gateway->findByTitle($title);
   }

Разве это не круто? Класс BlogPost теперь является частью бизнес-логики (вспомните схему проектирования верхнего уровня сверху). Пользовательский интерфейс / MVC создает объекты BlogPost и использует конкретные реализации Gateway для сохранения данных. Все зависимости указывают на бизнес-логику.

Остался только один шаг: создать конкретную реализацию Gateway . Ниже SqlitePost класс SqlitePost :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
require_once ‘Gateway.php’;
 
class SqlitePost implements Gateway {
    private $dbhandle;
 
    function __construct($dbhandle = null) {
        $this->dbhandle = $dbhandle ?
    }
 
    public function persist(BlogPost $blogPost) {
        $query = sprintf(‘INSERT INTO BlogPosts VALUES («%s»,»%s»,»%s»)’, $blogPost->title, $blogPost->content, $blogPost->timestamp);
        $this->dbhandle->exec($query);
    }
 
    public function findByTitle($title) {
        $SqliteResult = $this->dbhandle->query(sprintf(‘SELECT * FROM BlogPosts WHERE title = «%s»‘, $title));
        $blogPostAsString = $SqliteResult->fetchArray(SQLITE3_ASSOC);
        return new BlogPost($blogPostAsString[‘title’], $blogPostAsString[‘content’], $blogPostAsString[‘timestamp’]);
    }
}

Примечание. Тест для этой реализации также доступен в исходном коде, но из-за его сложности и длины я не включил его здесь.


Active Record — одна из самых противоречивых моделей. Некоторые принимают это (например, Rails и CakePHP), а другие избегают. Многие приложения Object Relational Mapping (ORM) используют этот шаблон для сохранения объектов в таблицах. Вот его схема:

Шаблон активной записи

Как видите, объекты на основе Active Record могут сохраняться и извлекать себя. Обычно это достигается путем расширения класса ActiveRecordBase класса, который знает, как работать с базой данных.

Самая большая проблема с Active Record — это зависимость extends . Как мы все знаем, наследование — это самый сильный тип зависимости, и лучше всего избегать его большую часть времени.

Прежде чем мы пойдем дальше, вот где мы сейчас находимся:

Шлюз в схеме высокого уровня

Интерфейс шлюза относится к бизнес-логике, а его конкретные реализации относятся к уровню персистентности. Наш BlogPostController имеет две зависимости, обе указывают на бизнес-логику: шлюз BlogPost класс BlogPost .

Есть много других шаблонов, таких как Шаблон Proxy , которые тесно связаны с постоянством.

Если бы мы следовали шаблону Active Record в точности так, как он представлен Мартином Фаулером в его книге 2003 года « Шаблоны архитектуры корпоративных приложений» , нам бы пришлось перенести SQL-запросы в класс BlogPost . Это, однако, имеет проблему нарушения как принципа обращения зависимостей, так и принципа открытого закрытого типа . Принцип обращения зависимостей гласит:

  • Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

И открытый закрытый принцип гласит: программные объекты (классы, модули, функции и т. Д.) Должны быть открыты для расширения, но закрыты для модификации. Мы выберем более интересный подход и интегрируем шлюз в наше решение Active Record.

Если вы попытаетесь сделать это самостоятельно, вы, вероятно, уже поняли, что добавление шаблона Active Record в код приведет к путанице. По этой причине я SqlitePost тестирование контроллера и SqlitePost чтобы сосредоточиться только на классе BlogPost . Первые шаги: BlogPost загрузить себя, установив его конструктор как приватный и подключив его к интерфейсу шлюза. Вот первая версия файла BlogPostTest :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
require_once ‘../BlogPost.php’;
require_once ‘../InMemoryPost.php’;
require_once ‘../ActiveRecordBase.php’;
 
class BlogPostTest extends PHPUnit_Framework_TestCase {
 
 
    function testItCanConnectPostToGateway() {
        $blogPost = BlogPost::load();
        $blogPost->setGateway($this->inMemoryPost());
        $this->assertEquals($blogPost->getGateway(), $this->inMemoryPost());
    }
 
    function testItCanCreateANewAndEmptyBlogPost() {
        $blogPost = BlogPost::load();
        $this->assertNull($blogPost->title);
        $this->assertNull($blogPost->content);
        $this->assertNull($blogPost->timestamp);
        $this->assertInstanceOf(‘Gateway’, $blogPost->getGateway());
    }
 
    private function inMemoryPost() {
        return new InMemoryPost();
    }
 
}

Он проверяет, правильно ли инициализирована запись в блоге, и может ли она иметь шлюз, если он установлен. Рекомендуется использовать несколько утверждений, когда все они проверяют одну и ту же концепцию и логику.

Наш второй тест имеет несколько утверждений, но все они относятся к одной и той же общей концепции пустого поста в блоге . Конечно, класс BlogPost также был изменен:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class BlogPost {
    private $title;
    private $content;
    private $timestamp;
    private static $gateway;
 
    private function __construct($title = null, $content = null, $timestamp = null) {
        $this->title = $title;
        $this->content = $content;
        $this->timestamp = $timestamp;
    }
 
    function __get($name) {
        return $this->$name;
    }
 
    function setGateway($gateway) {
        self::$gateway = $gateway;
    }
 
    function getGateway() {
        return self::$gateway;
    }
 
    static function load() {
        if(!self::$gateway) self::$gateway = new SqlitePost();
        return new self;
    }
}

Теперь у него есть метод load() который возвращает новый объект с допустимым шлюзом. С этого момента мы продолжим реализацию метода load($title) для создания нового BlogPost с информацией из базы данных. Для удобства тестирования я реализовал класс InMemoryPost для сохранения. Он просто хранит список объектов в памяти и возвращает информацию по желанию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
class InMemoryPost implements Gateway {
    private $blogPosts = array();
 
    public function findByTitle($blogPostTitle) {
        return array(
            ‘title’ => $this->blogPosts[$blogPostTitle]->title,
            ‘content’ => $this->blogPosts[$blogPostTitle]->content,
            ‘timestamp’ => $this->blogPosts[$blogPostTitle]->timestamp);
 
    }
 
    public function persist(BlogPost $blogPostObject) {
        $this->blogPosts[$blogPostObject->title] = $blogPostObject;
    }
}

Затем я понял, что первоначальная идея подключения BlogPost к шлюзу через отдельный метод была бесполезна. Итак, я изменил тесты, соответственно:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class BlogPostTest extends PHPUnit_Framework_TestCase {
 
    function testItCanCreateANewAndEmptyBlogPost() {
        $blogPost = BlogPost::load();
        $this->assertNull($blogPost->title);
        $this->assertNull($blogPost->content);
        $this->assertNull($blogPost->timestamp);
    }
 
    function testItCanLoadABlogPostByTitle() {
        $gateway = $this->inMemoryPost();
        $aBlogPosWithData = $this->aBlogPostWithData($gateway);
 
        $gateway->persist($aBlogPosWithData);
 
        $this->assertEquals($aBlogPosWithData, BlogPost::load(‘some_title’, null, null, $gateway));
    }
 
    private function inMemoryPost() {
        return new InMemoryPost();
    }
 
    private function aBlogPostWithData($gateway = null) {
        return BlogPost::load(‘some_title’, ‘some content’, ‘123’, $gateway);
    }
 
}

Как видите, я радикально изменил способ использования BlogPost .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class BlogPost {
    private $title;
    private $content;
    private $timestamp;
 
    private function __construct($title = null, $content = null, $timestamp = null) {
        $this->title = $title;
        $this->content = $content;
        $this->timestamp = $timestamp;
    }
 
    function __get($name) {
        return $this->$name;
    }
 
    static function load($title = null, $content = null, $timestamp = null, $gateway = null) {
        $gateway = $gateway ?
 
        if(!$content) {
            $postArray = $gateway->findByTitle($title);
            if ($postArray) return new self($postArray[‘title’], $postArray[‘content’], $postArray[‘timestamp’]);
        }
 
        return new self($title, $content, $timestamp);
    }
}

Метод load() проверяет параметр $content на значение и создает новый BlogPost если значение было предоставлено. Если нет, метод пытается найти сообщение в блоге с заданным названием. Если сообщение найдено, оно возвращается; если его нет, метод создает пустой объект BlogPost .

Чтобы этот код работал, нам также необходимо изменить работу шлюза. Наша реализация должна возвращать ассоциативный массив с элементами title , content и timestamp вместо самого объекта. Это соглашение, которое я выбрал. Вы можете найти другие варианты, такие как простой массив, более привлекательными. Вот модификации в SqlitePostTest :

01
02
03
04
05
06
07
08
09
10
11
12
function testItCanRetrieveABlogPostByItsTitle() {
       […]
       //we expect an array instead of an object
       $this->assertEquals($this->blogPostAsArray, $gateway->findByTitle($this->blogPostAsArray[‘title’]));
   }
   private function aBlogPostWithValues() {
       //we use static load instead of constructor call
       return $blogPost = BlogPost::load(
               $this->blogPostAsArray[‘title’],
               $this->blogPostAsArray[‘content’],
               $this->blogPostAsArray[‘timestamp’]);
   }

И изменения реализации:

1
2
3
4
5
public function findByTitle($title) {
       $SqliteResult = $this->dbhandle->query(sprintf(‘SELECT * FROM BlogPosts WHERE title = «%s»‘, $title));
       //return the result directly, don’t construct the object
       return $SqliteResult->fetchArray(SQLITE3_ASSOC);
   }

Мы почти закончили. Добавьте метод BlogPost persist() в BlogPost и вызовите все недавно реализованные методы из контроллера. Вот метод persist() который будет использовать только метод persist() шлюза:

1
2
3
private function persist() {
       $this->gateway->persist($this);
   }

И контроллер:

01
02
03
04
05
06
07
08
09
10
11
12
class BlogPostController {
 
    function save($data) {
        $blogPost = BlogPost::load($data[‘title’], $data[‘content’], $data[‘timestamp’]);
        $blogPost->persist();
    }
 
    function findByTitle($title) {
        return BlogPost::load($title);
    }
 
}

BlogPostController стал настолько простым, что я удалил все его тесты. Он просто вызывает метод BlogPost persist() объекта BlogPost . Естественно, вы захотите добавить тесты, если и когда у вас будет больше кода в контроллере. Загрузка кода по-прежнему содержит тестовый файл для BlogPostController , но его содержание прокомментировано.


Это только верхушка айсберга.

Вы видели две разные реализации персистентности: шаблоны Gateway и Active Record . С этого момента вы можете реализовать абстрактный класс ActiveRecordBase для расширения для всех ваших классов, которым требуется постоянство. Этот абстрактный класс может использовать разные шлюзы для сохранения данных, и каждая реализация может даже использовать разную логику, чтобы соответствовать вашим потребностям.

Но это только верхушка айсберга. Есть много других шаблонов, таких как шаблон Proxy , которые тесно связаны с постоянством; каждый шаблон работает для конкретной ситуации. Я рекомендую сначала всегда реализовывать простейшее решение, а затем реализовывать другой шаблон, когда ваши потребности меняются.

Я надеюсь, что вам понравился этот урок, и я с нетерпением жду ваших мнений и альтернативных вариантов реализации моего решения в комментариях ниже.