Каждый компонент в Mithril должен отвечать за одну конкретную задачу. В традиционных UI-фреймворках часто возникает соблазн объединять логику взаимодействия с сервером, управление состоянием и отрисовку в одном компоненте. В Mithril это приводит к трудностям при масштабировании и тестировании.
Пример соблюдения SRP в Mithril:
// Компонент только для отображения списка пользователей
const UserListView = {
view: ({ attrs }) => m("ul", attrs.users.map(user => m("li", user.name)))
};
// Компонент для загрузки данных
const UserListModel = {
users: [],
load: () => m.request({ method: "GET", url: "/api/users" }).then(data => UserListModel.users = data)
};
// Компонент связывает модель и представление
const UserList = {
oninit: UserListModel.load,
view: () => m(UserListView, { users: UserListModel.users })
};
Разделение ответственности делает код более предсказуемым и легко тестируемым.
Компоненты должны быть открыты для расширения, но закрыты для
модификации. В Mithril это достигается через использование
атрибутов (attrs) и композиции компонентов вместо изменения
их внутреннего кода.
Пример расширения компонента:
const Button = {
view: ({ attrs }) => m("button", { class: attrs.class || "default" }, attrs.label)
};
// Создание новой версии кнопки без изменения исходного кода
const DangerButton = {
view: ({ attrs }) => m(Button, { ...attrs, class: "danger" })
};
Использование композиции и передачи атрибутов позволяет изменять поведение компонента без вмешательства в его внутреннюю реализацию.
Любой производный компонент должен корректно заменять базовый без нарушения функциональности. В Mithril это означает, что компонент, который наследует структуру и интерфейс другого, должен корректно работать в любом месте, где используется базовый.
Пример соблюдения LSP:
const InputField = {
view: ({ attrs }) => m("input", { type: "text", value: attrs.value, oninput: e => attrs.oninput(e.target.value) })
};
const PasswordField = {
view: ({ attrs }) => m(InputField, { ...attrs, type: "password" })
};
PasswordField можно использовать вместо
InputField в любых формах без изменения их логики.
Компоненты не должны зависеть от методов, которые они не используют.
В Mithril это проявляется в минимизации атрибутов и методов,
передаваемых через attrs и state. Избыточный
интерфейс приводит к сложной и запутанной логике.
Пример:
// Плохой подход — один интерфейс с множеством ненужных методов
const Form = {
view: ({ attrs }) => m("form", [
m("input", { oninput: attrs.onChangeName }),
m("input", { oninput: attrs.onChangeEmail }),
m("button", { onclick: attrs.onSubmit })
])
};
// Разделение интерфейса на специализированные компоненты
const NameInput = {
view: ({ attrs }) => m("input", { oninput: attrs.onChange })
};
const EmailInput = {
view: ({ attrs }) => m("input", { oninput: attrs.onChange })
};
const SubmitButton = {
view: ({ attrs }) => m("button", { onclick: attrs.onClick }, "Submit")
};
Каждый компонент получает только необходимые методы, интерфейс становится чистым и понятным.
Компоненты должны зависеть от абстракций, а не от конкретных
реализаций. В Mithril это реализуется через передачу функций и сервисов
через attrs или отдельные модули, а не жесткую привязку к
конкретной реализации API.
Пример:
// Абстракция для получения данных
const DataService = {
fetchUsers: () => m.request({ method: "GET", url: "/api/users" })
};
// Компонент зависит от абстракции, а не конкретной реализации
const UserList = {
oninit: ({ attrs }) => attrs.service.fetchUsers().then(data => attrs.users = data),
view: ({ attrs }) => m("ul", (attrs.users || []).map(u => m("li", u.name)))
};
// Использование
m.mount(document.body, {
view: () => m(UserList, { service: DataService })
});
Такой подход делает код гибким и легко тестируемым: можно заменить
DataService на мок для юнит-тестов или изменить источник
данных без изменения компонента.
Применение принципов SOLID в фронтенд-разработке на Mithril повышает читаемость, масштабируемость и тестируемость кода, превращая простой фреймворк в инструмент для построения сложных, поддерживаемых приложений.