Приветствуем, Хабр.

Мы стараемся лицензировать в издательстве «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)


  1. EgorovDenis
    05.07.2026 05:01

    Вот это я понимаю кладезь полезных знаний для бэкендера в этой книге.

    Жаль, что эту книгу обделили вниманием и мало кто о ней рассказывает


  1. mike_pol
    05.07.2026 05:01

    А промокод?


  1. aquaksy
    05.07.2026 05:01

    Хорошая статья


  1. An_Artur
    05.07.2026 05:01

    Admin_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 True
    
    login
    
    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(
                    "Ошибка",
                    "Вы ввели неверный логин или пароль. Пожалуйста проверьте ещё раз введенные данные",
                )