پرش به مطلب اصلی

وقتی «برنامه‌نویس» هنوز شغل نبود

· ۱۰ دقیقه مطالعه
مهدی مالوردی
مهندس نرم‌افزار و نویسندهٔ این سایت

در فرم ازدواج، یک جای خالی بود: شغل.

ادسخر دایجسترا نوشت: برنامه‌نویس.

کارمندان آمستردام نپذیرفتند. از نگاه آنان، چنین شغلی وجود نداشت. نه اینکه دایجسترا کاری نمی‌کرد؛ مسئله این بود که جهان هنوز برای کاری که او انجام می‌داد، نام و جایگاه روشنی نداشت.

در سند رسمی ازدواجش، به جای «برنامه‌نویس» نوشتند: فیزیک‌دان نظری.

همین صحنه برای آغاز قصه کافی است: مردی که بعدها یکی از چهره‌های بزرگ علوم رایانه شد، در روز ازدواجش حتی نتوانست شغل واقعی خود را روی کاغذ رسمی ثبت کند.

تصویر مفهومی درباره‌ی دایجسترا و روزگاری که برنامه‌نویسی هنوز شغل رسمی نبود

قابلیت اداره‌پذیری سیستم‌ها چیست و چرا فقط با مانیتورینگ به دست نمی‌آید؟

· ۷ دقیقه مطالعه
مهدی مالوردی
مهندس نرم‌افزار و نویسندهٔ این سایت

فرض کن نسخه‌ی تازه‌ی سرویس سفارش را مستقر کرده‌ایم. چند دقیقه‌ی اول، همه‌چیز عادی به نظر می‌رسد: نمودارها وضعیت بدی نشان نمی‌دهند، نرخ خطا بالا نرفته، و از بیرون انگار سامانه دارد کار می‌کند. کمی بعد، پشتیبانی خبر می‌دهد که چند پرداخت موفق بوده، اما سفارش در پنل کاربر دیده نمی‌شود. لاگ‌ها را باز می‌کنیم، متریک‌ها را می‌بینیم، تریس‌ها را دنبال می‌کنیم؛ داده کم نیست، اما جواب روشن پیدا نمی‌شود. معلوم نیست مشکل از پرداخت است، صف پیام، موجودی، نسخه‌ی تازه، یا داده‌ای که در میانه‌ی مسیر نیمه‌کاره مانده است.

من معمولاً همین‌جا تفاوت میان «کار کردن» و «قابل اداره بودن» را می‌بینم. سامانه ممکن است تا حدی زنده باشد، درخواست بگیرد و حتی بیشتر مسیرها را درست پاسخ بدهد، اما در لحظه‌ی تغییر یا اختلال، برای تیم قابل فهم نباشد. قابلیت اداره‌پذیری، یا operability، درباره‌ی همین لحظه‌هاست: لحظه‌هایی که باید بفهمیم چه رخ داده، آسیب را محدود کنیم، تصمیمی امن بگیریم، و اگر لازم شد از مسیر اشتباه برگردیم.

تصویر مفهومی درباره‌ی قابلیت اداره‌پذیری سامانه‌ها

کد مهم‌تر نباید به کد کم‌اهمیت‌تر وابسته باشد

· ۱۰ دقیقه مطالعه
مهدی مالوردی
مهندس نرم‌افزار و نویسندهٔ این سایت

هسته‌ی روشن سیستم در مرکز که ابزارهایی مانند دیتابیس و وب در پیرامون آن قرار دارند و جهت وابستگی از ابزارها به سمت هسته است.

فرض کنید در یک سامانه‌ی مالی، مهم‌ترین قانون کسب‌وکار این است که فقط کاربرانی که موجودی کافی دارند بتوانند درخواست برداشت ثبت کنند. این قانون، از جنس سیاست اصلی سیستم است؛ یعنی همان چیزی که اگر روزی دیتابیس، فریم‌ورک، یا روش ارتباط با سرویس‌های بیرونی عوض شود، هنوز باید معتبر بماند.

اما در عمل، خیلی وقت‌ها این قانون را مستقیم به جزئیات فنی می‌دوزیم. چیزی شبیه این:

import {PrismaClient} from '@prisma/client'
import axios from 'axios'

const prisma = new PrismaClient()

class WithdrawService {
async withdraw(userId: string, amount: number) {
const user = await prisma.user.findUnique({
where: {id: userId},
})

if (!user) {
throw new Error('User not found')
}

if (user.balance < amount) {
throw new Error('Insufficient balance')
}

await axios.post('https://wallet-service.example.com/withdraw', {
userId,
amount,
})

await prisma.withdrawRequest.create({
data: {
userId,
amount,
status: 'submitted',
},
})
}
}

در این کد، منطق اصلی برداشت مستقیماً هم به ORM وابسته است، هم به یک کارخواه وب. یعنی مهم‌ترین بخش تصمیم‌گیری سیستم، به چیزهایی وصل شده که در اصل فقط ابزار اجرا هستند.

مسئله فقط این نیست که کد کمی شلوغ شده است. مسئله این است که جهت وابستگی برعکس چیزی است که طراحی خوب می‌خواهد. این‌جا سیاست سطح بالا به جزئیات سطح پایین آویزان شده است.

