خانه / OOP / DDD / Specification Pattern

Specification Pattern

Specifications Pattern

یکی از الگوهای بسیار مهم که دارای بیان های مختلف و پیاده سازی های متفاوتی می باشد و در بیشتر موارد و سناریوها قابلیت استفاده و بکارگیری دارد بی شک Specification Pattern می باشد. یک الگوی همه کاره که تقریبا خیلی از جاها می توان جای پایی از آن بیابیم. در این نوشتار قصد پرداختن به فلسفه و نحوه ی معرفی این الگوی مهم و ارائه نکات مهم در مورد این الگو خواهم داشت. سناریوهای مختلفی که این الگو می تواند در آنها مفید باشد نیز در اینجا بررسی خواهد شد.

  1. مقدمه و تاریخچه ی مختصر

پیدایش و تفکر اولیه در مورد این الگو بر می گردد به زمانی که مارتین فاولر و اریک اوانس با هم در Silicon Valley کار می کردند. در حالی که اریک اوانس در حال کار بر روی مساله ای بود بین او و مارتین فاولر صحبتی انجام می شود در مورد مساله ای که اریک اوانس درگیر آن بود. مساله بدین صورت بود: یک سری درخواست های کاربر وجود داشت و همچنین مجموعه ای از سرویس های حمل و نقل که باید سرویس های حمل و نقل مناسب و متناسب را با درخواست مشتری match می کرد.  مساله ی مشابه دیگری بدین صورت بود که شرکت های حمل و نقل درخواست های خود را برای پیمانکاران ایجاد می کردند. اریک با یافتن تشابهاتی در این دو مساله و همچنین مساله ی نشان دادن نیازمندی های یک مسیر و اطمینان از اینکه محموله و بار در کانتینر مناسب(بر اساس مشخصات کانتینر؛ مثلا برای بار گوشتی و فاسد شدنی کانتینر باید حتما مجهز به یخچال می بود) قرار می گیرد از مدلی بنام “Specification” استفاده کرد. مارتین نیز قبلا با مساله ی مشابهی درگیر بود. یک سیستم بازرگانی که افراد مسئول یافتن ریسک های اعمل شده توسط برخی قراردادهای بانکی هستند. مارتین یکسری extension به الگوی مورد استفاده توسط اریک یعنی specific اعمال نمود. که در ادامه بررسی خواهد شد.

 

  1. فضای مساله

همانطور که احتمالا حدس زده اید در تمام مسایل بالا و البته صدها مسائل مشابه دیگر با یک خصوصیات و ویژگی های مشترک در فضای مسئله مواجه هستیم. در این مسایل همواره یک مجموعه از انتخاب ها برای یک  سری اشیا وجود دارد که هر کدام از موارد موجود در این فضای انتخاب در صورتی که شرایط خاصی را برای هر شی x مورد نظر برآورده نمایند انتخاب خواهند شد. مجموعه اشیا را در اینجا به عنوان Domain Entity ها در نظر بگیرید که هر کدام قصد انتخاب یک زیر مجموعه از انتخاب های موجود بر اساس یک سری Criteria را دارند. ایده ی اصلی Specification جدا کردن عباراتی است که راه حل های کاندید را مشخص می کند از خود اشیایی است که قصد انتخاب این کاندیدا ها را دارند. در یکی از مثال های بالا می توان موارد بیان شده به این صورت بیان کرد. کانتینر به عنوان یک شی که می تواند از میان مجموعه بارها و محموله برخی از آنها را در بر گیرد. این بارها همان انتخاب ها یا کاندید ها هستند. در اینجا عبارات منظور شرایطی است که می توان یک بار را در یک کانتینر جای داد. مثلا برای محموله گوشت یک عبارت می تواند بدین صورت باشد که: 1-کانتینر دارای فریزر باشد و 2-دمای کمتر از 4 درجه سانتی گراد باشد. مثال ها و نمونه های بسیار مختلف و متنوعی را می توان در اینجا اشاره کرد که منطبق با کانسپت Specification می توانند باشند. نکته ی مهم اینکه در اینجا؛ ایده اصلی و ساده درspecification  جدا کردن این عبارت از خود اشیا می باشد. که در ادامه در مورد آن بیشتر توضیح خواهم داد.

  1. انواع مختلف Specification

