Приветствуем, Хабр.
Мы стараемся лицензировать в издательстве «O’Reilly» и переводить для вас фундаментальные книги, в том числе, по формирующимся парадигмам и подходам к разработке. Одним из наиболее успешных базовых изданий такого рода была книга «Основы инженерии данных» Джо Риса и Мэта Хоусли, вышедшая в середине 2024 года (допечатка – январь 2026). Желая развить эту тему, мы издали и вторую, возможно, не менее фундаментальную книгу «Data Engineering Design Patterns», которую написал для издательства «O’Reilly» Бартош Конечны. Русское издание вышло в конце июня и называется «Инженерия данных. Паттерны проектирования». Автор считает, что в дисциплине инженерии данных можно выделить такие же многоразовые и универсальные шаблоны проектирования типичных решений, как и в традиционном программировании. То есть, он берётся повторить в этой области фундаментальный труд «Банды четырёх», который в середине 1990-х привёл к появлению всем известной книги «Design Patterns». Ранее мы размещали статью с примерами вычленения паттернов проектирования в области инженерии данных — это был перевод, а оригинал статьи написал сам Бартош Конечны, собираясь обосновать готовящуюся книгу и очертить её тематическое поле. Под катом рассмотрим, какие именно темы вошли в новую книгу.
Полагаем, что эта книга должна стать для вас не менее чем второй, а не первой на тему инженерии данных — то есть, рекомендуем начать изучение этой отрасли с книги «Основы инженерии данных». Однако структурно книга Бартоша Конечны более напоминает книгу Гаммы, Хелма, Джонса и Влиссидеса; представьте себе «Data Engineering Design Patterns» на месте «Software Engineering Design Patterns». В заключительной части книги делается обзор всех рассмотренных в книге паттернов. Вот они:

Такое авторское резюме исчерпывающим образом иллюстрирует, какие качества данных и этапы их обработки наиболее важны а рамках практической Data Engineering. Читатель может отметить, что перечисленные темы частично перекликаются с дисциплиной «Site Reliability Engineering» (на эту тему у нас также выйдет новинка, сейчас готовим обложку). В основном же разобранные в книге паттерны помогут вам проектировать программы и сервисы, сразу рассчитанные на обработку и классификацию разнородных данных, а также на увеличение пропускной способности приложения.
Названия для всех паттернов проектирования автор придумал сам, и мы приложили максимум усилий, чтобы адекватно перевести их, не перебарщивая с метафоричностью. Русское название каждого паттерна сопровождается оригинальной англоязычной формулировкой.
Под спойлером – полное оглавление книги
Оглавление





Обязательно заходите под спойлер – ведь, как знаете, порой ничего не бывает выразительнее списка. Но со своей стороны подчеркнём, что оглавление иллюстрирует, прежде всего, универсальность изданной нами книги. Рекомендуем читать книгу сначала и до конца, однако, если после знакомства с первой главой вы сочтёте книгу чрезмерно поверхностной – без промедления переходите к предметным главам в центральной части книги. Наиболее универсальна и самодостаточна глава 4, в которой описано, как обеспечить идемпотентность данных. Главы 5 («Паттерны проектирования ценности данных») и 6 («Паттерны проектирования потоков данных») будут наиболее интересны специалистам по распределённым системам. Главы 9 («Паттерны проектирования хранения данных») и 10 («Паттерны проектирования наблюдаемости данных») будут интересны специалистам по обеспечению надёжности и поддержке систем. Особое место в книге занимает глава 3, посвящённая обработке ошибок. Здесь затронуты такие важные темы, как обращение с недоставленными или запоздалыми данными, работа с контрольными точками, устранение дубликатов и многое другое.
Книга богато иллюстрирована блок-схемами, которые мы перевели в полном соответствии с терминологией, вводимой в тексте книги. Например, вот рисунок 3.3, иллюстрирующий устранение дубликатов в окне.

А вот рисунок, на котором представлена операция TRUNCATE TABLE (описывается в главе 4 в разделе о паттерне «Быстрая очистка метаданных»).

Большинство разделов в книге построены по принципу «Проблема-решение-описание». Весь код к главам выложен автором на Github: https://github.com/bartosz25/data-engineering-design-patterns-book. Для чтения кода необходимо знать Python и SQL, желательно умение читать Scala и знакомство с фреймворком Spark.
Заключение
Конечно же, это не последняя наша книга, посвящённая теме Data Engineering. У нас заключён договор на русскоязычную книгу, автор которой собирается представить тему Data Engineering в контексте бизнес-аналитики и анализа данных, то есть, раскрыть «пользовательский» взгляд на эту сложную тему. Но книга Бартоша Конечны кажется нам особенно своевременной и долговечной, так как впервые обобщает best practices, охватывающие инженерию данных во всей полноте. Надеемся, что именно эта книга станет материалом для комментариев, толкований, дискуссий и спиноффов – наверняка автор учёл не все практические случаи инженерии данных, сводимые к узнаваемым и стабильно воспроизводимым шаблонам. Плодотворного вам чтения и развития в области инженерии данных.
Комментарии (4)

