Об авторе joey Ant Financial Data Experience Technology Team
предисловие
Работа нашей команды заключается в реализации веб-инструментов в виде одностраничного приложения. Он включает в себя управление десятками тысяч и сотнями тысяч строк внешнего кода, а проектный цикл длится несколько лет.
Как хорошо управлять интерфейсным кодом такого масштаба и поддерживать актуальность кода во время процесса итерации — это сложная задача для нас.
Хорошо работающий проект, помимо наличия хорошей архитектуры, также требует хорошего дизайна каждого функционального модуля.Изучение шаблонов проектирования позволяет умело проектировать новые функции и проводить рефакторинг существующего кода.
В интернете есть много поговорок о том, что изучение паттернов проектирования неэффективно, а некоторые паттерны устарели, и без обучения можно работать, но после изучения легко перепроектировать.
Я думаю, что понимание вещей — это процесс «обучение — понимание — прорыв». Когда вы не понимаете, сначала научитесь, а когда есть разница между тем, что вы узнали, и практическим опытом, вы можете понять это, подумав. И очевидно, что перепроектирование все еще находится между обучением и пониманием.
То же самое относится и к шаблонам проектирования. 23 шаблона проектирования, перечисленные в разделе «Шаблоны проектирования», — это не все из них. Применение шаблонов часто не столь очевидно, и несколько шаблонов часто используются в комбинации. Изучение паттернов проектирования подобно тому, как Чжан Уцзи изучает тайцзи-цюань в «Вечном убийце драконов»: сначала он изучает движения, затем повторяет их несколько раз и, в конце концов, забывает эти движения. 23 режима проектирования — это просто ходы.Цель нашего исследования — повысить уровень нашего дизайна и достичь царства «Махаяна», где мы можем разрабатывать схемы в сочетании со сценами, не ограничиваясь ходами.
В процессе изучения шаблонов проектирования я обнаружил, что исходный демонстрационный код книги Gang of 4 написан на C++, а демо-статьи о шаблонах проектирования в Интернете в основном написаны на Java. Таким образом, в сочетании с интерфейсными функциями языка js, демонстрации каждого режима отсортированы, чтобы учащиеся, заинтересованные в изучении режимов проектирования, могли понять и добиться прогресса вместе.
описание сцены
Предположим, мы хотим реализовать лабиринт, исходный код выглядит следующим образом:
function createMazeDemo() {
const maze = new Maze();
const r1 = new Room(1);
const r2 = new Room(2);
const door = new Door(r1, r2);
maze.addRoom(r1);
maze.addRoom(r2);
r1.setSide('east', new Wall());
r1.setSide('west', door);
return maze;
}
createMazeDemo();
Мы уже достигли лабиринта, на этот раз приходит новый спрос, все вещи в лабиринте наделены магией, но все еще необходимо повторно использовать существующие макеты (все компоненты, такие как Комната, Стена, Дверь, должны быть заменены новым типом).
Видно, что этот метод жесткого кодирования недостаточно гибок, так как же модифицировать метод createMaze, чтобы он мог легко создавать лабиринты с новыми типами объектов?
Общее определение понятия
- Система: содержимое, создаваемое всей программой, например лабиринт, представляет собой систему;
- Продукт: объекты, составляющие систему, такие как дверь лабиринта, комната и стена, являются продуктом;
Абстрактная фабрика
определение
Предоставляет интерфейс для создания ряда связанных или взаимозависимых объектов без указания их конкретных классов.
структура
Шаблон абстрактной фабрики включает следующие роли:
- AbstractFactory: абстрактная фабрика
- ConcreteFactory: бетонный завод
- AbstractProduct: абстрактный продукт
- Продукт: конкретный продукт
Пример
// 迷宫的基类
class Maze {
addRoom(room: Room): void {
}
}
// 墙的基类
class Wall {
}
// 房间的基类
class Room {
constructor(id: number) {
}
setSide(direction: string, content: Room|Wall): void {
}
}
// 门的基类
class Door {
constructor(roo1: Room, room2: Room) {
}
}
// 迷宫工厂的基类,定义了生成迷宫各个构件的接口和默认实现,
// 子类可以复写接口的实现,返回不同的具体类对象。
class MazeFactory {
makeMaze(): Maze {
return new Maze();
}
makeWall(): Wall {
return new Wall();
}
makeRoom(roomId: number): Room {
return new Room(roomId);
}
makeDoor(room1: Room, room2: Room): Door {
return new Door(room1, room2);
}
}
// 通过传入工厂对象,调用工厂的接口方法创建迷宫,
// 由于工厂的接口都是一样的,所以传入不同的工厂对象,就能创建出不同系列的具体产品
function createMazeDemo(factory: MazeFactory): Maze {
const maze = factory.makeMaze();
const r1 = factory.makeRoom(1);
const r2 = factory.makeRoom(2);
const door = factory.makeDoor(r1, r2);
maze.addRoom(r1);
maze.addRoom(r2);
r1.setSide('east', factory.makeWall());
r1.setSide('west', door);
return maze;
}
// 标准系列工厂对象,工厂的每个产品都是标准的
const standardSeries = new MazeFactory();
// 创建出标准的迷宫
createMazeDemo(standardSeries);
// 附了魔法的房间,继承自房间的基类
class MagicRoom extends Room {
...
}
// 附了魔法的门,继承自门的基类
class MagicDoor extends Door {
...
}
// 魔法系列的工厂,工厂的房间和门是被附了魔法的
class MagicMazeFactory extends MazeFactory {
makeRoom(roomId: number): Room {
return new MagicRoom(roomId);
}
makeDoor(room1: Room, room2: Room): Door {
return new MagicDoor(room1, room2);
}
...
}
// 魔法系列工厂对象,工厂创建出的门和房间是附了魔法的
const magicSeries = new MagicMazeFactory();
createMazeDemo(magicSeries);
Применимая сцена
- Массив связанных объектов продукта. Если есть волшебная комната и волшебная дверь, то они относятся к компоненту волшебного лабиринта и имеют определенную взаимосвязь;
- Система должна быть сконфигурирована одним из нескольких семейств продуктов. Например, когда у пользователя есть выбор из нескольких стилей лабиринта, когда выбран черный стиль, все компоненты лабиринта должны быть компонентами черного стиля;
- Система не зависит от конкретной категории продукта. Например, при написании программы-лабиринта вам не нужно заботиться о том, какой именно класс комнаты используется, вам нужно только знать интерфейс базового класса комнаты, чтобы управлять комнатой. Классы комнат, возвращаемые различными фабриками лабиринтов, наследуются от базового класса комнаты, а интерфейс тот же.Даже если обычная комната заменена волшебной комнатой, нет необходимости оперировать кодом комнаты, например, код макета лабиринта и позаботьтесь об изменении.
преимущество
- Конкретные классы разделены. Если лабиринт должен быть превращен в волшебный лабиринт, код макета лабиринта
createMazeDemo
отсутствие необходимости коррекции; - Простая замена продуктовых линеек. Когда лабиринт заменяется волшебным лабиринтом, вам нужно только
createMazeDemo
Просто передайте новый заводской объект; - Способствует консистенции продукта. Продукты одной серии сильно коррелированы. Например, все определенные объекты в волшебном лабиринте волшебные.
недостаток
- Сложности с поддержкой новых видов продукции. Это связано с тем, что интерфейс абстрактной фабрики определяет набор продуктов, которые могут быть созданы.Для поддержки новых видов продуктов интерфейс фабрики необходимо расширить, что будет включать изменения в класс абстрактной фабрики и все его подклассы.
Связанные шаблоны
- Абстрактный фабричный шаблон обычно реализуется с помощью фабричных методов, но его также можно реализовать с помощью шаблона-прототипа.
- Бетонная фабрика обычно представляет собой одноэлементный шаблон.
Строитель
определение
Отделение построения сложного объекта от его представления позволяет одному и тому же процессу построения создавать различные представления.
структура
Режим строителя включает в себя следующие роли:
- Строитель: абстрактный строитель
- ConcreteBuilder: конструктор бетона
- Режиссер: дирижёр
- Продукт: Роль продукта
Пример
import { Maze, Wall, Room, Door } from './common';
// 迷宫建造者基类,定义了所有生成迷宫构件的接口,以及最终返回完整迷宫的接口
// 自身不创建迷宫,仅仅定义接口
class MazeBuilder {
buildMaze(): void {}
buildWall(roomId: number, direction: string): void {}
buildRoom(roomId: number): void {}
buildDoor(roomId1: number, roomId2: number): void {}
getCommonWall(roomId1: number, roomId2: number): Wall { return new Wall(); };
getMaze(): Maze|null { return null; }
}
// 创建迷宫的流程
// 相比最原始的代码,使用建造者模式只需要声明建造过程,而不需要知道建造过程中用到的每个构件的所有信息
// 比如,建造门的时候,只需要声明要建造一扇门,而不需要关心建造门的方法内部是如何将门与房间关联起来的
// 建造者模式只在迷宫被完全建造完成时,才从建造者对象里取出整个迷宫,从而能很好地反映出完整的建造过程
function createMaze(builder: MazeBuilder) {
builder.buildMaze();
builder.buildRoom(1);
builder.buildRoom(2);
builder.buildDoor(1, 2);
return builder.getMaze();
}
// 标准迷宫的建造者,继承自建造者基类
class StandardMazeBuilder extends MazeBuilder {
currentMaze: Maze;
constructor() {
super();
this.currentMaze = new Maze();
}
getMaze(): Maze|null {
return this.currentMaze;
}
buildRoom(roomId: number): void {
if (this.currentMaze) {
const room = new Room(roomId);
this.currentMaze.addRoom(room);
room.setSide('north', new Wall());
room.setSide('south', new Wall());
room.setSide('east', new Wall());
room.setSide('west', new Wall());
}
}
buildDoor(roomId1: number, roomId2: number): void {
const r1 = this.currentMaze.getRoom(roomId1);
const r2 = this.currentMaze.getRoom(roomId2);
const door = new Door(r1, r2);
r1.setSide(this.getCommonWall(roomId1, roomId2), door);
r2.setSide(this.getCommonWall(roomId2, roomId1), door);
}
}
// 建造一个标准的迷宫
const standardBuilder = new StandardMazeBuilder();
createMaze(standardBuilder);
/**
* 建造者也可以根本不建造具体的构件,而只是对建造过程进行计数。
*/
// 计数的数据结构声明
interface Count {
rooms: number;
doors: number;
}
// 不创建迷宫,只记数的建造者,也继承自建造者基类
class CountingMazeBuilder extends MazeBuilder {
doors = 0;
rooms = 0;
buildRoom(): void {
this.rooms += 1;
}
buildDoor(): void {
this.doors += 1;
}
getCounts(): Count {
return {
rooms: this.rooms,
doors: this.doors,
};
}
}
const countBuilder = new CountingMazeBuilder();
createMaze(countBuilder);
countBuilder.getCounts();
Применимая сцена
- Когда алгоритм создания сложного объекта должен быть независим от того, из чего этот объект сделан и как они собраны. В приведенном выше примере процесс построения лабиринта эквивалентен алгоритму, и каждый метод генерации компонентов включает в себя способ создания определенных объектов и сборки определенных объектов в лабиринте. Один и тот же процесс строительства может быть адаптирован для многих разных строителей, и один и тот же строитель может поддерживать множество разных строительных процессов, эти два процесса независимы друг от друга.
- Когда процесс строительства должен допускать различные представления построенного объекта. Процесс строительства такой же, но он должен поддерживать множество различных методов сборки. Как и в приведенном выше примере, один и тот же процесс строительства должен поддерживать два разных сценария. В одном сценарии нужно построить настоящий лабиринт, а в другом только сценарий. нужно подсчитать процесс строительства.Сколько комнат и сколько дверей построено в Китае.
преимущество
- Можно изменить внутреннее представление продукта. Интерфейс может скрывать представление и внутреннюю структуру изделия, и в то же время скрывать процесс сборки изделия. Поскольку продукты создаются с помощью абстрактных интерфейсов, все, что вам нужно сделать, чтобы изменить внутреннее представление этого продукта, — это определить новый конструктор.
- Отделите код построения от кода представления. Шаблон Builder улучшает модульность объектов, инкапсулируя способ создания и представления сложного объекта. Клиенту не нужно знать всю информацию о классах, определяющих внутреннюю структуру продукта.
- Возможен более детальный контроль над процессом строительства. Шаблон Builder извлекает продукт из Builder только после его завершения, поэтому шаблон Builder лучше отражает процесс создания продукта, чем другие шаблоны создания.
Связанные шаблоны
- Абстрактная фабрика похожа на шаблон Builder в том, что она также может создавать сложные объекты. Основное отличие состоит в том, что шаблон построителя фокусируется на пошаговом построении сложного объекта. Абстрактная фабрика, с другой стороны, фокусируется на нескольких сериях объектов продукта. Строитель возвращает продукт на последнем этапе, тогда как для абстрактной фабрики продукт возвращается немедленно.
- Шаблоны комбинаций обычно генерируются с использованием шаблона построителя.
Фабричный метод
определение
Определите интерфейс для создания объектов и позвольте подклассам решать, какой класс создавать. Фабричный метод задерживает создание экземпляра класса для его подклассов.
структура
Шаблон Factory Method включает следующие роли:
- Продукт: абстрактный продукт
- ConcreteProduct: Бетонный продукт
- Фабрика: абстрактная фабрика
- ConcreteFactory: бетонный завод
Пример
Подклассы решают создавать конкретные продукты
import { Maze, Wall, Room, Door } from './common';
// 迷宫游戏类
class MazeGame {
// 创建迷宫的主方法
createMaze(): Maze {
const maze = this.makeMaze();
const r1 = this.makeRoom(1);
const r2 = this.makeRoom(2);
const door = this.makeDoor(r1, r2);
maze.addRoom(r1);
maze.addRoom(r2);
r1.setSide('north', this.makeWall());
r1.setSide('east', door);
return maze;
}
// 以下是工厂方法,通过工厂方法创建构件,而不是直接在主方法中new出具体类
// 工厂方法最重要的是定义出返回产品的接口,虽然这里提供了默认实现,但也可以只提供接口,让子类来实现
makeMaze(): Maze { return new Maze(); }
makeRoom(roomId: number): Room { return new Room(roomId); }
makeWall(): Wall { return new Wall(); }
makeDoor(room1: Room, room2: Room): Door { return new Door(room1, room2); }
}
// 创建普通迷宫游戏
const mazeGame = new MazeGame();
mazeGame.createMaze();
// 带炸弹的墙
class BombedWall extends Wall {
...
}
// 带炸弹的房间
class RoomWithABomb extends Room {
...
}
// 子类可以复写工厂方法,以下是带炸弹的迷宫游戏类
class BombedMazeGame extends MazeGame {
// 复写创建墙的方法,返回一面带炸弹的墙
makeWall(): Wall {
return new BombedWall();
}
// 复写创建房间的方法,返回一个带炸弹的房间
makeRoom(roomId: number): Room {
return new RoomWithABomb(roomId);
}
}
// 创建带炸弹的迷宫游戏
const bombedMazeGame = new BombedMazeGame();
bombedMazeGame.createMaze();
Параметризованные фабричные методы
class Creator {
createProduct(type: string): Product {
if (type === 'normal') return new NormalProduct();
if (type === 'black) return new BlackProduct();
return new DefaultProduct();
}
}
// 子类可以很容易地扩展或改变工厂方法返回的产品
class MyCreator extends Creator {
createProduct(type: string): Product {
// 改变产品
if (type === 'normal) return new MyNormalProduct();
// 扩展新的产品
if (type === 'white') return new WhiteProduct();
// 注意这个操作的最后一件事是调用父类的`createProduct`,这是因为子类仅对某些type的处理上与父类不同,对其他的type不感兴趣
return Creator.prototype.createProduct.call(this, type);
}
}
Применимая сцена
- Когда класс не знает класс объекта, который он должен создать.
- Когда класс хочет, чтобы его подклассы определяли объекты, которые он создает.
- Когда класс делегирует создание объекта одному из нескольких вспомогательных подклассов, и вы хотите локализовать информацию о том, какой вспомогательный подкласс является делегатом. Это сценарий параметризованного фабричного метода, где процесс указания конкретного класса по параметру локализован в фабричном методе.
преимущество
- Использование фабричных методов для создания объектов внутри класса часто более гибко, чем непосредственное создание объектов. в сравнении с
createMaze
Создавать объект прямо в методеconst r1 = new Room(1);
, заводским методомconst r1 = this.makeRoom(1)
, который можно переопределить в подклассахmakeRoom
метод для создания экземпляров разных комнат, который может быть более гибким, чтобы реагировать на изменения спроса.
Связанные шаблоны
- Абстрактные фабрики часто реализуются с использованием фабричных методов.
- Фабричные методы обычно вызываются в шаблоне метода шаблона.
- Шаблон прототипа не требует создания подклассов, но обычно требует операции инициализации класса продукта. Фабричный метод не требует такой операции.
Прототип
определение
Используйте экземпляры прототипов, чтобы указать типы создаваемых объектов, и создавайте новые объекты, повторно используя эти прототипы.
Пример
На других языках образец прототипа снижает накладные расходы определения классов и создания экземпляра путем копирования объекта, а затем модифицируя свойства нового объекта.
Однако, поскольку js естественным образом поддерживает прототип, реализация прототипа несколько отличается от других языков наследования классов, и его не нужно предоставлять через объекты.clone
способ реализации шаблона модели.
import { Maze, Wall, Room, Door } from './common';
interface Prototype {
prototype?: any;
}
// 根据原型返回对象
function getNewInstance(prototype: Prototype, ...args: any[]): Wall|Maze|Room|Door {
const proto = Object.create(prototype);
const Kls = class {};
Kls.prototype = proto;
return new Kls(...args);
}
// 迷宫工厂,定义了生成构件的接口
class MazeFactory {
makeWall(): Wall { return new Wall(); }
makeDoor(r1: Room, r2: Room): Door { return new Door(r1, r2); }
makeRoom(id: number): Room { return new Room(id); }
makeMaze(): Maze { return new Maze(); }
}
// 原型迷宫工厂,根据初始化时传入的原型改变返回的迷宫构件
class MazePrototypeFactory extends MazeFactory {
mazePrototype: Prototype;
wallPrototype: Prototype;
roomPrototype: Prototype;
doorPrototype: Prototype;
constructor(mazePrototype: Prototype, wallPrototype: Prototype, roomPrototype: Prototype, doorPrototype: Prototype) {
super();
this.mazePrototype = mazePrototype;
this.wallPrototype = wallPrototype;
this.roomPrototype = roomPrototype;
this.doorPrototype = doorPrototype;
}
makeMaze() {
return getNewInstance(this.mazePrototype);
}
makeRoom(id: number) {
return getNewInstance(this.roomPrototype, id);
}
makeWall() {
return getNewInstance(this.wallPrototype);
}
makeDoor(r1: Room, r2: Room): Door {
const door = getNewInstance(this.doorPrototype, r1, r2);
return door;
}
}
// 创建迷宫的过程
function createMaze(factory: MazeFactory): Maze {
const maze = factory.makeMaze();
const r1 = factory.makeRoom(1);
const r2 = factory.makeRoom(2);
const door = factory.makeDoor(r1, r2);
maze.addRoom(r1);
maze.addRoom(r2);
r1.setSide('east', factory.makeWall());
r1.setSide('west', door);
return maze;
}
// 各个迷宫构件的原型
const mazePrototype = {...};
const wallPrototype = {...};
const roomPrototype = {...};
const doorPrototype = {...};
// 生成简单的迷宫
const simpleMazeFactory = new MazePrototypeFactory(mazePrototype, wallPrototype, roomPrototype, doorPrototype);
createMaze(simpleMazeFactory);
// 带有炸弹的迷宫构件的原型
const bombedWallPrototype = {...};
const roomWithABombPrototype = {...};
// 生成带有炸弹的迷宫
const bombedMazeFactory = new MazePrototypeFactory(mazePrototype, bombedWallPrototype, roomWithABombPrototype, doorPrototype);
createMaze(bombedMazeFactory);
Применимая сцена
- Класс для создания экземпляра указывается во время выполнения. Например, после получения прототипа через динамическую загрузку экземпляр создается непосредственно по прототипу во время работы программы, а не через предопределенный класс.
new
вне объекта. Или класс должен динамически изменяться в зависимости от среды выполнения, и различные объекты могут быть созданы путем изменения прототипа.
преимущество
- Добавляйте продукты во время выполнения. Потому что новые типы объектов могут динамически генерироваться из прототипов во время выполнения.
- Сократите построение подклассов и уменьшите количество классов, используемых в системе.
- Динамически настройте свое приложение с помощью классов. Загружайте классы динамически во время выполнения.
Связанные шаблоны
- Шаблон «Прототип» и шаблон «Абстрактная фабрика» в некотором роде конкурируют друг с другом. Но их можно использовать и вместе. Абстрактная фабрика может хранить коллекцию прототипов и возвращать объекты продукта.
- Дизайны, которые интенсивно используют шаблоны композиции и оформления, часто также могут извлечь выгоду из шаблонов прототипов.
Синглтон
определение
Гарантирует наличие только одного экземпляра класса и предоставляет к нему глобальную точку доступа.
структура
Шаблон singleton включает в себя следующие роли:
- Синглтон: синглтон
Пример
простой синглтон
class MazeFactory {
// 将constructor设为私有,防止通过new该类产生多个对象,破坏单例
private constructor() {}
static instance: MazeFactory;
// 如果已经有了对象,则返回缓存的对象,不然就创建一个对象并缓存,保证系统内最多只有一个该类的对象
static getInstance(): MazeFactory {
if (!MazeFactory.instance) {
MazeFactory.instance = new MazeFactory();
}
return MazeFactory.instance;
}
}
Выберите фабрику лабиринтов для создания экземпляра на основе параметров
class BombedMazeFactory extends MazeFactory {
...
}
class AdvancedMazeFactory {
// 将constructor设为私有,防止通过new该类产生多个对象,破坏单例
private constructor() {}
static instance: MazeFactory;
static getInstance(type: string): MazeFactory {
if (!AdvancedMazeFactory.instance) {
if (type === 'bombed') {
AdvancedMazeFactory.instance = new BombedMazeFactory();
} else {
AdvancedMazeFactory.instance = new MazeFactory();
}
}
return AdvancedMazeFactory.instance;
}
}
Применимая сцена
- Когда класс может иметь только один экземпляр и пользователь может получить к нему доступ из известной точки доступа;
- когда единственный экземпляр должен быть расширяемым путем создания подклассов, как в приведенном выше примере, по расширению параметра, и пользователь должен иметь возможность использовать расширенный экземпляр без изменения кода;
преимущество
- Существует эффективно контролируемый доступ к уникальным экземплярам.
- Предотвращает загрязнение пространства имен глобальными переменными, хранящими уникальные экземпляры.
- Конкретный используемый экземпляр можно изменить в методе создания экземпляра.
- Допускается переменное количество экземпляров. Этот шаблон позволяет легко передумать и позволяет одновременно существовать нескольким экземплярам одноэлементного класса. Поскольку запись создания экземпляра находится в одном месте, удобно контролировать количество экземпляров, которым разрешено существовать одновременно.
Связанные шаблоны
- Многие шаблоны могут быть реализованы с помощью синглетонов, таких как абстрактные фабрики, конструкторы и прототипы.
Справочная документация
- Шаблоны проектирования: элементы многоразового объектно-ориентированного программного обеспечения
- Шаблон дизайна иллюстрации - абстрактный заводской шаблон
- Шаблон проектирования рисунка - режим строителя
- Шаблон дизайна иллюстрации - заводской шаблон
- Шаблон дизайна иллюстрации - шаблон singleton
В этой статье представлен только режим создания. Учащиеся, заинтересованные в режиме продолжения, могут подписаться на колонку или отправить свое резюме по адресу 'chaofeng.lcf####alibaba-inc.com'.replace('####' , '@'), приглашаем людей с высокими идеалами присоединиться~
Оригинальный адрес: https://juejin.cn/post/6844903508018364423