خوب همانطور که اشاره شد به ایده ی اولیه Specification بعد ها یکسری extension اضافه شد. پیاده سازی های مختلفی می توان از این الگوی مهم داشت که این پیاده سازی ها را بسته به فضای مسئله سه دسته مختلف زیر می توان دسته بندی کرد.

  • Hard coded specification: ساده ترین پیاده سازی برای این الگو هارد کد منطق پیاده سازی هر specification درون آن می باشد. در این نوع پیاده سازی خارج از هر کلاسی که به پیاده سازی specification پرداخته است هیچگونه کنترلی به پیاده سازی آن نخواهیم داشت. در واقع تمام عملیات بصورت ساده درون خود کلاس encapsulate شده اند. می توان گفت که این پیاده سازی در واقع برگرفته از الگوی Strategy[GoF] می باشد. هر چند این روش می تواند دارای flexibility لازم در برخی موارد باشند و به اندازه کافی با اصل keep it simple مطابقت دارند؛ ولی خوب بدیهی است که برای هر specification نیازمند پیاده سازی و کد نویسی مجدد می باشد و ممکن است تحت شرایطی دچار DRY شویم.
  • Parameterized Specification: در این نوع پیاده سازی سعی می شود که برخی از specification ها مشابه هم را بر اساس پارامترهایی در run time کنترل کند. طبیعی است که انعطاف پذیری این نوع پیاده سازی از روش قبل بیشتر خواهد بود. به زبان ساده تر در این نوع از پیاده سازی خواهیم توانست که specification های جدید را بر اساس الگوی قبلی و در زمان اجرا ایجاد کنیم.
  • Composite Specification: این نوع پیاده سازی شبیه یه الگوی Interpreter[GoF] می باشد. در این پیاده سازی سعی می شود با composite کردن specification موجود بر اساس عملگرهای بولین به specification های پیچیده تر دست پیدا کرد. این نوع پیاده سازی بیشترین انعطاف پذیری را دارد.
  1. برخی الگوهای مفید به همراه Specification

یکی از الگوهای بسیار مفید به همراه specification Subsumption می باشد. خوب همانطور که در بالا نیز بیان شد استفاده معمول از الگوی specification مشخص کردن این موضوع هست که آیا برای یکسری کاندیدا ها کدام یک از اعضای این مجموعه criteria موجود را پاس می کنند. Subsumption تلاش می کند که به ازای هر کدام از کاندید ها مشخص کند که آیا یک specification خاص برابر با specification دیگری هست یا خیر. در واقع باید گفت که Subsumption سعی می کند که دو specification خاص را با هم مقایسه کند.

گاهی اوقات هم این مورد می تواند مفید باشد که وقتی یک specification خاص پس نمی شود؛ کدام یک از شرایط پذیرش آن specification خاص رد شده اند. در واقع چه کاری می توان انجام داد تا specification مذکور پاس شود. Partially satisfied specification در این زمینه به ما کمک می کند.

 

در جدول زیر که از مقاله ی اصلی مارتین فاولر و اریک اوانس گرفته شده؛ انواع مختلف پیاده سازی specification بصورت خلاصه بیان شده است:

 

Problem Solutions Pattern
·       نیاز می باشد که زیر مجموعه ای از object ها بر اساس یکسری شرایط خاص انتخاب شوند؛ و البته با این قید که این معیارهای انتخاب در بازه های زمانی refresh می شود

·       برای یکسری rule های خاص نیاز هست که چک شود که فقط objectهای خاص انتخاب شوند

·       نیاز می باشد بین چگونگی انجام کار یک object  و اینکه چه کار انجام می دهد تفاوت قاثل شویم

 

یک Specification ایجاد می کنیم که قادر است با گرفتن object مورد نظر به عنوان ورودی مشخص کند که آیا این object تمامی criteria مشخص شده توسط specification را pass می کنید یا خیر. برای اینکار specification مورد نظر دارای متدی بنام

IsSatisfiedBy(object obj)

می باشد. که دارای خروجی بولین می باشد و در صورتی که تمامی criteria مورد نظر پاس شود true بر می گرداند.