اینترفیس بزرگ، کلاس کوچک را هم آلوده می‌کند

· ۱۱ دقیقه مطالعه
مهدی مالوردی
مهندس نرم‌افزار و نویسندهٔ این سایت

یک پریز بزرگ با کابل‌های زیاد که یک دستگاه کوچک فقط به یکی از آن‌ها نیاز دارد.

فرض کنید در یک سامانه‌ی مالی، یک ریپازیتوری بزرگ برای کار با تراکنش‌ها داریم. این ریپازیتوری همه‌چیز را با هم دارد: گرفتن تراکنش، ساخت گزارش، بستن حساب، بازسازی داده، خروجی گرفتن، و چند کار دیگر. روی کاغذ، شاید این طراحی «مرکزی» و «یک‌پارچه» به نظر برسد. اما کافی است یک مصرف‌کننده‌ی کوچک وارد ماجرا شود تا مشکل خودش را نشان دهد.

برای نمونه، یک سرویس داریم که فقط می‌خواهد یک تراکنش را بر اساس شناسه پیدا کند:

interface TransactionRepository {
findById(id: string): Promise<Transaction | null>
save(transaction: Transaction): Promise<void>
delete(id: string): Promise<void>
exportDailyReport(date: string): Promise<ReportFile>
rebuildBalances(): Promise<void>
closeMonth(month: string): Promise<void>
archiveOldTransactions(before: string): Promise<number>
}

و مصرف‌کننده‌ی ما فقط همین را لازم دارد:

class TransactionDetailsService {
constructor(
private readonly repository: TransactionRepository,
) {}

async getDetails(id: string) {
return this.repository.findById(id)
}
}

در نگاه نخست، شاید کسی بگوید: «خب چه اشکالی دارد؟ این سرویس که فقط از findById استفاده می‌کند.» اما مسئله دقیقاً همین‌جاست. این سرویس، حتی اگر فقط به یک متد نیاز داشته باشد، به اینترفیس بزرگی وابسته شده که پر از چیزهای نامربوط است. یعنی یک کلاس کوچک، ناخواسته بار یک قرارداد بزرگ را روی دوش می‌کشد.

هر چیزی که شبیه چیز دیگر است، جایگزین آن نیست

· ۱۰ دقیقه مطالعه
مهدی مالوردی
مهندس نرم‌افزار و نویسندهٔ این سایت

چند قطعه‌ی شبیه به هم که فقط یکی از آن‌ها واقعاً در جای قطعه‌ی اصلی می‌نشیند.

فرض کنید در یک سامانه‌ی پرداخت، کلاسی داریم که قرار است رسید پرداخت را تولید و ذخیره کند. برای همین، یک قرارداد ساده تعریف کرده‌ایم: هر «ذخیره‌ساز رسید» باید بتواند رسید را بگیرد و بدون غافل‌گیری آن را ذخیره کند.

interface ReceiptStore {
save(receipt: Receipt): void
}

حالا یک پیاده‌سازی معمولی داریم:

class DatabaseReceiptStore implements ReceiptStore {
save(receipt: Receipt) {
db.receipts.insert(receipt)
}
}

تا این‌جا همه‌چیز روشن است. اما بعدتر کسی می‌گوید: «ما یک نسخه‌ی فقط‌خواندنی هم لازم داریم. همان را هم از همین اینترفیس ارث ببریم.» و نتیجه چیزی شبیه این می‌شود:

class ReadOnlyReceiptStore implements ReceiptStore {
save(receipt: Receipt) {
throw new Error('This store is read-only')
}
}

از نظر نام و ساختار، این کلاس شبیه یک ReceiptStore است. همان اینترفیس را پیاده‌سازی کرده، همان متد را دارد، و حتی شاید از نظر ابزارهای ایستا هیچ خطایی هم نداشته باشد. اما از نظر رفتاری، قرارداد را شکسته است. مصرف‌کننده‌ای که به ReceiptStore اعتماد کرده بود، انتظار داشت save رسید را ذخیره کند، نه اینکه در زمان اجرا غافلگیر شود.

اینجا مسئله فقط یک استثنا یا یک خطای کوچک نیست. مسئله این است که ما چیزی ساخته‌ایم که شبیه نوع اصلی است، اما جایگزین آن نیست.

چرا یک ریفکتور سالم می‌تواند تست‌ها را بشکند؟

· ۴ دقیقه مطالعه
مهدی مالوردی
مهندس نرم‌افزار و نویسندهٔ این سایت

چند وقت پیش با وضعیتی روبه‌رو شدم که در نگاه اول عجیب به نظر می‌رسید: کد را تمیزتر کردم، رفتار سیستم را عوض نکردم، اما تست‌ها شکست خوردند. همان‌جا روشن شد که مشکل از خودِ ریفکتور نبود؛ مسئله از تست‌هایی می‌آمد که به‌جای سنجش رفتار، به جزئیات تعامل‌ها وابسته شده بودند.

مقایسهٔ ماک و فیک در آزمون‌نویسی