Open/close & Liskov در کامپوننت Button
کی در پروژه واقعی نیاز داریم PrimaryButton رو به جای Button قرار بدیم؟
پاسخ: هر وقت یک Design System داری که میخوای کنترل مرکزی داشته باشی، اما انعطاف هم داشته باشی.
سناریوی واقعی: شما در یک شرکت بزرگ هستید (مثل دیجیکالا، اسنپ، یا یک استارتاپ با ۵۰+ صفحه)
مشکل بدون LSP:
فرض کن در ۱۰۰ صفحه از سایت، دکمهی اصلی این شکلی استفاده شده:
jsx
<button className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
ثبت سفارش
</button>
حالا طراح UI میگه:
“از این به بعد همه دکمههای اصلی باید shadow-md و transition-all هم داشته باشن.”
اگر SOLID رعایت نشده باشه، باید ۱۰۰ فایل رو دستی ویرایش کنی!
راهحل با Liskov Substitution + Composition
۱. یک Button پایه میسازی (والد)
jsx
// components/ui/Button.js
export default function Button({ children, className = '', ...props }) {
return (
<button
className={`px-4 py-2 rounded font-medium transition-all ${className}`}
{...props}
>
{children}
</button>
);
}
۲. یک PrimaryButton میسازی (فرزند)
jsx
// components/ui/PrimaryButton.js
import Button from './Button';
export default function PrimaryButton({ children, ...props }) {
return (
<Button
className="bg-blue-600 text-white hover:bg-blue-700 shadow-md"
{...props}
>
{children}
</Button>
);
}
حالا کجا از Liskov Substitution استفاده میشه؟
سناریو ۱: کامپوننت فرم عمومی
فرض کن یک کامپوننت FormActions داری که همیشه یک دکمه اصلی و یک دکمه لغو داره:
jsx
// components/form/FormActions.js
export default function FormActions({ onSubmit, onCancel, submitLabel = 'ذخیره' }) {
return (
<div className="flex gap-3 mt-6">
<PrimaryButton onClick={onSubmit}>{submitLabel}</PrimaryButton>
<Button onClick={onCancel} className="bg-gray-300">لغو</Button>
</div>
);
}
اینجا PrimaryButton جایگزین Button شده، چون رفتارش مثل والد هست، ولی ظاهرش فرق داره.
سناریو ۲: تغییر ظاهر سراسری با یک خط کد
حالا طراح میگه:
“همه دکمههای اصلی باید rounded-lg بشن و font-bold داشته باشن.”
فقط PrimaryButton.js رو ویرایش میکنی:
jsx
className="bg-blue-600 text-white hover:bg-blue-700 shadow-md rounded-lg font-bold"
و همه ۱۰۰ صفحه خودبهخود آپدیت میشن!
سناریو ۳: تیمهای مختلف، یک استاندارد
- تیم A: فرم ثبت سفارش → PrimaryButton
- تیم B: فرم پروفایل → PrimaryButton
- تیم C: فرم پرداخت → PrimaryButton
همه از یک ظاهر استاندارد استفاده میکنن، بدون کپی-پیست کلاسها.
سناریو ۴: حالتهای خاص بدون شکستن LSP
jsx
// components/ui/DangerButton.js
import Button from './Button';
export default function DangerButton({ children, ...props }) {
return (
<Button
className="bg-red-600 text-white hover:bg-red-700"
{...props}
>
{children}
</Button>
);
}
حالا در یک modal حذف:
jsx
<FormActions
submitLabel="حذف"
onSubmit={handleDelete}
// اینجا PrimaryButton نیست، DangerButton است — اما هنوز Button هست!
/>
اگر FormActions به Button وابسته باشه (نه PrimaryButton)، میتونی DangerButton رو هم جایگزین کنی.
چرا LSP مهمه؟ (مزایا)
| مزیت | توضیح |
|---|---|
| کنترل مرکزی ظاهر | یک جا تغییر → همه جا آپدیت |
| کاهش تکرار کد | دیگر ۵۰ بار bg-blue-600 نمینویسی |
| تیمهای مستقل | هر تیم از PrimaryButton استفاده میکنه، بدون نگرانی از ظاهر |
| قابل تست | PrimaryButton رو جدا تست میکنی |
| قابل جایگزینی | بعداً PrimaryButton رو با AnimatedButton عوض کنی، همه جا کار میکنه |
کی نباید از LSP استفاده کرد؟
- اگر فقط یک نوع دکمه داری → نیازی به PrimaryButton نیست.
- اگر ظاهر دکمهها کاملاً متفاوت و غیرقابل پیشبینی هست → بهتره از variant prop استفاده کنی:
jsx
<Button variant="primary">ذخیره</Button>
<Button variant="danger">حذف</Button>
این روش هم خوبه، اما LSP برای وقتیه که بخوای یک “نوع استاندارد” داشته باشی.
جمعبندی: کی از LSP در پروژه واقعی استفاده میکنیم؟
| شرایط | استفاده از LSP |
|---|---|
| Design System داری | حتماً |
| چند تیم روی UI کار میکنن | حتماً |
| میخوای ظاهر رو مرکزی کنترل کنی | حتماً |
| فقط ۲-۳ دکمه ساده داری | شاید نه |
مثال نهایی از یک پروژه واقعی
jsx
// pages/dashboard/users/create.js
import FormActions from '@/components/form/FormActions';
import PrimaryButton from '@/components/ui/PrimaryButton';
export default function CreateUser() {
return (
<form>
{/* ... فیلدها */}
<FormActions
onSubmit={saveUser}
submitLabel="ایجاد کاربر"
/>
</form>
);
}
اگر FormActions این شکلی باشه:
jsx
// فقط به Button وابسته است، نه PrimaryButton
<Button className="bg-blue-600...">ذخیره</Button>
اما تو از PrimaryButton استفاده میکنی → LSP اجازه میده جایگزین کنی بدون شکستن کد.
نتیجه
Liskov Substitution در UI یعنی: “هر جا انتظار یک دکمه داری، یک دکمه استاندارد (مثل PrimaryButton) هم باید کار کنه.”
این اصل در پروژههای بزرگ، Design System، و تیمهای چندنفره حیاتیه.
“اگر از variant prop استفاده کنم، آیا اصل Open/Closed (O) رو نقض نمیکنم؟”
پاسخ کوتاه:
نه، نقض نمیکنی — به شرطی که درست طراحی کنی!
اما اگر بد طراحی کنی، بله، نقض میشه.
بیا با مثال واقعی بررسی کنیم.
مشکل: نقض Open/Closed با variant
نسخه بد (نقض اصل O)
jsx
// components/ui/Button.js
export default function Button({ variant = 'primary', children, ...props }) {
let className = '';
if (variant === 'primary') {
className = 'bg-blue-600 text-white hover:bg-blue-700';
} else if (variant === 'secondary') {
className = 'bg-gray-300 text-gray-700 hover:bg-gray-400';
} else if (variant === 'danger') {
className = 'bg-red-600 text-white hover:bg-red-700';
} else if (variant === 'success') {
className = 'bg-green-600 text-white hover:bg-green-700';
} else if (variant === 'warning') {
className = 'bg-yellow-500 text-white hover:bg-yellow-600';
}
return (
<button className={`px-4 py-2 rounded ${className}`} {...props}>
{children}
</button>
);
}
مشکل:
- هر بار که یک variant جدید میخوای (مثلاً outline, ghost, link)، باید کد Button.js رو تغییر بدی.
- Closed for modification نیست → نقض اصل Open/Closed
راهحل: رعایت Open/Closed با variant
نسخه خوب: استفاده از مپینگ استایلها (Style Map)
jsx
// components/ui/buttonStyles.js
export const buttonVariants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 shadow-md',
secondary: 'bg-gray-300 text-gray-700 hover:bg-gray-400',
danger: 'bg-red-600 text-white hover:bg-red-700',
success: 'bg-green-600 text-white hover:bg-green-700',
outline: 'border border-gray-400 text-gray-700 hover:bg-gray-100',
ghost: 'text-gray-700 hover:bg-gray-200',
link: 'text-blue-600 hover:underline'
};
jsx
// components/ui/Button.js
import { buttonVariants } from './buttonStyles';
export default function Button({
variant = 'primary',
children,
className = '',
...props
}) {
const variantStyle = buttonVariants[variant] || buttonVariants.primary;
return (
<button
className={`px-4 py-2 rounded font-medium transition-all ${variantStyle} ${className}`}
{...props}
>
{children}
</button>
);
}
حالا چطور گسترش میدیم؟
jsx
// components/ui/customVariants.js (فایل جدید!)
import { buttonVariants } from './buttonStyles';
export const extendedVariants = {
...buttonVariants,
premium: 'bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg',
neon: 'bg-black text-green-400 border border-green-400 animate-pulse'
};
jsx
// یا حتی در یک صفحه خاص:
import { buttonVariants } from '@/components/ui/buttonStyles';
const localVariants = {
...buttonVariants,
special: 'bg-gradient-to-r from-indigo-500 to-purple-600 text-white font-bold'
};
<Button variant="special">ویژه</Button>
راهحل بهتر: استفاده از className + variant به صورت ترکیبی
jsx
// components/ui/Button.js
import { buttonVariants } from './buttonStyles';
export default function Button({
variant,
children,
className = '',
...props
}) {
const baseStyle = 'px-4 py-2 rounded font-medium transition-all';
const variantStyle = variant ? buttonVariants[variant] : '';
return (
<button
className={`${baseStyle} ${variantStyle} ${className}`}
{...props}
>
{children}
</button>
);
}
استفاده:
jsx
<Button variant="primary" className="text-lg">بزرگ</Button>
<Button className="bg-orange-500 text-white">کاملاً سفارشی</Button>
حالا هم variant داری، هم کاملاً قابل گسترش بدون تغییر کد اصلی.
مقایسه: LSP vs Variant
| روش | Open/Closed | انعطاف | کنترل مرکزی | مناسب برای |
|---|---|---|---|---|
| LSP (PrimaryButton) | بله | متوسط | بالا | Design System با ظاهر ثابت |
| Variant + Map | بله | بالا | بالا | ظاهرهای زیاد و قابل پیشبینی |
| Variant + if/else | خیر | بالا | پایین | پروژههای کوچک یا موقت |
چه زمانی از کدام استفاده کنیم؟
| شرایط | بهترین روش |
|---|---|
| ۵-۱۰ نوع دکمه با ظاهر مشخص (مثل primary, danger, outline) | Variant + Map |
| فقط ۲ نوع دکمه (مثلاً اصلی و فرعی) | LSP (PrimaryButton) |
| ظاهر کاملاً متفاوت و غیرقابل پیشبینی | className کامل (بدون variant) |
| Design System در تیم بزرگ | ترکیب LSP + Variant Map |
بهترین روش ترکیبی (توصیه حرفهای)
jsx
// components/ui/Button.js
import { buttonVariants } from './buttonStyles';
export default function Button({ variant, children, className = '', ...props }) {
const variantStyle = variant ? buttonVariants[variant] : '';
return (
<button
className={`px-4 py-2 rounded font-medium transition-all ${variantStyle} ${className}`}
{...props}
>
{children}
</button>
);
}
jsx
// components/ui/PrimaryButton.js (اختیاری — برای LSP)
import Button from './Button';
export default function PrimaryButton(props) {
return <Button variant="primary" {...props} />;
}
حالا هم Open/Closed رعایت شده، هم LSP داری، هم انعطاف کامل.
نتیجه نهایی
| سؤال | پاسخ |
|---|---|
| آیا variant اصل Open/Closed رو نقض میکنه؟ | فقط اگر با if/else داخل کامپوننت باشه |
| چطور رعایت کنیم؟ | استایلها رو در فایل جدا (Map) بگذاریم |
| LSP و Variant با هم تداخل دارن؟ | خیر — میتونن مکمل باشن |
“آیا یک کامپوننت باید هم variant داشته باشه، هم className؟ آیا این کار حرفهایه؟”
پاسخ کوتاه:
بله، این کار نه تنها درسته، بلکه بهترین روش حرفهای در صنعت هست.
چون:
- variant → استانداردهای طراحی (Design Tokens)
- className → انعطاف برای موارد خاص
این ترکیب هم کنترل مرکزی داره، هم انعطاف.
چرا این روش بهترین است؟ (با مثال واقعی)
فرض کن در یک Design System کار میکنی (مثل دیجیکالا، اسنپ، یا یک بانک)
۱. variant = استانداردهای تیم طراحی
<Button variant="primary">ثبت سفارش</Button>
<Button variant="danger">حذف حساب</Buttاینها ثابت و مرکزی هستن.
طراح UI میگه: “همه primary باید shadow-md داشته باشن” → فقط buttonStyles.js رو تغییر میدی.
۲. className = انعطاف برای توسعهدهنده
<Button variant="primary" className="text-lg px-6">
دکمه بزرگ
</Button>
<Button variant="primary" className="w-full">
دکمه تمامعرض
</Button>اینها موارد خاص هستن.
مثلاً در یک فرم، فقط یک دکمه باید بزرگتر باشه.
مثال واقعی: کتابخانههای معروف
| کتابخانه | variant | className | نتیجه |
|---|---|---|---|
| Tailwind UI | بله | بله | ترکیب عالی |
| Shadcn/ui | بله | بله | حرفهای |
| MUI (Material UI) | بله (variant) | بله (className) | استاندارد |
| Chakra UI | بله (variant) | بله (className) | انعطاف بالا |
همه از ترکیب استفاده میکنن!
چرا ترکیب variant + className بهترین است؟
| مزیت | توضیح |
|---|---|
| کنترل مرکزی | variant=”primary” → همیشه یک شکل |
| انعطاف محلی | className=”text-xl” → فقط اینجا بزرگتر |
| قابل پیشبینی | همه میدونن primary چیه |
| قابل تست | تست میکنی که variant=”danger” قرمز باشه |
| قابل گسترش | variant جدید در buttonStyles.js اضافه کنی |
| بدون نقض Open/Closed | کد Button.js تغییر نمیکنه |
نسخه نهایی (توصیه حرفهای)
// components/ui/buttonStyles.js
export const buttonVariants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 shadow-md',
secondary: 'bg-gray-300 text-gray-700 hover:bg-gray-400',
danger: 'bg-red-600 text-white hover:bg-red-700',
outline: 'border border-current text-current hover:bg-gray-100',
ghost: 'text-current hover:bg-gray-200',
};// components/ui/Button.js
import { buttonVariants } from './buttonStyles';
export default function Button({
variant,
children,
className = '',
...props
}) {
const baseStyles = 'px-4 py-2 rounded font-medium transition-all duration-200 disabled:opacity-50';
const variantStyles = variant ? buttonVariants[variant] : '';
return (
<button
className={`${baseStyles} ${variantStyles} ${className}`}
{...props}
>
{children}
</button>
);
}استفادههای واقعی
{/* استاندارد — همه جا یک شکل */}
<Button variant="primary">ذخیره</Button>
{/* خاص — فقط اینجا بزرگتر */}
<Button variant="primary" className="text-lg px-6">
ذخیره و ادامه
</Button>
{/* کاملاً سفارشی — بدون variant */}
<Button className="bg-gradient-to-r from-purple-600 to-pink-600 text-white font-bold">
ویژه
</Button>
{/* ترکیب — استاندارد + خاص */}
<Button variant="danger" className="w-full mt-4">
حذف دائمی
</Button>چه زمانی نباید از className استفاده کرد؟
| شرایط | راهکار |
|---|---|
| تغییر رنگ، اندازه، یا سایه | از variant یا size استفاده کن |
| تغییر رفتار (مثل loading) | از loading prop استفاده کن |
| فقط برای یک بار در کل پروژه | از className استفاده کن |
جایگزینهای حرفهای (اختیاری)
// اگر میخوای کنترل بیشتری داشته باشی
<Button size="lg" variant="primary" className="animate-pulse">
بزرگ و چشمکزن
</Button>// components/ui/Button.js (نسخه پیشرفته)
const sizeStyles = {
sm: 'px-2 py-1 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
};
export default function Button({ size = 'md', variant, ... }) {
const sizeStyle = sizeStyles[size];
// ...
}جمعبندی: چرا این روش بهترین است؟
| سوال | پاسخ |
|---|---|
| آیا میشه هم variant داشت هم className؟ | بله، و باید داشت! |
| آیا حرفهایه؟ | بله، همه Design Systemهای بزرگ همین کار رو میکنن |
| آیا Open/Closed رعایت میشه؟ | بله، اگر استایلها در فایل جدا باشه |
| آیا LSP هم رعایت میشه؟ | بله، میتونی PrimaryButton هم داشته باشی |
نسخه نهایی ترکیبی (LSP + Variant + className)
// components/ui/PrimaryButton.js
import Button from './Button';
export default function PrimaryButton({ children, ...props }) {
return <Button variant="primary" {...props}>{children}</Button>;
}// استفاده
<PrimaryButton className="w-full">ذخیره</PrimaryButton>
{/* یا */}
<Button variant="primary" className="w-full">ذخیره</Button>هر دو کار میکنن!
PrimaryButton برای LSP، variant برای استاندارد، className برای انعطاف.
“اگر variant=”primary” داریم، چرا یک PrimaryButton جدا بسازیم؟”
پاسخ کوتاه:
چون variant برای توسعهدهنده است، PrimaryButton برای تیم و استاندارد است.
تفاوت واقعی بین این دو:
| مورد | Button variant=”primary” | PrimaryButton |
|---|---|---|
| مخاطب | توسعهدهنده (Developer) | تیم طراحی + توسعهدهنده |
| هدف | انعطاف | استانداردسازی |
| خطر | ممکنه اشتباه بنویسن | نمیتونن اشتباه کنن |
| قابلیت LSP | خیر | بله |
مثال واقعی: چرا PrimaryButton نیازه؟
فرض کن در یک شرکت با ۵۰ توسعهدهنده کار میکنی.
سناریو ۱: بدون PrimaryButton (فقط variant)
jsx
// صفحه ثبت سفارش
<Button variant="primary">ثبت سفارش</Button>
// صفحه پروفایل
<Button variant="primary">ذخیره تغییرات</Button>
// صفحه پرداخت
<Button variant="primery">پرداخت</Button> // غلط املایی!
مشکلات:
- variant=”primery” → اشتباه تایپی → دکمه خاکستری میشه!
- variant=”Primary” → با حرف بزرگ → کار نمیکنه!
- variant=”blue” → یکی به جای primary از blue استفاده کرد!
هیچ تضمینی نیست که همه درست بنویسن.
سناریو ۲: با PrimaryButton
jsx
// همه جا
<PrimaryButton>ثبت سفارش</PrimaryButton>
<PrimaryButton>ذخیره تغییرات</PrimaryButton>
<PrimaryButton>پرداخت</PrimaryButton>
مزایا:
- نمیتونی اشتباه کنی — فقط یک راه وجود داره.
- استاندارد تیم — همه میدونن دکمه اصلی چیه.
- قابلیت LSP — میتونی در کامپوننتهای والد از PrimaryButton انتظار داشته باشی.
مثال عملی: LSP در عمل
فرض کن یک کامپوننت FormActions داری:
jsx
// components/form/FormActions.js
import PrimaryButton from '../ui/PrimaryButton';
import Button from '../ui/Button';
export default function FormActions({ onSubmit, onCancel }) {
return (
<div className="flex gap-2">
<PrimaryButton onClick={onSubmit}>ذخیره</PrimaryButton>
<Button onClick={onCancel} variant="secondary">لغو</Button>
</div>
);
}
حالا هر صفحهای که از FormActions استفاده کنه، همیشه دکمه اصلی یک شکل خواهد بود.
اگر فقط variant داشتی، باید در ۱۰۰ صفحه بنویسی:
jsx
<Button variant="primary">ذخیره</Button>
و اگر یکی اشتباه بنویسه → UI خراب میشه.
چه زمانی PrimaryButton لازم است؟
| شرایط | نیاز به PrimaryButton |
|---|---|
| تیم بزرگ (۵+ توسعهدهنده) | حتماً |
| Design System داری | حتماً |
| میخوای ظاهر دکمه اصلی همیشه یکسان باشه | حتماً |
| فقط ۱ توسعهدهنده | شاید نه |
چه زمانی فقط variant کافیه؟
| شرایط | فقط variant |
|---|---|
| پروژه کوچک | بله |
| فقط ۱-۲ نوع دکمه | بله |
| توسعهدهنده کنترل کامل داره | بله |
بهترین روش حرفهای: هر دو رو داشته باش!
jsx
// components/ui/Button.js
import { buttonVariants } from './buttonStyles';
export default function Button({ variant, className = '', ...props }) {
const variantStyle = variant ? buttonVariants[variant] : '';
return (
<button className={`px-4 py-2 rounded font-medium ${variantStyle} ${className}`} {...props}>
{props.children}
</button>
);
}
jsx
// components/ui/PrimaryButton.js
import Button from './Button';
export default function PrimaryButton({ children, ...props }) {
return <Button variant="primary" {...props}>{children}</Button>;
}
jsx
// components/ui/DangerButton.js
import Button from './Button';
export default function DangerButton({ children, ...props }) {
return <Button variant="danger" {...props}>{children}</Button>;
}
استفاده در پروژه
jsx
{/* برای تیم — استاندارد */}
<PrimaryButton>ذخیره</PrimaryButton>
{/* برای توسعهدهنده — انعطاف */}
<Button variant="primary" className="text-lg">ذخیره بزرگ</Button>
{/* برای موارد خاص */}
<Button className="bg-gradient-to-r from-purple-600 to-pink-600 text-white">
ویژه
</Button>
مقایسه نهایی
| روش | مزایا | معایب |
|---|---|---|
| فقط variant | انعطاف بالا | خطر اشتباه، عدم LSP |
| فقط PrimaryButton | استاندارد بالا | انعطاف کم |
| هر دو | استاندارد + انعطاف | کمی کد بیشتر |
نتیجه نهایی
PrimaryButton رو بساز، چون:
- استاندارد تیم رو تضمین میکنه
- خطای انسانی رو حذف میکنه
- LSP رو فعال میکنه
- با variant تداخل نداره — مکمل هستن!
مثال از Shadcn/ui (استاندارد صنعت)
tsx
// shadcn/ui
<Button variant="default">Default</Button>
<Button variant="destructive">Delete</Button>
// اما در کدهای واقعی:
<SubmitButton>Save</SubmitButton> // یک کامپوننت جدا!
حتی shadcn که variant داره، از کامپوننتهای جدا مثل SubmitButton استفاده میکنه!