Specification Pattern
پیاده سازی های مختلف Specification تمام کدهای مربوط به هر specification درون متد IsSatisfiedBy نوشته می شود. و این کار برای هر specification دیگر نیز تکرار می شود. Hard Coded Specification
برای مقادیر مشترکی که ممکن است تغییر کند یک سری پارامتر تعریف می شود و سپس IsSatisfiedBy از این پارامترها استفاده می کند. بر خلاف حالت بالا در این شرایط می توان این پارامترها را در run time تغییر داد. Parameterized Specification
با پیاده سازی نوع خاصی از Interpreter Patter[GoF] بر روی specification های موجود جهت ترکیب آنها. Composite Specification
مقایسه دو specification خاص به چه صورت انجام می شود بصورتی که بتوان مشخص کرد که آیا یکی از آنها زیر مجموعه دیگری یا برابر با آن هست یا خیر؟ می توان یک متد بصورت زیر تعریف کرد:

IsGeneralizationOf(Specification spec)

که دارای خروجی بولین می باشد؛ و مشخص می کند که آیا ورودی نوعی خاص یا عمومی یا برابر با specification مورد نظر می باشد یا خیر؟

Subsumption
نیاز هست که مشخص شود که برای یک specification خاص چه اقدامات دیگری باید انجام شود تا آن specification پاس شود.

و همچنین نیاز می باشد که به user توضیح داده شود که به چه دلیل یک specification  خاص satisfied نشده است.

با اضافه کردن متد دیگری بصورت زیر:

ReminderUnsatisfiedBy

که این متد یک specification بر می گرداند که فقط criteria هایی که satisfy نشده اند را مشخص می کند.

Partially Satisfied Specification

 

  1. مثالی از استفاده از Specification

صورت مساله: در بالا مثالی کلاسیک را در مورد وجود یک سری محموله ی باری از یک طرف و همچنین وجود یکسری کانتینر در طرف دیگر بیان کردم. مسئله بدین صورت می باشد که یک سری بار وجود دارد که نیاز می باشد که توسط کانتینرها از مبدا به مقصد حمل شوند؛ و اینکه برای برخی از بارهای یکسری شرایط خاص بسته با ماهیت محموله و بار وجود دارد؛ از جمله برای بارهایی مثل گوشت منجمد نیاز به وجود یخچال با دمای مشخص می باشد.


راه حل: در اینحالت بهنگام تخصیص هر بار به یک کانتینر چک می شود که آیا کانتینر مورد نظر دارای criteria مورد نظر می باشد یا خیر. برای اینکار یک StorageSpecification را ایجاد می کنیم که با گرفتن یک کانتینر مشخص می کند که آیا کانتینر مورد نظر برای بار مذکور شرایط لازم را دارا می باشد یا خیر. استراتژی های مختلف پیاده سازی این StorageSpecification می تواند متفاوت باشد. مثلا می توان برای هر بار با داشتن این متد شرایط مورد نظر را چک کرد. نمودار UML زیر بیانگر این موضوع می باشد.

نکته ی مهمی که در اینجا می توان مشاهده کرد و در بالا نیز بر آن تاکید شده بود decouple کردن specification از domain object هست. که مارتین فاولر و اریک اوانس مزایای زیر را برای آن بر می شمارند.

  1. مزایای Specification

باعث جداسازی پیاده سازی منطق و نیازمندی ها از اعتبار سنجی ها و پالیسی ها منجر به این می شود برای هر domain object مسئولیت های کمتری وجود داشته باشد و به SRP بیش از پیش پایبند بود. همچنین این جداسازی مسئولیت ها باعث می شود که از دوباره و چندباره نوشتن یک سری functionality خود داری شود. یکی از اولین دلایل استفاده از specification ها همین می باشد که یکسری پالیسی های عمومی که قرار است به domain object ها اعمال شود و باعث فیلتر شدن آنها شود از خود domain object ها جدا شود تا از DRY پرهیز کنیم. همچنین این جدا کردن این دو مورد طبیعتا باعث شفافیت بیشتر در تعریف policy ها و domain object ها خواهد شد.

  1. در استفاده از Specification محتاط باشیم

