Практический опыт многоуровневой архитектуры интерфейса (с открытым исходным кодом)

внешний фреймворк

В последнее время автор использует идею DDD / Clean Architecture для разработки CRM, используемой в компании.Я чувствую, что эта многоуровневая архитектура может решить текущие проблемы, поэтому я решил провести рефакторинг текущего проекта передовой практики мобильного терминала с открытым исходным кодом.Следующее это проект Заметка о многоуровневой архитектуре. Для получения дополнительной информации, пожалуйста, проверьте исходный код:GitHub.com/MCU король/шаблоны…

Введение в приложение

Во-первых, давайте представим приложение этого проекта, которое представляет собой простое и интерактивное приложение Todo, Приложение называется Memo, сокращение от Memory, ссылаясь на приложения Microsoft To Do, Listify, Trello и другие. Однако самое большое отличие заключается в том, что проект не полагается на бэкэнд, а использует для хранения данных indexDB, предоставляемый браузером, что может обеспечить абсолютную безопасность данных. Кроме того, обновление приложения не приведет к удалению исходных данных, если приложение не будет удалено. Схема эффекта выглядит следующим образом:

платформа опыта QR код Связь
Web нажмите опыт
Android нажмите опыт

Слои архитектуры

В настоящее время фронтенд-разработка в основном базируется на одностраничных приложениях, и когда бизнес-логика приложения достаточно сложна, оно всегда будет сталкиваться с такими проблемами, как:

  • Бизнес-логика слишком сконцентрирована на уровне представления, что делает невозможным совместное использование бизнес-логики несколькими платформами, которая должна быть независимой от платформы.Например, продукт должен поддерживать как мобильную, так и ПК-сторону, или один и тот же продукт имеет обе стороны. Web и React Native заканчиваются;

  • Когда продукт требует совместной работы нескольких человек, стиль кода каждого человека и понимание бизнеса различаются, что приводит к неорганизованному распределению бизнес-логики;

  • Понимание продукта остается на постраничном уровне, что приводит к большому расхождению между реализованной технической моделью и фактической бизнес-моделью.Когда потребности бизнеса меняются, техническая модель легко разрушается;

  • Слишком большая зависимость от фреймворка переднего плана приводит к необходимости переписывать всю бизнес-логику и проводить регрессионное тестирование при рефакторинге для переключения фреймворков.

В ответ на проблемы, с которыми столкнулись выше, автор получил некоторые знания о DDD (Domain Driven Design), чистой архитектуре и т. д., а также собрал практические данные о внешнем интерфейсе подобных идей, сформировав следующую многоуровневую архитектуру внешнего интерфейса:

Слой View должен быть всем знаком, поэтому я не буду его здесь представлять, а сосредоточусь на значении следующих трех слоев:

Слой сервисов

Уровень служб используется для работы базовой технологии, такой как инкапсуляция запросов AJAX, работа с файлами cookie браузера, locaStorage, indexDB, работа с собственными возможностями (такими как вызов камер и т. д.) и создание веб-сокетов для взаимодействия с серверной частью.

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

Получить данные о котировках из бэкенда:

export class CommonService implements ICommonService {
  @m({ maxAge: 60 * 1000 })
  public async getQuoteList(): Promise<IQuote[]> {
    const {
      data: { list }
    } = await http({
      method: 'post',
      url: '/quote/getList',
      data: {}
    });

    return list;
  }
}

Синхронизируйте данные Note с календарем клиента:

export class NativeService implements INativeService {
  // 同步到日历
  @p()
  public syncCalendar(params: SyncCalendarParams, onSuccess: () => void): void {
    const cb = async (errCode: number) => {
      const msg = NATIVE_ERROR_CODE_MAP[errCode];

      Vue.prototype.$toast(msg);

      if (errCode !== 6000) {
        this.errorReport(msg, 'syncCalendar', params);
      } else {
        await onSuccess();
      }
    };

    dsbridge.call('syncCalendar', params, cb);
  }
  ...
}

Прочитайте подробные данные Note из indexDB:

import { noteTranslator } from './translators';

export class NoteService implements INoteService {
  public async get(id: number): Promise<INotebook | undefined> {
    const db = await createDB();

    const notebook = await db.getFromIndex('notebooks', 'id', id);
    return noteTranslator(notebook!);
  }
}

Среди них noteTranslator относится к уровню транслятора и используется для исправления данных заметки, возвращаемых интерфейсом, который определяется следующим образом:

export function noteTranslator(item: INotebook) {
  // item.themeColor = item.color;
  return item;
}

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

Слой сущностей

Сущность Сущность является основной концепцией предметно-ориентированного проектирования, она является носителем доменных услуг, она определяет свойства и методы человека в бизнесе. Например, Note и Notebook являются сущностями в этом проекте. Различение того, является ли объект сущностью, в основном зависит от того, имеет ли он уникальный идентификатор (например, id). Ниже приведено примечание об объекте для этого проекта:

export default class Note {
  public id: number;
  public name: string;
  public deadline: Date | undefined;
  ...

