رایگان شدن مخازن خصوصی github
هزینه استفاده از دات نت فریم ورک چقدر است؟
هزینه استفاده از دات نت فریم ورک چقدر است؟
معماری پیازی توسط جفری پالرمو در سال 2008 ابداع شد. این معماری راه بهتری را برای ساخت برنامههای کاربردی جهت تست پذیری، نگهداری و قابلیت اطمینان بهتر بر روی زیرساختهایی مانند پایگاههای داده و خدمات ارائه میدهد. هدف اصلی این معماری، پرداختن به چالشهای پیش روی معماری 3 لایه و ارائه راه حلی برای مشکلات رایج مانند اتصال و جداسازی وابستگیها است. دو نوع اتصال وجود دارند؛ اتصال محکم و اتصال ضعیف که در ادامه آنها را بررسی میکنیم.
اتصال محکم
هنگامی که یک کلاس، به یک وابستگی مشخصی وابسته است، گفته میشود که به شدت با آن کلاس همراه است. یک اتصال محکم جفت شده، به یک شیء دیگر وابسته است. این بدان معناست که تغییر یک شیء در یک برنامهی با اتصال محکم جفت شده، اغلب نیاز به تغییر در تعدادی از اشیاء دیگر دارد. هنگامیکه یک برنامه کوچک است، دشوار نیست، اما در یک برنامهی بزرگ، ایجاد تغییرات بسیار دشوار است.
اتصال ضعیف
یعنی دو شیء مستقل هستند و یک شیء میتواند بدون اینکه به آن وابسته باشد، از شیء دیگری استفاده کند. این یک هدف طراحی است که به دنبال کاهش وابستگیهای متقابل بین اجزای یک سیستم، با هدف کاهش خطر این است که تغییرات در یک جزء، مستلزم تغییر در هر جزء دیگر باشد.
مزایای معماری پیازی
چندین مزیت برای معماری پیازی وجود دارند که در زیر ذکر شدهاند:
- قابلیت نگهداری بهتری را فراهم میکند؛ زیرا همه کدها به لایهها یا مرکز، بستگی دارند.
- تست پذیری بهتری را فراهم میکند؛ زیرا آزمون واحد را میتوان برای لایههای جداگانه، بدون تأثیر بر سایر ماژولهای برنامه ایجاد کرد.
- این برنامه یک برنامهی کاربردی با اتصال آزاد را ایجاد میکند؛ زیرا لایه بیرونی برنامه، همیشه از طریق واسطها با لایه داخلی، ارتباط برقرار میکند.
- هرگونه پیاده سازی پیوسته، در زمان اجرا به برنامه ارائه میشود.
- موجودیتهای دامنه، هسته و بخش مرکزی هستند. میتواند به هر دو لایه پایگاه داده و UI دسترسی داشته باشد.
- لایههای داخلی هرگز به لایه خارجی وابسته نیستند. کدی که ممکن است تغییر کرده باشد، باید بخشی از یک لایه خارجی باشد.
لایههای معماری پیاز
این معماری به شدت به اصل وارونگی وابستگی، متکی است. رابط کاربری از طریق واسطها با منطق تجاری ارتباط برقرار میکند و دارای چهار لایه است. لایهها به سمت مرکز هستند. بخش مرکزی، موجودیتهای Domain است که نشاندهنده موضوعات تجاری و رفتاری است. این لایهها میتوانند متفاوت باشند اما لایه موجودیتهای دامنه، همیشه بخشی از دامنهی مرکزی است. لایه دیگر، رفتار بیشتر یک شیء را تعریف میکند. در ادامه به توضیح لایههای معماری پیاز توجه فرمایید:
Domain Entities Layer
این بخش مرکزی معماری است. تمام اشیاء دامنهی برنامه را در خود نگه میدارد. اگر برنامه ای با چهارچوب موجودیت ORM توسعه داده شود، این لایه دارای کلاسهای POCO (Code First) یا Edmx (Database First) با موجودیتها است. این نهادهای دامنه هیچ وابستگی ندارند.
Repository Layer
این لایه برای ایجاد یک لایه Abstraction بین لایه نهادهای دامنه و لایه منطق تجاری یک برنامه، در نظر گرفته شدهاست. این یک الگوی دسترسی به دادهاست که باعث میشود یک رویکرد مرتبطتر برای دسترسی به دادهها وجود داشته باشد. ما یک مخزن عمومی را ایجاد میکنیم که منبع داده را برای دادهها جستجو میکند، دادهها را از منبع داده به یک نهاد تجاری نگاشت میکند و تغییرات موجودیت تجاری را به منبع داده ارائه میدهد.
Service Layer
این لایه دارای رابطهایی است که برای برقراری ارتباط بین لایه UI و لایه مخزن استفاده میشود و به همراه منطق تجاری برای یک موجودیت است. بنابراین به آن لایه منطق تجاری نیز میگویند.
UI Layer
خارجیترین لایه است و میتواند برنامهی وب، Web API یا پروژه واحد تست باشد. این لایه دارای یک پیاده سازی از جنس Dependency Inversion Principle است بطوری که برنامه، یک برنامهی کاربردی جفت شدهی آزاد میسازد و از طریق واسطها با لایه داخلی ارتباط برقرار میکند.
در مطالب بعدی با مبحث معماری پیازی، نکات تکمیلی و مهمتری از لایه پیاز را تشریح میکنیم و یک پروژه را با معماری پیاز، راه اندازی میکنیم.
<Button Text="This is a test button" />
Tabbed Page نیز چندین Tab را نمایش میدهد که هر Tab خود یک Content Page است. Carousel Page نیز همانند Tabbed Page است، ولی با Swipe کردن به چپ و راست میشود بین صفحات چرخید. هر دوی اینها Multi Page محسوب میشوند. MasterDetail نیز این امکان را میدهد که از بغل منویی برای Swipe کردن وجود داشته باشد. در نهایت Navigation Page محتوای یک Content Page را نمایش میدهد، ولی در بالای آن Navigation Bar دارد؛ شامل دکمه بازگشت به صفحه قبل و Title صفحه جاری و ...
علاوه بر Page ها، Layoutها نیز وجود دارند. برای مثال، Stack Layout برای چینش خطی (افقی یا عمودی) استفاده میشود. Grid برای ساختار شبکهای استفاده میشود و Flex Layout عملکردی مشابه با Flex در وب دارد.
برای مثال، در صورتی که بخواهید چهار دکمه را هم اندازه با هم نمایش دهید، دارید:
<Grid> <Button Text="1" Grid.Row="0" Grid.Column="0" /> <Button Text="2" Grid.Row="0" Grid.Column="1" /> <Button Text="3" Grid.Row="1" Grid.Column="0" /> <Button Text="4" Grid.Row="1" Grid.Column="1" /> </Grid>
در نهایت کنترلها را داریم. برای مثال Label، Button و ... هر کدام از اینها نقشی را ایفا میکنند و امکاناتی دارند.
پس Page داریم، داخل Page از Layout استفاده میکنیم برای چینش کلی صفحه و در نهایت از کنترلهای Image، ListView، Button و ... استفاده میکنیم تا ظاهر فرم تکمیل شود.
هر Page علاوه بر ظاهر خود، دارای یک منطق نیز هست. منطق، کاری است که آن فرم انجام میدهد. برای مثال فرم لاگین میتواند یک Stack Layout عمودی باشد، شامل یک Entry برای گرفتن نام کاربری، یک Entry برای گرفتن رمز عبور، که IsPassword آن True است و در نهایت یک دکمه که برای انجام عمل لاگین است.
در قسمت منطق که با CSharp نوشته میشود، ما یک Property از جنس string برای نگه داشتن نام کاربری داریم. یک Property از جنس string برای نگه داشتن رمز عبور و یک Command که عمل لاگین را انجام دهد. Property اول با نام UserName به Text آن Entry اول وصل میشود (به اصطلاح Bind میشود) و همین طور Property دوم با نام Password نیز به Text آن Entry دوم که IsPassword اش True بود وصل میشود و در نهایت Command لاگین به دکمه لاگین وصل میشود.
برای زدن ظاهر فرم لاگین، در پروژه XamApp روی فولدر Views راست کلیک نموده و از منوی Add به New Item رفته و Content Page را میزنیم. نام آن را LoginView.xaml میگذاریم که داخل تگ Content Page خواهیم داشت:
<StackLayout Orientation="Vertical"> <Entry Placeholder="User name" Text="{Binding UserName}" /> <Entry IsPassword="True" Placeholder="Password" Text="{Binding Password}" /> <Button Command="{Binding LoginCommand}" Text="Login" /> </StackLayout>
برای زدن منطق، در پروژه XamApp روی فولدر ViewModels راست کلیک نموده و از منوی Add گزینه Class را انتخاب کرده و نام آن را LoginViewModel.cs میگذاریم که در داخل آن خواهیم داشت:
public class LoginViewModel : BitViewModelBase { public string UserName { get; set; } public string Password { get; set; } public BitDelegateCommand LoginCommand { get; set; } public LoginViewModel() { LoginCommand = new BitDelegateCommand(Login); } public async Task Login() { // Login implementation ... } }
BitDelegateCommand در این مثال، وظیفه اجرای متد Login را به عهده دارد و آن را اجرا میکند؛ زمانیکه کاربر روی دکمه لاگین Click یا Tap کند.
برای این که Content Page جدید، یعنی LoginView به همراه منطق آن، یعنی LoginViewModel در برنامه نشان داده شوند، لازم است با Navigation به آن صفحه برویم. برای این کار، ابتدا باید این زوج را رجیستر کنیم. برای این کار به متد RegisterTypes در کلاس App رفته (زیر فایل App.xaml یک فایل App.xaml.cs است) و خط زیر را به آن اضافه میکنیم:
containerRegistry.RegisterForNav<LoginView, LoginViewModel>("Login");
حال در متد OnInitializedAsync در چند خط بالاتر داریم:
await NavigationService.NavigateAsync("/Login", animated: false);
این سطر باعث میشود که Navigation Service که همان طور که از اسمش بر میآید، کارش Navigation بین صفحات است، صفحه لاگین را باز کند.
هم اکنون پروژه XamApp بروز شده و دارای این مثال است. در صورتی که آن را الآن Clone کنید و یا در صورتی که از قبل گرفته بودید، دستور git pull را برای گرفتن آخرین تغییرات بزنید، میتوانید این کدها رو داخل پروژه داشته باشید.
برنامه را اجرا کنید و در متد Login، یک Break point بگذارید. سپس برنامه را اجرا کنید. User Name و Password را پر کنید و بر روی دکمه لاگین بزنید. خواهید دید که متد لاگین اجرا میشود و User Name و Password با مقادیری که نوشته بودید، پر شدهاند.
هنوز موارد زیادی برای آموزش باقی مانده، اما با این توضیحات میتوانید در محیط توسعهای که آماده کردهاید، فرمهایی ساده را پیاده سازی کنید و برایشان منطقهایی ساده را بنویسید و به برنامه بگویید که در ابتدای اجرا آن، صفحه را برای شما باز کند. در قسمت بعدی، به صورت عمیقتر وارد UI میشویم.
معماری میکروسرویسها
- از آنجایی که ارتباط بین سرویسها در بستر شبکه انجام میشود، انتظار کندی عملکرد سرویسها دور از ذهن نیست. (اتفاقا بخاطر توزیع برنامه بر روی چند سیستم در زمانی که بار زیادی بر روی سیستم هست پاسخ گویی به کاربر میتونه خیلی بسرعت انجام بپذیره و اتفاقا یکی از مزایای اون هست)
- به دلیل ارتباطات شبکهای، احتمال آسیب پذیریهای امنیتی در این نوع برنامهها بیشتر است. (البته بیشتر این توزیع در server farm انجام میشه ،یعنی پشت فایروال و کسی جز سرورها در این شبکه خصوصی وجود ندارد، نمیگم نیست ولی خیلی نیست)
- نوشتن سرویسهایی که در بستر شبکه با سایر سرویسها در ارتباط هستند سختی و مشکلات خود را دارد. برنامهنویس در این شرایط، درگیر برقراری ارتباط، رمزگذاری دادهها در صورت نیاز و تبدیل آنها میشود.(همان موارد بالا)
- به دلیل مجزا بودن بخشهای مختلف برنامه، مانیتور کردن و ردیابی عملکرد سرویسها، یکی از کارهای اصلی توسعه دهنده یا استفاده کننده از برنامه است. (اینم خودش یک فایده است و طبق اصل SRP و تفاوت MicroServic با SOA بیشتر بر همین نکته تاکید داره که یک میکرو سرویس کاملا مستقل میباشد و راحتتر قابل مانیتور کردن و ردیابی عملکرد سرویس میباشد
- در مجموع سرعت برنامههای نوشته شده با معماری Microservices کندتر از برنامههای نوشته شده با معماری Monolithic است. دلیل آن محیط اجرایی برنامهها است. برنامههایی با معماری Monolithic بر روی حافظه سرور پردازش میشوند. (باز تاکید که اصل استفاده از میکرو سرویس برای سیستم هایی با تراکنش بالا میباشد ،هدف توسعه راحتر و بدون تاثیر بر بقیه سرویسها و حتی بدون توقف آنها میباشد، همچنین امکان horizontal Scalability نرم افزار و بالا بردن تعداد سرورهای ارائه دهنده سرویس براحتی بوجود خواهد امد ، پس میتونه سرعت رو خیلی بالا ببره و مشکل توقف سرویس که در خیلی از سامانههای ایرانی میبینیم رو از بین میبره )
هدف از این مطلب، ارائه راه حلی برای تولید خودکار کد یا شماره یکتا و ترتیبی در زمان ثبت رکورد جدید به صورت یکپارچه با EF Core، میباشد. به عنوان مثال فرض کنید در زمان ثبت سفارش، نیاز است بر اساس یکسری تنظیمات، یک شماره منحصر به فرد برای آن سفارش، تولید شده و در فیلدی تحت عنوان Number قرار گیرد؛ یا به صورت کلی برای موجودیتهایی که نیاز به یک نوع شماره گذاری منحصر به فرد دارند، مانند: سفارش، طرف حساب و ...
یک مثال واقعی
در زمان ثبت یک Task، کاربر میتواند به صورت دستی یک شماره منحصر به فرد را نیز وارد کند؛ در غیر این صورت سیستم به طور خودکار شمارهای را به رکورد در حال ثبت اختصاص خواهد داد. بررسی یکتایی این کد در صورت وارد کردن به صورت دستی، توسط اعتبارسنج مرتبط باید انجام گیرد؛ ولی در غیر این صورت، زیرساخت مورد نظر تضمین میکند که شماره یکتایی را ایجاد کند.
public interface INumberedEntity { string Number { get; set; } }
foreach (var entityType in builder.Model.GetEntityTypes() .Where(e => typeof(INumberedEntity).IsAssignableFrom(e.ClrType))) { builder.Entity(entityType.ClrType) .Property(nameof(INumberedEntity.Number)).IsRequired().HasMaxLength(50); if (typeof(IMultiTenantEntity).IsAssignableFrom(entityType.ClrType)) { builder.Entity(entityType.ClrType) .HasIndex(nameof(INumberedEntity.Number), nameof(IMultiTenantEntity.TenantId)) .HasName( $"UIX_{entityType.ClrType.Name}_{nameof(IMultiTenantEntity.TenantId)}_{nameof(INumberedEntity.Number)}") .IsUnique(); } else { builder.Entity(entityType.ClrType) .HasIndex(nameof(INumberedEntity.Number)) .HasName($"UIX_{entityType.ClrType.Name}_{nameof(INumberedEntity.Number)}") .IsUnique(); } }
public class NumberedEntity : Entity, IMultiTenantEntity { public string EntityName { get; set; } public long NextNumber { get; set; } public long TenantId { get; set; } }
public class NumberedEntityConfiguration : IEntityTypeConfiguration<NumberedEntity> { public void Configure(EntityTypeBuilder<NumberedEntity> builder) { builder.Property(a => a.EntityName).HasMaxLength(256).IsRequired().IsUnicode(false); builder.HasIndex(a => a.EntityName).HasName("UIX_NumberedEntity_EntityName").IsUnique(); builder.ToTable(nameof(NumberedEntity)); } }
شاید به نظر، استفاده از این موجودیت ضروریتی نداشته باشد و خیلی راحت میتوان آخرین شماره ثبت شدهی در جدول مورد نظر را واکشی، مقداری را به آن اضافه و به عنوان شماره منحصر به فرد رکورد جدید استفاده کرد؛ با این رویکرد حداقل دو مشکل زیر را خواهیم داشت:
- ایجاد Gap مابین شمارههای تولید شده، که مدنظر ما نمیباشد. (با توجه به اینکه امکان ثبت دستی را هم داریم، ممکن است کاربر شمارهای را وارد کرده باشد که با آخرین شماره ثبت شده تعداد زیادی فاصله دارد که به خودی خود مشکل ساز نیست؛ ولی در زمان ثبت رکورد بعدی اگر به صورت خودکار ثبت شماره داشته باشد، قطعا آخرین شماره (بزرگترین) را که به صورت دستی وارد شده بود، از جدول دریافت خواهد کرد)
پیاده سازی یک PreInsertHook برای مقداردهی پراپرتی Number
internal class NumberingPreInsertHook : PreInsertHook<INumberedEntity> { private readonly IUnitOfWork _uow; private readonly IOptions<NumberingConfiguration> _configuration; public NumberingPreInsertHook(IUnitOfWork uow, IOptions<NumberingConfiguration> configuration) { _uow = uow ?? throw new ArgumentNullException(nameof(uow)); _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); } protected override void Hook(INumberedEntity entity, HookEntityMetadata metadata) { if (!entity.Number.IsNullOrEmpty()) return; bool retry; string nextNumber; do { nextNumber = GenerateNumber(entity); var exists = CheckDuplicateNumber(entity, nextNumber); retry = exists; } while (retry); entity.Number = nextNumber; } private bool CheckDuplicateNumber(INumberedEntity entity, string nextNumber) { //... } private string GenerateNumber(INumberedEntity entity) { //... } }
ابتدا بررسی میشود اگر پراپرتی Number مقداردهی شدهاست، عملیات مقداردهی خودکار برروی آن انجام نگیرد. سپس با توجه به اینکه ممکن است به صورت دستی قبلا شمارهای مانند Task_1000 وارد شده باشد و NextNumber مرتبط هم مقدار 1000 را داشته باشد؛ در این صورت به هنگام ثبت رکورد بعدی، با توجه به Prefix تنظیم شده، دوباره به شماره Task_1000 خواهیم رسید که در این مورد خاص با استفاده از متد CheckDuplicateNumber این قضیه تشخیص داده شده و سعی مجددی برای تولید شماره جدید صورت میگیرد.
بررسی متد GenerateNumber
private string GenerateNumber(INumberedEntity entity) { var option = _configuration.Value.NumberedEntityOptions[entity.GetType()]; var entityName = $"{entity.GetType().FullName}"; var lockKey = $"Tenant_{_uow.TenantId}_" + entityName; _uow.ObtainApplicationLevelDatabaseLock(lockKey); var nextNumber = option.Start.ToString(); var numberedEntity = _uow.Set<NumberedEntity>().AsNoTracking().FirstOrDefault(a => a.EntityName == entityName); if (numberedEntity == null) { _uow.ExecuteSqlCommand( "INSERT INTO [dbo].[NumberedEntity]([EntityName], [NextNumber], [TenantId]) VALUES(@p0,@p1,@p2)", entityName, option.Start + option.IncrementBy, _uow.TenantId); } else { nextNumber = numberedEntity.NextNumber.ToString(); _uow.ExecuteSqlCommand("UPDATE [dbo].[NumberedEntity] SET [NextNumber] = @p0 WHERE [Id] = @p1 ", numberedEntity.NextNumber + option.IncrementBy, numberedEntity.Id); } if (!string.IsNullOrEmpty(option.Prefix)) nextNumber = option.Prefix + nextNumber; return nextNumber; }
ابتدا با استفاده از متد الحاقی ObtainApplicationLevelDatabaseLock یک قفل منطقی را برروی یک منبع مجازی (lockKey) در سطح نرم افزار از طریق sp_getapplock ایجاد میکنیم. به این ترتیب بدون نیاز به درگیر شدن با مباحث isolation level بین تراکنشهای همزمان یا سایر مباحث locking در سطح row یا table، به نتیجه مطلوب رسیده و تراکنش دوم که خواهان ثبت Task جدید میباشد، با توجه به اینکه INumberedEntity میباشد، لازم است پشت این global lock صبر کند و بعد از commit یا rollback شدن تراکنش جاری، به صورت خودکار قفل منبع مورد نظر باز خواهد شد.
پیاده سازی متد مذکور به شکل زیر میباشد:
public static void ObtainApplicationLevelDatabaseLock(this IUnitOfWork uow, string resource) { uow.ExecuteSqlCommand(@"EXEC sp_getapplock @Resource={0}, @LockOwner={1}, @LockMode={2} , @LockTimeout={3};", resource, "Transaction", "Exclusive", 15000); }
با توجه به اینکه ممکن است درون تراکنش جاری چندین نمونه از موجودیتهای INumberedEntity در حال ذخیره سازی باشند و از طرفی Hook ایجاد شده به ازای تک تک نمونهها قرار است اجرا شود، ممکن است تصور این باشد که اجرای مجدد sp مذکور مشکل ساز شود و در واقع به Lock خود برخواهد خورد؛ ولی از آنجایی که پارامتر LockOwner با "Transaction" مقداردهی میشود، لذا فراخوانی مجدد این sp درون تراکنش جاری مشکل ساز نخواهد بود.
گام بعدی، واکشی NextNumber مرتبط با موجودیت جاری میباشد؛ اگر در حال ثبت اولین رکورد هستیم، لذا numberedEntity مورد نظر مقدار null را خواهد داشت و لازم است شماره بعدی را برای موجودیت جاری ثبت کنیم. در غیر این صورت عملیات ویرایش با اضافه کردن IncrementBy به مقدار فعلی انجام میگیرد. در نهایت اگر Prefix ای تنظیم شده باشد نیز به ابتدای شماره تولیدی اضافه شده و بازگشت داده خواهد شد.
ساختار NumberingConfiguration
public class NumberingConfiguration { public bool Enabled { get; set; } public IDictionary<Type, NumberedEntityOption> NumberedEntityOptions { get; } = new Dictionary<Type, NumberedEntityOption>(); }
public class NumberedEntityOption { public string Prefix { get; set; } public int Start { get; set; } = 1; public int IncrementBy { get; set; } = 1; }
با استفاده از دوکلاس بالا، امکان تنظیم الگوی تولید برای موجودیتها را خواهیم داشت.
گام آخر: ثبت PreInsertHook توسعه داده شده و همچنین تنظیمات مرتبط با الگوی تولید شماره موجودیتها
public static void AddNumbering(this IServiceCollection services, IDictionary<Type, NumberedEntityOption> options) { services.Configure<NumberingConfiguration>(configuration => { configuration.Enabled = true; configuration.NumberedEntityOptions.AddRange(options); }); services.AddTransient<IPreActionHook, NumberingPreInsertHook>(); }
و استفاده از این متد الحاقی در Startup پروژه
services.AddNumbering(new Dictionary<Type, NumberedEntityOption> { [typeof(Task)] = new NumberedEntityOption { Prefix = "T_", Start = 1000, IncrementBy = 5 } });
و موجودیت Task
public class Task : TrackableEntity, IAggregateRoot, INumberedEntity { public const int MaxTitleLength = 256; public const int MaxDescriptionLength = 1024; public string Title { get; set; } public string NormalizedTitle { get; set; } public string Description { get; set; } public TaskState State { get; set; } = TaskState.Todo; public byte[] RowVersion { get; set; } public string Number { get; set; } }
با خروجیهای زیر