از آنجایی که بین یک domain object و specification مربوط به آن از نقطه نظر مسئولیت تشابهات زیادی می توان یافت در نگاه اول؛ و براحتی ممکن است در تمیز دادن این دو دچار اشتباه شویم؛ پس باید با احتیاط بیشتری دست به اقدام بزنیم. به عنوان مثال در موردی که بالا ذکر شد پس از انتخاب کانتینر مناسب نوبت به انتخاب مسیر بین مبدا و مقصد می باشد یعنی عملیات routing. خوب در اینجا نیز به ازای هر route خاص یک route specification خواهیم داشت که وضعیت route مذکور را از حیث دارای بودن شرایط criteria بررسی می کند. خوب route دارای تعریف مشخص می باشد؛ مسیری از مبدا به مقصد را مشخص می کند و طول مسیر و زمان تقریبی رسیدن از مبدا تا مقصد و موارد را مشخص می کند. برای برخی از بارها رسیدن در یک زمان مشخص یک مجدودیت می باشد که باید در specification به عنوان criteria چک شود. پس route specification بررسی می کند که آیا یک بار در زمان مشخص به مقصد خواهد رسید یا خیر. در نگاه اول ممکن است با توجه به اینکه route دارای زمان رسیدن هم می باشد این بررسی کردن را نیز به route منتقل کنیم. یعنی route به غیر از ترسیم مسیر از مبدا به مقصد وظیفه ی این بررسی را انجام می دهد. هر چند ممکن است که این مورد در شرایطی خاص اوکی باشد ولی در استفاده از آن باید احتیاط لازم به خرج دهیم.

  1. پیاده سازی Specification

تمام کدهای این بخش از اینجا قابل دسترسی می باشند. توجه به نکات زیر در مورد این کدها بسیار حائز اهمیت می باشد:

  • این کدها صرفا جنبه ی آموزشی دارد و بسیار ساده می باشند و به هیچ وجه برای production مناسب نمی باشند.
  • هرچند این سورس کد به زبان C# می باشد؛ اما اصول و مقدمات آن برای سایر زبان ها نیز بصورت مشابه می باشد. همانطور که می توانید مشاهده بفرمائید مثلا در این پیاده سازی از هیچ قابلیت دات نت از جمله مثلا expression استفاده نشده است.

ابتدا یک کلاس پایه برای تمامی specification ها در این مثال ساده آورده شده است.

internal abstract class Specification
{
public abstract bool IsSatisfiedBy(Container container);
}

  • Hard Coded Specification: اولین و ساده ترین نوع پیاده سازی specification همانطور که در بالا نیز اشاره شد Hard Coded می باشد. در این روش باید به ازای هر specification اقدام به نوشتن کد جدید کرد. به عنوان مثال در اینجا دو specification مختلف آورده شده است.


//Hard Coded Specification
class VegtableStorageSpecification : Specification
{
public override bool IsSatisfiedBy(Container container)
{
//Checking all criteria
return container.InternalTemperature > 5
&& container.MaxWeight >= 1;
}
}


//Hard Coded Specification
class MeatStorageSpecification : Specification
{
public override bool IsSatisfiedBy(Container container)
{
//Checking all criteria
return container.HasRefrigerator && container.InternalTemperature < 4
&& container.MaxWeight >= 20;
}
}

 

همانطور که مشاهده می کنید مجبور خواهیم بود برای هر Specification اقدام به پیاده سازی کد جدید کنیم. مثلا اگر برای Vegetable ها شرط دیگری مثلا با دمای کمتر از 2 نیاز می بود باید یک Specification جدید برای آن نوشته بشود.

  • مزیت ها: بوضوح مشخص است که این روش بسیار ساده و سر راست می باشد؛ کافی است برای هر specification جدید یک کلاس جدید ایجاد و criteria را به سادگی درون متد IsSatisfiedBy نوشت.
  • معایب: خوب این روش دارای کمترین انعطاف پذیری می باشد؛ شما مجبور خواهید بود برای هر specification جدید حتی آنهایی که ممکن فقط از نظر value با specification های موجود تفاوت دارند و منطق آنها یکی است نیز اقدام به پیاده سازی کنید.
  • Parametrized Specification

در این مورد سعی می شود برای specification که دارای منطق یکسان هستند اما attribute های دارای مقادیر متفاوت هستند با تعریف یکسری attribute به عنوان پارامتر از دوباره نویسی جلوگیری کند.

class ParameterizedStorageSpecification : Specification
{
private readonly int _maxWeight;
private readonly int _maxTemp;
public ParameterizedStorageSpecification(int maxTemp, int maxWeight)
{
_maxTemp = maxTemp;
_maxWeight = maxWeight;
}
public override bool IsSatisfiedBy(Container container)
{
//Checking all criteria
return container.HasRefrigerator && container.InternalTemperature < _maxTemp
&& container.MaxWeight >= _maxWeight;
}
}