  constructor(note: INote) {
    this.id = note.id;
    this.name = note.name;
    this.deadline = note.deadline;
    ...
  }

  public get isExpire() {
    if (this.deadline) {
      return this.deadline.getTime() < new Date().getTime();
    }
  }

  public get deadlineStr() {
    if (this.deadline) {
      return formatTime(this.deadline);
    }
  }
}

Как видно из приведенного выше кода, здесь в основном используются атрибуты и производные атрибуты самой сущности.Конечно, у самой сущности могут быть и методы для реализации бизнес-логики, принадлежащие самой сущности (думаю, бизнес-логика может разделить на две части: часть бизнес-логики тесно связана с сущностью и должна быть реализована методами в классе сущности, другая часть бизнес-логики больше связана с бизнесом между сущностями, которые могут быть реализованы на уровне Interactors. ). Просто этот проект еще не освещал ее, поэтому я не буду делать здесь больше пояснений.Если вам интересно, вы можете обратиться к статьям, переведенным автором, перечисленным ниже:Масштабируемые внешние интерфейсы № 2. Общие шаблоны (перевод).

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

Слой интеракторов

Уровень Interactors — это уровень, отвечающий за обработку бизнес-логики, в основном состоящий из бизнес-прецедентов. В общем, Interactor — это синглтон, который позволяет нам хранить некоторое состояние и избегать ненужных HTTP-вызовов, предоставляет способ сброса свойств состояния приложения (например, восстановление данных при потере записей об изменениях), решает, когда следует загружать новые данные.

Ниже приведены общие сервисы вызовов, предоставляемые уровнем Common Interactors в этом проекте:

class CommonInteractor {
  public static getInstance() {
    return this._instance;
  }

  private static _instance = new CommonInteractor(new CommonService());

  private _quotes: any;

  constructor(private _service: ICommonService) {}

  public async getQuoteList() {
    // 单例模式下,将一些基本固定不变的接口数据保存在内存中,避免重复调用
    // 但要注意避免内存泄露
    if (this._quotes !== undefined) {
      return this._quotes;
    }

    let response;

    try {
      response = await this._service.getQuoteList();
    } catch (error) {
      throw error;
    }

    this._quotes = response;
    return this._quotes;
  }
}

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

Кроме того, уровень Interactors также может получать классы сущностей, предоставляемые уровнем Entities, и объединять бизнес-логику, тесно связанную с сущностями, предоставляемыми классами сущностей, и бизнес-логику уровня Interactors со слоем View. код слоя Interactors в Note выглядит следующим образом:

class NoteInteractor {
  public static getInstance() {
    return this._instance;
  }

  private static _instance = new NoteInteractor(
    new NoteService(),
    new NativeService()
  );

  constructor(
    private _service: INoteService,
    private _service2: INativeService
  ) {}

  public async getNote(notebookId: number, id: number) {
    try {
      const note = await this._service.get(notebookId, id);
      if (note) {
        return new Note(note);
      }
    } catch (error) {
      throw error;
    }
  }
}

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

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

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

Наконец, краткое изложение в статье «Дизайн, ориентированное на предметную область, переднюю часть» о технологии обработки данных Ant Financial цитируется как конец:

Следует понимать, что целью разделения доменного слоя является не повторное использование страницы, которая должна быть преобразована в мыслях. Уровень предметной области не абстрагируется, поскольку он повторно используется в нескольких местах. Причина, по которой его вытащили:

  • Уровень домена стабилен (страница и модули, привязанные к странице, нестабильны)
  • Уровень домена отделен (страница будет связана, и данные страницы будут поступать с нескольких интерфейсов и нескольких доменов).
  • Слой предметной области имеет чрезвычайно высокую сложность и заслуживает отдельного управления (уровень представления обрабатывает отрисовку страницы и управление логикой страницы, сложность достаточно высока, а разделение слоя предметной области может облегчить слой представления. Слой представления столь же легковесен насколько это возможно, это основные идеи нашего архитектора cnfi)
  • Уровень предметной области можно повторно использовать послойно (ваш код может отказаться от определенной технической системы, переключиться с vue на реакцию или может запустить мобильную версию, в этих случаях весь слой предметной области можно повторно использовать напрямую )
  • За непрерывную эволюцию модели предметной области (цель модели — позволить людям сосредоточиться, преимущество сосредоточения — улучшить понимание бизнеса интерфейсной командой, а процесс размышлений о бизнесе может заставить бизнес двигаться вперед)

Рекомендуется несколько связанных библиотек:

react-clean-architecture

business-rules-package

ddd-fe-demo

Рекомендуется несколько статей по теме:

Front-End Architecture — Делаем рефакторинг менее болезненным (перевод)

Масштабируемый интерфейс № 1 — Основы архитектуры

Масштабируемые внешние интерфейсы № 2. Общие шаблоны (перевод)

Практика доменно-ориентированного проектирования в развитии интернет-бизнеса

Фронтенд-разработка — дизайн, управляемый доменом

Применение дизайна, управляемого доменом, во внешнем интерфейсе

PS:

Ближайшие планы проекта Mobile Web Best Practices:

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

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