۱۴۰۴/۰۸/۱۱ Nebular

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 = استانداردهای تیم طراحی

jsx
<Button variant="primary">ثبت سفارش</Button>
<Button variant="danger">حذف حساب</Butt

این‌ها ثابت و مرکزی هستن.
طراح UI می‌گه: “همه primary باید shadow-md داشته باشن” → فقط buttonStyles.js رو تغییر می‌دی.


۲. className = انعطاف برای توسعه‌دهنده

jsx
<Button variant="primary" className="text-lg px-6">
دکمه بزرگ
</Button>

<Button variant="primary" className="w-full">
دکمه تمام‌عرض
</Button>

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


مثال واقعی: کتابخانه‌های معروف

کتابخانهvariantclassNameنتیجه
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 تغییر نمی‌کنه

نسخه نهایی (توصیه حرفه‌ای)

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',
outline: 'border border-current text-current hover:bg-gray-100',
ghost: 'text-current hover:bg-gray-200',
};
jsx
// 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>
);
}

استفاده‌های واقعی

jsx
{/* استاندارد — همه جا یک شکل */}
<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 استفاده کن

جایگزین‌های حرفه‌ای (اختیاری)

jsx
// اگر می‌خوای کنترل بیشتری داشته باشی
<Button size="lg" variant="primary" className="animate-pulse">
بزرگ و چشمک‌زن
</Button>
jsx
// 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)

jsx
// components/ui/PrimaryButton.js
import Button from './Button';

export default function PrimaryButton({ children, ...props }) {
return <Button variant="primary" {...props}>{children}</Button>;
}
jsx
// استفاده
<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 استفاده می‌کنه!

Accept Cookies
Accept Cookies
[your-shortcode]