در این روش می توان در زمان اجرا نیز اقدام به تعریف یک سری specification با منطق مشابه ولی مقادیر متفاوت نمود.

  • مزیت ها: نسبت به روش قبل دارای انعطاف پذیری بیشتری می باشد؛ و از دوباره نویسی های بی مورد جلوگیری می کند.
  • معایب: هر چند مقداری انعطاف پذیری در این روش اضافه شده است؛ اما هنوز هم در صورتی که مقداری منطق تغییر کند نیاز می باشد که مجدد specification نوشته شود.
  • Composite Specification

در اینجا سعی خواهد شد با ترکیب specification  های موجود به کاربر اجازه داده شود که اقدام به ترکیب این specification ها نماید. به دو روش می توان به این کار دست یافت. می توان بسادگی با ترکیب تمام specification های موجود به این مهم دست پیدا کرد؛ همانند زیر:

class CompositeSpecification : Specification
{
readonly List<Specification> _specifications;

public CompositeSpecification()
{
_specifications = new List<Specification>
{
new SanitaryForMeatSpecification(),
new MaxWeightSpecification()
};
}
public override bool IsSatisfiedBy(Container container)
{
return _specifications.All(specification =>     specification.IsSatisfiedBy(container));
}

public void AddSpecification(Specification specification)
{
_specifications.Add(specification);
}
}

و یا می توان به کمک عملگرهای منطقی و ترکیب این عملگرها این اجازه را به کاربر داد. توضیح اینکه این عملگرها بسته به نیاز اضافه خواهند شد؛ و اینجا فقط برای سادگی این سه عملگر آشنا آورده شده اند:

abstract class LogicalCompositeSpecification : Specification
{
public abstract override bool IsSatisfiedBy(Container container);

public LogicalCompositeSpecification And(Specification specification)
{
return new AndSpecification(this, specification);
}

public LogicalCompositeSpecification Or(Specification specification)
{
return new OrSpecification(this, specification);
}
public LogicalCompositeSpecification Not()
{
return new NotSpecification(this);
}
}

به عنوان مثال پیاده سازی And در پایین آورده شده است. سایر موارد نیز مشابه می باشند.

class AndSpecification : LogicalCompositeSpecification
{
private readonly Specification _leftSpecification;
private readonly Specification _rigtSpecification;
public AndSpecification(Specification lefSpecification, Specification rightSpecification)
{
_leftSpecification = lefSpecification;
_rigtSpecification = rightSpecification;

}
public override bool IsSatisfiedBy(Container container)
{
return _leftSpecification.IsSatisfiedBy(container) && _rigtSpecification.IsSatisfiedBy(container);
}
}

  • مزیت ها: همانطور که می توان حدس زد با این روش براحتی می توان با ترکیب های مختلفی که از specification های موجود می توان ایجاد کرد؛ انعطاف پذیری بالایی بدست آورد.
  • معایب: طبیعتا پیچیدگی بیشتر نسبت به دو روش قبل(که البته بستگی به فریمورک و زبان هم دارد) مهمترین عیب این روش می باشد.
  1. پیاده سازی Subsumption

یکی از موارد مهم و ارزشمند به همراه specification بحث مقایسه ی دو specification می باشد و تشخیص اینکه آیا یکی از آنها نوع خاصی/عمومی از دیگری می باشد. این مورد می تواند بصورت قانون زیر نیز بیان شود:

اگر به ازای هر x و دو specification  مورد نظر مثلا(A و B) در صورتی که برای تمامی xهایی که B برای آنها پاس می شود؛ این اتفاق برای A نیز بیافتد می توان نتیجه گرفت که B نوع خاصی از A می باشد؛ و در صورتی که نتیجه برای هر ورودی برای B درست باشد بدون بررسی A می توان نتیجه گرفت که نتیجه به ازای A نیز صحیح می باشد.

به عنوان فرض کنید:

A: مسیر رفتن از بهبهان به تهران

B: مسیر رفتن از بهبهان به تهران از طریق اهواز

با توجه به اینکه B بیان خاصی از A می باشد در نتیجه هر مسیری که به ازای B درست باشد؛ بدون بررسی کردن می توان نتیجه گرفت که برای A نیز صحیح می باشد.