An_Artur
05.07.2026 05:01Admin_forms import tkinter as tk from tkinter import messagebox, ttk import database class AdminWindow(tk.Toplevel): def __init__(self, master, user): super().__init__(master) self.title('Рабочий стол администратора - ООО "Кофейня Темная материя"') self.geometry("760x500") self.minsize(720, 460) self.selected_user_id = None self.create_widgets(user) self.load_users() def create_widgets(self, user): header = tk.Label(self, text=f"Администратор: {user['login']}", font=("Arial", 14, "bold")) header.pack(pady=10) self.tree = ttk.Treeview( self, columns=("id", "login", "role", "blocked", "attempts"), show="headings", ) self.tree.heading("id", text="ID") self.tree.heading("login", text="Логин") self.tree.heading("role", text="Роль") self.tree.heading("blocked", text="Блокировка") self.tree.heading("attempts", text="Ошибки") self.tree.column("id", width=50) self.tree.column("login", width=180) self.tree.column("role", width=150) self.tree.column("blocked", width=120) self.tree.column("attempts", width=80) self.tree.pack(fill="both", expand=True, padx=10) self.tree.bind("<<TreeviewSelect>>", self.select_user) form = tk.Frame(self) form.pack(fill="x", padx=10, pady=10) tk.Label(form, text="Логин").grid(row=0, column=0, sticky="w") self.login_entry = tk.Entry(form, width=25) self.login_entry.grid(row=1, column=0, padx=5, sticky="w") tk.Label(form, text="Пароль").grid(row=0, column=1, sticky="w") self.password_entry = tk.Entry(form, width=25, show="*") self.password_entry.grid(row=1, column=1, padx=5, sticky="w") tk.Label(form, text="Роль").grid(row=0, column=2, sticky="w") self.role_box = ttk.Combobox(form, values=["Администратор", "Пользователь"], state="readonly", width=20) self.role_box.current(1) self.role_box.grid(row=1, column=2, padx=5, sticky="w") buttons = tk.Frame(self) buttons.pack(fill="x", padx=10, pady=5) tk.Button(buttons, text="Добавить", command=self.add_user).pack(side="left", padx=5) tk.Button(buttons, text="Изменить", command=self.edit_user).pack(side="left", padx=5) tk.Button(buttons, text="Снять блокировку", command=self.unblock_user).pack(side="left", padx=5) tk.Button(buttons, text="Обновить", command=self.load_users).pack(side="left", padx=5) def load_users(self): for item in self.tree.get_children(): self.tree.delete(item) for user in database.list_users(): blocked = "Да" if user["is_blocked"] else "Нет" self.tree.insert( "", "end", values=(user["user_id"], user["login"], user["role"], blocked, user["failed_attempts"]), ) def select_user(self, event): selected = self.tree.selection() if not selected: return values = self.tree.item(selected[0], "values") self.selected_user_id = int(values[0]) self.login_entry.delete(0, tk.END) self.login_entry.insert(0, values[1]) self.password_entry.delete(0, tk.END) self.role_box.set(values[2]) def add_user(self): login = self.login_entry.get().strip() password = self.password_entry.get().strip() role = self.role_box.get() if not login or not password: messagebox.showwarning("Проверка", "Заполните логин и пароль") return ok, text = database.create_user(login, password, role) if ok: messagebox.showinfo("Информация", text) self.load_users() else: messagebox.showerror("Ошибка", text) def edit_user(self): if self.selected_user_id is None: messagebox.showwarning("Проверка", "Выберите пользователя из таблицы") return login = self.login_entry.get().strip() password = self.password_entry.get().strip() role = self.role_box.get() if not login: messagebox.showwarning("Проверка", "Логин не может быть пустым") return ok, text = database.update_user(self.selected_user_id, login, role, password if password else None) if ok: messagebox.showinfo("Информация", text) self.load_users() else: messagebox.showerror("Ошибка", text) def unblock_user(self): if self.selected_user_id is None: messagebox.showwarning("Проверка", "Выберите пользователя из таблицы") return database.unblock_user(self.selected_user_id) messagebox.showinfo("Информация", "Блокировка снята") self.load_users()captcha import random import tkinter as tk class PuzzleCaptcha(tk.Frame): def __init__(self, master): super().__init__(master) self.canvas = tk.Canvas(self, width=420, height=150, bg="white", highlightthickness=1) self.canvas.pack() self.pieces = [] self.drag_item = None self.drag_start = (0, 0) self.build_puzzle() def build_puzzle(self): self.canvas.delete("all") self.pieces.clear() target_x = [20, 110, 200] labels = ["1", "2", "3"] for index, x in enumerate(target_x): self.canvas.create_rectangle(x, 35, x + 80, 115, outline="gray", dash=(3, 2)) self.canvas.create_text(x + 40, 125, text=f"место {index + 1}", fill="gray") start_positions = [(310, 20), (310, 65), (310, 110)] random.shuffle(labels) for label, (x, y) in zip(labels, start_positions): rect = self.canvas.create_rectangle(x, y, x + 70, y + 35, fill="#e6e6e6", outline="black", tags=("piece", label)) text = self.canvas.create_text(x + 35, y + 18, text=label, font=("Arial", 12, "bold"), tags=("piece", label)) self.pieces.append((label, rect, text)) self.canvas.tag_bind("piece", "<ButtonPress-1>", self.start_drag) self.canvas.tag_bind("piece", "<B1-Motion>", self.drag) self.canvas.tag_bind("piece", "<ButtonRelease-1>", self.stop_drag) def start_drag(self, event): item = self.canvas.find_closest(event.x, event.y)[0] tags = self.canvas.gettags(item) if "piece" not in tags: return label = tags[1] self.drag_item = label self.drag_start = (event.x, event.y) def drag(self, event): if self.drag_item is None: return dx = event.x - self.drag_start[0] dy = event.y - self.drag_start[1] for label, rect, text in self.pieces: if label == self.drag_item: self.canvas.move(rect, dx, dy) self.canvas.move(text, dx, dy) self.drag_start = (event.x, event.y) def stop_drag(self, event): self.drag_item = None def is_correct(self): correct_places = { "1": (20, 35, 100, 115), "2": (110, 35, 190, 115), "3": (200, 35, 280, 115), } for label, rect, _ in self.pieces: x1, y1, x2, y2 = self.canvas.coords(rect) center_x = (x1 + x2) / 2 center_y = (y1 + y2) / 2 tx1, ty1, tx2, ty2 = correct_places[label] if not (tx1 <= center_x <= tx2 and ty1 <= center_y <= ty2): return False return Truelogin import tkinter as tk from tkinter import messagebox import database from forms.admin_form import AdminWindow from forms.captcha import PuzzleCaptcha from forms.user_form import UserWindow class LoginWindow(tk.Tk): def __init__(self): super().__init__() self.title('Авторизация - ООО "Кофейня Темная материя"') self.geometry("500x430") self.minsize(480, 420) self.create_widgets() def create_widgets(self): title = tk.Label(self, text="Авторизация пользователя", font=("Arial", 16, "bold")) title.pack(pady=15) form = tk.Frame(self) form.pack(pady=5) tk.Label(form, text="Логин").grid(row=0, column=0, sticky="w", padx=5, pady=3) self.login_entry = tk.Entry(form, width=30) self.login_entry.grid(row=1, column=0, padx=5, pady=3) tk.Label(form, text="Пароль").grid(row=2, column=0, sticky="w", padx=5, pady=3) self.password_entry = tk.Entry(form, width=30, show="*") self.password_entry.grid(row=3, column=0, padx=5, pady=3) tk.Label(self, text="Соберите пазл: 1, 2, 3 слева направо").pack(pady=(15, 5)) self.captcha = PuzzleCaptcha(self) self.captcha.pack() buttons = tk.Frame(self) buttons.pack(pady=10) tk.Button(buttons, text="Войти", width=14, command=self.login).pack(side="left", padx=5) tk.Button(buttons, text="Обновить капчу", width=14, command=self.captcha.build_puzzle).pack(side="left", padx=5) def login(self): login = self.login_entry.get().strip() password = self.password_entry.get().strip() if not login or not password: messagebox.showwarning("Ошибка", "Заполните логин и пароль") return user = database.find_user(login) if user is not None and user["is_blocked"] == 1: messagebox.showerror("Ошибка", "Вы заблокированы. Обратитесь к администратору") return if not self.captcha.is_correct(): database.add_failed_attempt(login) blocked_user = database.find_user(login) if blocked_user is not None and blocked_user["is_blocked"] == 1: messagebox.showerror("Ошибка", "Вы заблокированы. Обратитесь к администратору") else: messagebox.showerror("Ошибка", "Капча собрана неверно") self.captcha.build_puzzle() return status, checked_user = database.check_user(login, password) if status == "ok": messagebox.showinfo("Информация", "Вы успешно авторизовались") if checked_user["role"] == "Администратор": AdminWindow(self, checked_user) else: UserWindow(self, checked_user) elif status in ("blocked", "blocked_now"): messagebox.showerror("Ошибка", "Вы заблокированы. Обратитесь к администратору") else: messagebox.showerror( "Ошибка", "Вы ввели неверный логин или пароль. Пожалуйста проверьте ещё раз введенные данные", )
EgorovDenis
Вот это я понимаю кладезь полезных знаний для бэкендера в этой книге.
Жаль, что эту книгу обделили вниманием и мало кто о ней рассказывает