سناریوهای مختلفی می توان یافت که subsumption می تواند مفید باشد. به عنوان مثال؛ در صورتی که container دارای یک attribute بنام contentsSpecification از نوع StorageSpecification  باشد و خود Cargo نیز دارای attribute با نام storageSpecification و از نوع StorageSpecification باشد؛ با توجه به اینکه می دانیم که این attribute در container نوع خاصی از همان  attribute در cargo می باشد؛ در نتیجه در متد IsSatisfiedBy در cargo  بدون بررسی مستقیم و به کمک contentsSpecification نیز می توان به نتیجه مورد نظر دست پیدا کرد. و این عمل باعث decouple شدن cargo و container خواهد شد.

//Hard Coded Specification
class MeatStorageSpecification : Specification
{
public override bool IsSatisfiedBy(Container container)
{
return container.contentsSpecification();
}
}

Subsumption با parameterized Specification و Composite Specification قابل استفاده می باشد. در parameterized specification می توان با افزودن دو متد isSpecialCaseOf() و isGeneralizationOf() به هر specification این شرایط را بررسی کرد. در روش Parametrized طبیعی است که باید دو specification که قصد مقایسه آنها را داریم باید هم نوع باشند؛ تا امکان مقایسه آنها وجود داشته باشد.

  1. Partially Fellfield Specification

اکثر اوقات اینکه یک candidate خاص specification مورد نظر را satisfy می کند یا خیر کافی است. در حالت معمول IsSatisfiedBy() یا پاسخ true یا false برمی گرداند. گاهی اوقات نیاز می باشد که برای هر candidate که ممکن است بخشی از criteria یک specification خاص را پاس می کند بفهمیم که چه بخش ها و قسمت ها باعث پاس نشدن شده است. به عنوان مثال مورد زیر را در نظر بگیرید:

در دامین routing که در بالا مطرح شد یک مسیر بصورت زیر انتخاب خواهد شد؛ و route specification  مورد نظر را پاس می کند.

  • Origin: Honk Kong
  • Destination: Chicago
  • Stop-off: Salt Lake City (to drop off part of the Cargo)
  • Customer Clearance: Seattle

خوب این مسیر اصلی انتخاب شده و شروع به حرکت می کند اما در میانه ی مسیر متوجه می شود که به دلیل بارندگی و برخی مشکلات امکان ادامه دادن از مسیر فعلی وجود ندارد. در واقع فقط بخشی از مسیر فعلی طی شده است. از آنجایی که مسیر طی شده قابل برگشت نمی باشد و نمی توان مجدد به انتخاب مسیر از ابتدای راه اقدام کرد. چه بهتر می بود که برای مسیر اصلی انتخاب شده؛ متد دیگری نیز وجود می داشت که بخشی از مسیری که برای specification مورد نظر که در بالا نشان داده شده؛ مشخص کند که چه بخش هایی pass نشده اند. این کار با افزودن متد دیگری به specification بنام reminderUnsatisfiedBy  می باشد؛ که یک specification حاوی موارد پاس نشده بر می گرداند. که می تواند برای پاس کردن انتخاب فعلی کمک کننده باشد. به عنوان نمونه در مثال ذکر شده؛ این متد یک specification بصورت زیر بر می گرداند:

  • Destination: Chicago
  • Stop-off: Salt Lake City

حالا می توان با توجه به نقطه ای که مسیر قبلی متوقف شده و به کمک specification برگشت داده شده اقدام به انتخاب مسیر دیگری نمود. با فرض اینکه مسیر قبلی تا Seattle پیش رفته بود؛ specification جدید می تواند بصورت زیر باشد.

  • Origin: Seattle
  • Destination: Chicago
  • Stop-off: Salt Lake City (to drop off part of the Cargo)

 

منابع:

درباره ی masoud@admin

همچنین ببینید

مقایسه سه Pattern مهم مشتق شده از MV* بنام MVC و MVP و MVVM

مقایسه سه Pattern مهم مشتق شده از MV* بنام MVC و MVP و MVVM MVC …

Tell Don’t Ask Principal

#Tell_Dont_Ask_Principal #OOP #اندکی_تامل ✍✍✍✍✍✍✍✍✍✍✍ یکی از اصول بسیار مهم در دنیای Object-Oriented اصل بسیار مهم …

پاسخ دهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

در تلگرام هم همراه شما هستم

اگر علاقمند به معماری نرم افزار و مبحث محبوب مایکروسرویس هستید؛ در کانال با ما همراه باشید. اطلاعات مفید زیادی در این کانال انتظار شما را می کشند. فقط کافیست دکمه ی پیوستن را بفشارید.

پیوستن بستن