private void ModifyItemList() { ItemList.Clear(); var AllItems = from BetaData in BetaContext.BetaDatas where BetaData.ProjectName == Globals.ProjectName && BetaData.ProjectCode == Globals.ProjectCode && (BetaData.Type == Globals.Types.Partition || BetaData.Type == Globals.Types.Door) select BetaData; foreach(BetaData data in AllItems) { byte[] imageByte = data.Image.ToArray(); MemoryStream MS = new MemoryStream(imageByte); Image img = Image.FromStream(MS); StiImage stiImg = new StiImage(); stiImg.Image = img; Items newItem = new Items { Image = img, Name = data.Name }; ItemList.Add(newItem); } } private void modiefieItemListToolStripMenuItem_Click(object sender, EventArgs e) { } private void modifiedItemListToolStripMenuItem_Click(object sender, EventArgs e) { ModifyItemList(); ModifiedItemListReport.RegBusinessObject("Items",ItemList); ModifiedItemListReport.Compile(); ModifiedItemListReport.Render(); ModifiedItemListReport.Show(); } public class Items { public string Name { get; set; } public Image Image { get; set; } }
معرفی روش جدید نوشتن عبارات switch در C#8.0
فرض کنید یک enum که معرف تعدادی رنگ است را تعریف کردهایم:
public enum Rainbow { Red, Orange, Yellow, Green, Blue, Indigo, Violet }
class RGBColor { internal byte Red { get; } internal byte Green { get; } internal byte Blue { get; } internal RGBColor(byte red, byte green, byte blue) { Red = red; Green = green; Blue = blue; } public override string ToString() => $"rgb({Red}, {Green}, {Blue})"; }
internal static RGBColor FromRainbow(Rainbow rainbowBolor) { switch (rainbowBolor) { case Rainbow.Red: return new RGBColor(0xFF, 0x00, 0x00); case Rainbow.Orange: return new RGBColor(0xFF, 0x7F, 0x00); case Rainbow.Yellow: return new RGBColor(0xFF, 0xFF, 0x00); case Rainbow.Green: return new RGBColor(0x00, 0xFF, 0x00); case Rainbow.Blue: return new RGBColor(0x00, 0x00, 0xFF); case Rainbow.Indigo: return new RGBColor(0x4B, 0x00, 0x82); case Rainbow.Violet: return new RGBColor(0x94, 0x00, 0xD3); default: throw new ArgumentException(message: "invalid enum value", paramName: nameof(rainbowBolor)); }; }
internal static RGBColor TasteTheRainbow(Rainbow rainbowColor) => rainbowColor switch { Rainbow.Red => new RGBColor(0xFF, 0x00, 0x00), Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00), Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00), Rainbow.Green => new RGBColor(0x00, 0xFF, 0x00), Rainbow.Blue => new RGBColor(0x00, 0x00, 0xFF), Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82), Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3), _ => throw new ArgumentException(message: "invalid enum value", paramName: nameof(rainbowColor)), };
- در ادامه تمام caseها حذف میشوند و بجای آنها صرفا مقادیر مدنظر باقی میماند. در اینجا <= به صورت expressed as خوانده میشود.
- caseهای مختلف با کاما از هم جدا میشوند.
- همچنین در سطر آخر آن نیز از یک discard استفاده شدهاست که معادل همان حالت default یا حالتی است که هیچ تطابقی صورت نگرفته باشد.
- به علاوه اگر دقت کنید، نتیجهی نهایی این switch جدید، به صورت یک مقدار، توسط متد TasteTheRainbow، بازگشت داده شدهاست. بنابراین نوشتن یک چنین عباراتی در C# 8.0، مجاز است:
var operation = "+"; int a = 1, b = 2; var result = operation switch { "+" => a + b, "-" => a - b, "/" => a / b, _ => throw new NotSupportedException() };
معرفی Property Patterns در C# 8.0
کلاس زیر را درنظر بگیرید که از تعدادی خاصیت عمومی تشکیل شدهاست:
class Address { public string AddressLine1 { get; set; } public string AddressLine2 { get; set; } public string City { get; set; } public string State { get; set; } public string PostalCode { get; set; } public string CountryRegion { get; set; } }
static class PropertyPatterns { internal static decimal ComputeSalesTax( Address location, decimal salePrice) => location switch { { State: "Fars" } => salePrice * 0.06m, { State: "Tehran", City: "Tehran" } => salePrice * 0.056m, // Other cases removed for brevity... _ => 0M }; }
معرفی Tuple Patterns در C# 8.0
در switchهای C# 8.0، میتوان از tuples نیز برای تشکیل قسمت case و همچنین مقداری که قرار است switch بر روی آن صورت گیرد، استفاده کرد:
static class TuplePatterns { internal static string RockPaperScissors( string first, string second) => (first, second) switch { ("rock", "paper") => "Rock is covered by Paper. Paper wins!", ("rock", "scissors") => "Rock breaks Scissors. Rock wins!", ("paper", "rock") => "Paper covers Rock. Paper wins!", ("paper", "scissors") => "Paper is cut by Scissors. Scissors wins!", ("scissors", "rock") => "Scissors is broken by Rock. Rock wins!", ("scissors", "paper") => "Scissors cuts Paper. Scissors wins!", (_, _) => "tie" }; }
بهبودهای Pattern Matching بر روی اشیاء در C# 8.0
فرض کنید شیء پایهی Shape را تعریف و بر اساس آن دو شیء جدید دایره و مستطیل را ایجاد کردهایم:
class Shape { protected internal double Height { get; } protected internal double Length { get; } protected Shape(double height = 0, double length = 0) { Height = height; Length = length; } } class Circle : Shape { internal double Radius => Height / 2; internal double Diameter => Radius * 2; internal double Circumference => 2 * Math.PI * Radius; internal Circle(double height = 10, double length = 10) : base(height, length) { } } class Rectangle : Shape { internal bool IsSquare => Height == Length; internal Rectangle(double height = 10, double length = 10) : base(height, length) { } }
static class ObjectPatterns { internal static string ShapeDetails(this Shape shape) => shape switch { Circle c => $"circle with (C): {c.Circumference}", Rectangle s when s.IsSquare => $"L:{s.Length} H:{s.Height}, square", Rectangle r => $"L:{r.Length} H:{r.Height}, rectangle", _ => "Unknown shape!" // Discard }; }
معرفی Positional Patterns در C# 8.0
در اینجا یک Point را داریم که میخواهیم بر اساس آن یک Quadrant را استخراج کنیم:
class Point { public int X { get; } public int Y { get; } public Point(int x, int y) => (X, Y) = (x, y); public void Deconstruct(out int x, out int y) => (x, y) = (X, Y); } enum Quadrant { Unknown, Origin, One, Two, Three, Four, OnBorder }
static class PositionalPatterns { internal static Quadrant AsQuadrant(Point point) => point switch { (0, 0) => Quadrant.Origin, var (x, y) when x > 0 && y > 0 => Quadrant.One, var (x, y) when x < 0 && y > 0 => Quadrant.Two, var (x, y) when x < 0 && y < 0 => Quadrant.Three, var (x, y) when x > 0 && y < 0 => Quadrant.Four, (_, _) => Quadrant.OnBorder, // Either are 0, but not both _ => Quadrant.Unknown }; }
در اینجا اگر دقت کنید و case مخصوص discards معرفی شدهاست. اولی برای حالتهایی است که هیچکدام از شرایط پیش از آن را برآورده نمیکند، مانند حالت (1,0)، در غیراینصورت سطر بعد از آن بازگشت داده میشود.
public class Token { [JsonPropertyName("refreshToken")] public string RefreshToken { get; set; } } public async Task<IActionResult> RefreshToken([FromBody]Token model)
مروری بر روشهای موجود
همواره روشهای مختلفی برای پیاده سازی یک ایده در دنیای نرم افزار وجود دارد که هر روش را میتوان بر حسب نیاز مورد استفاده قرار داد. در برنامههای مبتنی بر WPF معمولا از دو روش عمده برای این منظور استفاده میشود:
1-استفاده از فایلهای resx
در این روش که برای Win App نیز استفاده میشود، اطلاعات مورد نیاز برای هر زبان به شکل جدول هایی دارای کلید و مقدار در داخل یک فایل .resx نگهداری میشود و در زمان اجرای برنامه بر اساس انتخاب کاربر اطلاعات زبان مورد نظر از داخل فایل resx خوانده شده و نمایش داده میشود. یکی از ضعف هایی که این روش در عین ساده بودن دارد این است که همه اطلاعات مورد نیاز داخل assembly اصلی برنامه قرار میگیرد و امکان افزودن زبانهای جدید بدون تغییر دادن برنامه اصلی ممکن نخواهد بود.
2-استفاده از فایلهای csv که به فایلهای dll تبدیل میشوند
در این روش با استفاده از ابزارهای موجود در کامپایلر WPF برای هر کنترل یک property به نام Uid ایجاد شده و مقدار دهی میشود. سپس با ابزار دیگری ( که جزو ابزارهای کامپایلر محسوب نمیشود ) از فایل csproj پروژه یک خروجی اکسل با فرمت csv ایجاد میشود که شامل Uidهای کنترلها و مقادیر آنها است. پس از ترجمه متون مورد نظر به زبان مقصد با کمک ابزار دیگری فایل اکسل مورد نظر به یک net assembly تبدیل میشود و داخل پوشه ای با نام culture استاندارد ذخیره میشود. ( مثلا برای زبان فارسی نام پوشه fa-IR خواهد بود ). زمانی که برنامه اجرا میشود بر اساس culture ای که در سیستم عامل انتخاب شده است و در صورتی که برای آن culture فایل dll ای موجود باشد، زبان مربوط به آن culture را load خواهد کرد. با وجود این که این روش مشکل روش قبلی را ندارد و بیشتر با ویژگیهای WPF سازگار است اما پروسه ای طولانی برای انجام کارها دارد و به ازای هر تغییری باید کل مراحل هر بار تکرار شوند. همچنین مشکلاتی در نمایش برخی زبانها ( از جمله فارسی ) در این روش مشاهده شده است.
روش سوم!
روش سوم اما کاملا بر پایه WPF و در اصطلاح WPF-Native میباشد. ایده از آنجا ناشی شده است که برای ایجاد skin در برنامههای WPF استفاده میشود. در ایجاد برنامههای Skin-Based به این شیوه عمل میشود که skinهای مورد نظر به صورت style هایی در داخل ResourceDictionary ها قرار میگیرند. سپس آن ResourceDictionary به شکل dll کامپایل میشود. در برنامه اصلی نیز همه کنترلها style هایشان را به شکل dynamic resource از داخل یک ResourceDictionary مشخص شده load میکنند. حال کافی است برای تغییر skin فعلی، ResourceDictionary مورد نظر از dll مشخص load شود و ResourceDictionary ای که در حال حاضر در برنامه از آن استفاده میشود با ResourceDictionary ای که load شده جایگزین شود. کنترلها مقادیر جدید را از ResourceDictionary جدید به شکل کاملا خودکار دریافت خواهند کرد.
به سادگی میتوان از این روش برای تغییر زبان برنامه نیز استفاده کرد با این تفاوت که این بار، به جای Style ها، Stringهای زبانهای مختلف را درون resourceها نگهداری خواهیم کرد.
یک مثال ساده
در این قسمت نحوه پیاده سازی این روش با ایجاد یک نمونه برنامه ساده که دارای دو زبان انگلیسی و فارسی خواهد بود آموزش داده میشود.
ابتدا یک پروژه WPF Application در Visual Studio 2010 ایجاد کنید. در MainWindow سه کنترل Button قرار دهید و یک ComboBox که قرار است زبانهای موجود را نمایش دهد و با انتخاب یک زبان، نوشتههای درون Buttonها متناسب با آن تغییر خواهند کرد.
توجه داشته باشید که برای Buttonها نباید به صورت مستقیم مقداری به Content شان داده شود. زیرا مقدار مورد نظر از داخل ResourceDictionary که خواهیم ساخت به شکل dynamic گرفته خواهد شد. پس در این مرحله یک ResourceDictionary به پروژه اضافه کرده و در آن resource هایی به شکل string ایجاد میکنیم. هر resource دارای یک Key میباشد که بر اساس آن، Button مورد نظر، مقدار آن Resource را load خواهد کرد. فایل ResourceDictionary را
Culture_en-US.xaml نامگذاری کنید و مقادیر مورد نظر را به آن اضافه نمایید.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:system="clr-namespace:System;assembly=mscorlib"> <system:String x:Key="button1">Hello!</system:String> <system:String x:Key="button2">How Are You?</system:String> <system:String x:Key="button3">Are You OK?</system:String> </ResourceDictionary>
دقت کنید که namespace ای که کلاس string در آن قرار دارد به فایل xaml اضافه شده است و پیشوند system به آن نسبت داده شده است.
با افزودن یک ResourceDictionary به پروژه، آن ResourceDictionary به MergedDictionary کلاس App اضافه میشود. بنابراین فایل App.xaml به شکل زیر خواهد بود:
<Application x:Class="BeRMOoDA.WPF.LocalizationSample.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml"> <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Culture_en-US.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application>
برای اینکه بتوانیم محتوای Buttonهای موجود را به صورت داینامیک و در زمان اجرای برنامه، از داخل Resourceها بگیریم، از DynamicResource استفاده میکنیم.
<Button Content="{DynamicResource ResourceKey=button1}" /> <Button Content="{DynamicResource ResourceKey=button2}" /> <Button Content="{DynamicResource ResourceKey=button3}" />
بسیار خوب! اکنون باید شروع به ایجاد یک ResourceDictionary برای زبان فارسی کنیم و آن را به صورت یک فایل dll کامپایل نماییم.
برای این کار یک پروژه جدید در قسمت WPF از نوع User control ایجاد میکنیم و نام آن را Culture_fa-IR_Farsi قرار میدهیم. لطفا شیوه نامگذاری را رعایت کنید چرا که در ادامه به آن نیاز خواهیم داشت.
پس از ایجاد پروژه فایل UserControl1.xaml را از پروژه حذف کنید و یک ResourceDictionary با نام Culture_fa-IR.xaml اضافه کنید. محتوای آن را پاک کنید و محتوای فایل Culture_en-US.xaml را از پروژه قبلی به صورت کامل در فایل جدید کپی کنید. دو فایل باید ساختار کاملا یکسانی از نظر key برای Resourceهای موجود داشته باشند. حالا زمان ترجمه فرا رسیده است! رشتههای دلخواه را ترجمه کنید و پروژه را build نمایید.
پس از ترجمه فایل Culture_fa-IR.xaml به شکل زیر خواهد بود:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:system="clr-namespace:System;assembly=mscorlib"> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Culture_fa-IR_Farsi.xaml"/> </ResourceDictionary.MergedDictionaries> <system:String x:Key="button1">سلام!</system:String> <system:String x:Key="button2">حالت چطوره؟</system:String> <system:String x:Key="button3">خوبی؟</system:String> </ResourceDictionary>
در ادامه میخواهیم راهکاری ارئه دهیم تا بتوان فایلهای dll مربوط به زبانها را در زمان اجرای برنامه اصلی، load کرده و نام زبانها را در داخل ComboBox ای که داریم نشان دهیم. سپس با انتخاب هر زبان در ComboBox، محتوای Buttonها بر اساس زبان انتخاب شده تغییر کند.
برای سهولت کار، نام فایلها را به گونه ای انتخاب کردیم که بتوانیم سادهتر به این هدف برسیم. نام هر فایل از سه بخش تشکیل شده است:
پوشه ای با نام Languages در کنار فایل اجرایی برنامه اصلی ایجاد کنید و فایل Culture_fa-IR_Farsi.dll را درون آن کپی کنید. تصمیم داریم همه dllهای مربوط به زبانها را داخل این پوشه قرار دهیم تا مدیریت آنها سادهتر شود.
برای مدیریت بهتر فایلهای مربوط به زبانها یک کلاس با نام CultureAssemblyModel خواهیم ساخت که هر instance از آن نشانگر یک فایل زبان خواهد بود. یک کلاس با این نام به پروژه اضافه کنید و propertyهای زیر را در آن تعریف نمایید:
public class CultureAssemblyModel { //the text will be displayed to user as language name (like Farsi) public string DisplayText { get; set; } //name of .dll file (like Culture_fa-IR_Farsi.dll) public string Name { get; set; } //standar notation of this culture (like fa-IR) public string Culture { get; set; } //name of resource dictionary file inside the loaded .dll (like Culture_fa-IR.xaml) public string XamlFileName { get; set; } }
برای خواندن لیست cultureهای موجود، لیستی از CultureAssmeblyModelها ایجاد کرده و با استفاده از متد LoadCultureAssmeblies، آن را پر میکنیم.
//will keep information about loaded assemblies public List<CultureAssemblyModel> CultureAssemblies { get; set; } //loads assmeblies in languages folder and adds their info to list void LoadCultureAssemblies() { //we should be sure that list is empty before adding info (do u want to add some cultures more than one? of course u dont!) CultureAssemblies.Clear(); //creating a directory represents applications directory\languages DirectoryInfo dir = new DirectoryInfo(System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\languages"); //getting all .dll files in the language folder and its sub dirs. (who knows? maybe someone keeps each culture file in a seperate folder!) var assemblies = dir.GetFiles("*.dll", SearchOption.AllDirectories); //for each found .dll we will create a model and set its properties and then add to list for (int i = 0; i < assemblies.Count(); i++) {
string name = assemblies[i].Name;
CultureAssemblyModel model = new CultureAssemblyModel() { DisplayText = name.Split('.', '_')[2], Culture = name.Split('.', '_')[1], Name = name , XamlFileName =name.Substring(0, name.LastIndexOf(".")) + ".xaml" }; CultureAssemblies.Add(model); } }
comboboxLanguages.ItemsSource = CultureAssemblies;
<ComboBox HorizontalAlignment="Left" Margin="10" VerticalAlignment="Top" MinWidth="100" Name="comboboxLanguages"> <ComboBox.ItemTemplate> <DataTemplate> <Label Content="{Binding DisplayText}"/> </DataTemplate> </ComboBox.ItemTemplate> </ComboBox>
در مرحله بعد، قرار است متدی بنویسیم که اطلاعات زبان انتخاب شده را گرفته و با جابجایی ResourceDictionary ها، زبان برنامه را تغییر دهیم.
متدی با نام LoadCulture در کلاس App ایجاد میکنیم که یک CultureAssemblyModel به عنوان ورودی دریافت کرده و ResourceDictionary داخل آن را load میکند و آن را با ResourceDictionary فعلی موجود در App.xaml جابجا مینماید.
با این کار، Button هایی که قبلا مقدار Content خود را از Resourceهای موجود دریافت میکردند، اکنون از Resourceهای جابجا شده خواهند گرفت و به این ترتیب زبان انتخاب شده بر روی برنامه اعمال میشود.
//loads selected culture public void LoadCulture(CultureAssemblyModel culture) { //creating a FileInfo object represents .dll file of selected cultur FileInfo assemblyFile = new FileInfo("languages\\" + culture.Name); //loading .dll into memory as a .net assembly var assembly = Assembly.LoadFile(assemblyFile.FullName); //getting .dll file name var assemblyName = assemblyFile.Name.Substring(0, assemblyFile.Name.LastIndexOf(".")); //creating string represents structure of a pack uri (something like this: /{myassemblyname;component/myresourcefile.xaml} string packUri = string.Format(@"/{0};component/{1}", assemblyName, culture.XamlFileName); //creating a pack uri Uri uri = new Uri(packUri, UriKind.Relative); //now we have created a pack uri that represents a resource object in loaded assembly //and its time to load that as a resource dictionary (do u remember that we had resource dictionary in culture assemblies? don't u?) var dic = Application.LoadComponent(uri) as ResourceDictionary; dic.Source = uri; //here we will remove current merged dictionaries in our resource dictionary and add recently-loaded resource dictionary as e merged dictionary var mergedDics = this.Resources.MergedDictionaries; if (mergedDics.Count > 0) mergedDics.Clear(); mergedDics.Add(dic); }
void comboboxLanguages_SelectionChanged(object sender, SelectionChangedEventArgs e) { var selectedCulture = (CultureAssemblyModel)comboboxLanguages.SelectedItem; App app = Application.Current as App; app.LoadCulture(selectedCulture); }
کار انجام شد!
از مزیتهای این روش میتوان به WPF-Native بودن، سادگی در پیاده سازی، قابلیت load کردن هر زبان جدیدی در زمان اجرا بدون نیاز به کوچکترین تغییر در برنامه اصلی و همچنین پشتیبانی کامل از نمایش زبانهای مختلف از جمله فارسی اشاره کرد.
public class SearchDTO { public string TableName { get; set; } public string ColumnName { get; set; } public string searchPhrase { get; set; } = string.Empty; public DateTimeOffset? searchDateFrom { get; set; } public DateTimeOffset? searchDateTo { get; set; } public int Include { get; set; } = 1; }
// Join the tables dynamically based on the table names for (int i = 1; i < filterList.Count; i++) { var joinEntityType = entityTypes.FirstOrDefault(t => t.ClrType.Name == filterList[0].TableName)?.ClrType; if (entityType == null) { return (null, 0, 0); } var joinEntityQuery = (IQueryable<object>)Activator.CreateInstance(typeof(DbSet<>).MakeGenericType(joinEntityType), _dbContext); query = query.Join(joinEntityQuery.ToList(), x => x.GetType().GetProperty($"{filterList[i - 1].TableName}.{filterList[i - 1].TableName}Id").GetValue(x), y => y.GetType().GetProperty($"{filterList[i].TableName}.{filterList[i].TableName}Id").GetValue(y), (x, y) => x); } // Apply the conditions dynamically based on the column names and conditions for (int i = 0; i < filterList.Count; i++) { if (!string.IsNullOrEmpty(filterList[i].searchPhrase)) { var parameter = Expression.Parameter(entityType, "x"); var condition = Expression.Call( typeof(string).GetMethod("Contains", new[] { typeof(string) }), Expression.PropertyOrField(parameter, filterList[i].ColumnName), Expression.Constant(filterList[i].searchPhrase) ); var lambda = Expression.Lambda<Func<object, bool>>(condition, parameter); query = query.Where(lambda); } if (filterList[i].searchDateFrom.HasValue) { //must write expression for date constraint } } // Select the specified columns dynamically ResourceManager resourceManager = new ResourceManager(typeof(TablePropertiesResources)); var columnNames = resourceManager.GetResourceSet(CultureInfo.CurrentCulture, true, true) .OfType<DictionaryEntry>() .Select(entry => entry.Key.ToString()) .ToList(); var selectColumns = columnNames.ToArray(); var selectedData = query .Select(x => new { // Dynamically select the desired properties Result = selectColumns.ToDictionary(column => column, column => x.GetType().GetProperty(column).GetValue(x)) }) .ToList();
در ادامه بحث «حذف کدهای تکراری»، روش Refactoring دیگری به نام "Extract Superclass" وجود دارد که البته در بین برنامه نویسهای دات نت به نام Base class بیشتر مشهور است تا Superclass. هدف آن هم انتقال کدهای تکراری بین چند کلاس، به یک کلاس پایه و سپس ارث بری از آن میباشد.
یک مثال:
در WPF و Silverlight جهت مطلع سازی رابط کاربری از تغییرات حاصل شده در مقادیر دادهها، نیاز است کلاس مورد نظر، اینترفیس INotifyPropertyChanged را پیاده سازی کند:
using System.ComponentModel;
namespace Refactoring.Day6.ExtractSuperclass.Before
{
public class User : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value) return;
_name = value;
raisePropertyChanged("Name");
}
}
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
و نکتهی مهم این است که اگر 100 کلاس هم داشته باشید، باید این کدهای تکراری اجباری مرتبط با raisePropertyChanged را در آنها قرار دهید. به همین جهت مرسوم است برای کاهش حجم کدهای تکرای، قسمتهای تکراری کد فوق را در یک کلاس پایه قرار میدهند:
using System.ComponentModel;
namespace Refactoring.Day6.ExtractSuperclass.After
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
و سپس از آن ارث بری میکنند:
namespace Refactoring.Day6.ExtractSuperclass.After
{
public class User : ViewModelBase
{
string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value) return;
_name = value;
RaisePropertyChanged("Name");
}
}
}
}
به این ترتیب این کلاس پایه در دهها و صدها کلاس قابل استفاده خواهد بود، بدون اینکه مجبور شویم مرتبا یک سری کد تکراری «اجباری» را copy/paste کنیم.
مثالی دیگر:
اگر با ORM های Code first کار کنید، نیاز است تا ابتدا طراحی کار توسط کلاسهای ساده دات نتی انجام شود؛ که اصطلاحا به آنها POCO یا Plain old CLR objects یا Plain old .NET Classes هم گفته میشود. در بین این کلاسها، متداول است که یک سری از خصوصیات، تکراری و مشترک باشد؛ مثلا تمام کلاسها تاریخ ثبت رکورد را هم داشته باشند به همراه نام کاربر و مشخصاتی از این دست. اینجا هم برای حذف کدهای تکراری، یک Base class طراحی میشود: (+)
در این مقاله چه چیزی را پوشش خواهیم داد:
· راه اندازی داکر
· پیکرهبندی container image
· وصل شدن به sql
· ساخت یک پروژه ساده net core.
· ایجاد دیتابیس
· ثبت رکورد در دیتابیس
قبل از هرچیز باید داکر را بر روی سیستم عامل خود (لینوکس) نصب نماید. چون نصب داکر بر روی لینوکس از حوصلهی این مقاله خارج میباشد، میتوانید با مراجعه به این لینک docker را نصب کنید. پس از نصب docker، برای اطمینان حاصل نمودن از نصب، با دستور docker version میتوان کانفیگ داکر را مشاهده کرد:
دانلود و نصب sql server بر روی داکر
قبل از هرچیز باید Image اسکیوال سرور را بر روی داکر دانلود نمائید. برای این کار وارد سایت dockerhub شوید و عبارت microsoft/mssql-server-linux را جستجو کنید.
همانطور که در تصویر نیز مشاهد میکنید، این بسته 10 میلیون بار دریافت شدهاست! در ادامه دستور زیر را در ترمینال خود Paste کنید و منتظر بمانید تا دانلود شود:
docker pull microsoft/mssql-server-linux:2017-latest
برای اجرای image sql از دستور زیر استفاده میکنیم:
sudo docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=<YourStrong!Passw0rd>' \ -p 1433:1433 --name sql1 \ -d mcr.microsoft.com/mssql/server:2017-latest
Set the SA_PASSWORD : پسورد environment variable ای که شما انتخاب میکنید.
p 1433:1433- : شماره پورتی که Docker container بر روی آن اجرا میشود.
-d microsoft/mssql-server-linux:2017-latest : نام Image ای که میخواهیم اجرا کنیم.
همانطور که ملاحظه میکنید، در قسمت status، عبارت up به معنای در حال اجرا بودن container است. اگر عبارت دیگری را مشاهده کردید، با دستور dockr start id و وارد کردن شماره image خود میتوانید آن را اجرا کنید.
تا اینجا توانستیم sql server را اجرا کنیم. برای توضیحات بیشتر به این لینک مراجعه کنید.
وصل شدن به sql
برای وصل شدن به دیتابیس باید connection string دیتابیس مربوطه را داشته باشیم. با توجه به کانفیگهایی که در بالا انجام دادیم، connection string ما به شکل زیر خواهد بود:
Server Host: localhost Port: 1433 Authentication: SQL Server Authentication Login: SA Password: <StrongPasswordYouSet>
sudo docker exec -it sql1 "bash"
/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P '<YourNewStrong!Passw0rd>'
تا اینجای کار sql server آمادهی اجرا دستورات شما میباشد. در ادامه میخواهیم چند دستور سادهی sql را بر روی آن اجرا کنیم.
ساخت دیتابیس
با دستور sqlcmd زیر، ابتدا یک دیتابیس را میسازیم:
CREATE DATABASE TestDB
ساخت جدول
در ادامه، دستور زیر را برای ساخت جدول مینویسیم:
CREATE TABLE Inventory (id INT, name NVARCHAR(50), quantity INT)
ایجاد رکورد
مرحله بعدی، ایجاد یک رکورد جدید در دیتابیس میباشد:
INSERT INTO Inventory VALUES (1, 'banana', 150); INSERT INTO Inventory VALUES (2, 'orange', 154);
در آخر با استفاده از دستور go، کوئریهای بالا را اجرا میکنیم. اکنون باید یک دیتابیس جدید به نام TestDB و یک جدول جدید نیز به نام Inventory همچنین یک رکورد جدید در آن ثبت شده باشد. برای مشاهدهی تغییرات بالا، از دستورات زیر استفاده میکنیم:
- با دستور زیر لیست دیتابیسهای موجود را میتوان دید:
SELECT Name from sys.Databases
SELECT * FROM Inventory WHERE quantity > 152;
تا اینجا توانستیم docker را بر روی سیستم راه ندازی و همچنین sql server را بر روی آن نصب و اجرا کنیم. همچنین با دستورات sqlcmd توانستیم بر روی sql کوئری بزنیم.
ساخت و وصل شدن یک پروژهی net core. و وصل شدن به sql server
حال میخواهیم با یک پروژهی سادهی net core. به sql server فوق وصل شده و یک جدول را به دیتابیس مذکور اضافه کرده و یک کوئری اضافه کردن رکوردی را به آن جدول بنویسیم. برای شروع، یک پروژهی خالی net core. را ایجاد میکنیم. برای مثال یک پروژهی api را ایجاد میکنیم:
dotnet new webapi -o dockerapi
dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Design
public class Students { public int Id { get; set; } public string Name { get; set; } public string Phone { get; set; } }
dotnet ef dbcontext scaffold "Server=localhost,1433\\Catalog=tutorial_database;Database=<YOUR_DATABASE_NAME>;User=SA;Password=<StrongPasswordYouSet>;" Microsoft.EntityFrameworkCore.SqlServer
"ConnectionStrings": { "TestingDatabase": "Server=localhost:1433\\Database=<YourDatabaseName>;User=SA;Password=<StrongPasswordYouSet>;" }
dotnet ef migrations add <NAME_OF_MIGRATION>
همانطور که مشاهده میکنید، migrations اضافه شده و موجودیت هم اضافه شدهاست. حال باید بر روی migrations خود آپدیت بزنیم:
ef database update
SELECT TABLE_NAME FROM dockerdb.INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'
ویژگی های پیشرفته ی AutoMapper - قسمت دوم
کار زیر را میتونی انجام بدی:
public class Anbar_KalaViewModel { public Anbar Anbar { get; set; } public Kala Kala{ get; set; } public int Tedad { get; set; } } //Class Configure Mapper.CreateMap<Anbar_Kala,Anbar_KalaViewModel>();
پیاده سازی Full-Text Search با SQLite و EF Core - قسمت اول - ایجاد و به روز رسانی جدول مجازی FTS
SELECT Title FROM Book WHERE Desc LIKE '%cat%';
معادل دستور LIKE در کوئری فوق، متد Contains در EF Core است:
var cats = context.Chapters.Where(item => item.Text.Contains("cat")).ToList();
یک نکته: کوئری فوق توسط EF Core و به همراه پروایدر SQLite آن، به صورت زیر ترجمه میشود (که آن نیز یک full table scan است):
SELECT "c"."Text" FROM "Chapters" AS "c" WHERE ('cat' = '') OR (instr("c"."Text", 'cat') > 0)
var cats = context.Chapters.Where(item => item.Text.StartsWith("cat")).ToList(); // SELECT "c"."Text", FROM "Chapters" AS "c" WHERE "c"."Text" IS NOT NULL AND ("c"."Text" LIKE 'cat%')
var cats = context.Chapters.Where(item => item.Text.EndsWith("cat")).ToList(); // SELECT "c"."Title" FROM "Chapters" AS "c" WHERE "c"."Text" IS NOT NULL AND ("c"."Text" LIKE '%cat')
معرفی موجودیتهای مثال این سری
هدف اصلی ما، ایندکس کردن full-text ستونهای متنی عنوان و متن جدول بانک اطلاعاتی متناظر با Chapter است:
using System.Collections.Generic; namespace EFCoreSQLiteFTS.Entities { public class User { public int Id { get; set; } public string Name { get; set; } public ICollection<Chapter> Chapters { get; set; } } public class Chapter { public int Id { get; set; } public string Title { get; set; } public string Text { get; set; } public User User { get; set; } public int UserId { get; set; } } }
زمانیکه عملیات Migration را در EF Core فعال و اجرا میکنیم، دو جدول متناظر با Chapter و User ایجاد میشوند. اما برای کار با full-text search، نیاز به ایجاد جداول دیگری است، تا کار نگهداری ایندکسهای تشکیل شدهی از ستونهای متنی مدنظر ما را انجام دهند. به این نوع جداول در SQLite، جدول مجازی و یا virtual table گفته میشود. یک virtual table در اصل تفاوتی با یک جدول معمولی ندارد. تفاوت در اینجا است که منطق دسترسی به این جدول مجازی از موتور FTS5 مربوط به SQLite باید عبور کند. تاکنون نگارشهای مختلفی از موتور full-text search آن منتشر شدهاند؛ مانند FTS3 ، FTS4 و غیره که آخرین نگارش آن، FTS5 میباشد و به همراه توزیعی که مایکروسافت ارائه میدهد، وجود دارد و نیازی به تنظیمات خاصی ندارد.
در اینجا روش ایجاد یک جدول مجازی جدید Chapters_FTS را مشاهده میکنید:
CREATE VIRTUAL TABLE "Chapters_FTS" USING fts5("Text", "Title", content="Chapters", content_rowid="Id")
ذکر پارامتر "content="Chapters اختیاری بوده و به این معنا است که نیازی نیست تا اصل دادههای مرتبط با ستونهای ذکر شده نیز ذخیره شوند و آنها را میتوان از جدول Chapters، بازیابی کرد. در این حالت برای برقراری ارتباط بین این جدول مجازی و جدول chapters، پارامتر "content_rowid="Id مقدار دهی شدهاست. content_rowid به primary key جدول content اشاره میکند. ذکر هر دوی این پارامترها اختیاری بوده و در صورت تنظیم، حجم نهایی بانک اطلاعاتی را کاهش میدهند. چون در این حالت دیگری نیازی به ذخیره سازی جداگانهی اصل اطلاعات متناظر با ایندکسهای FTS نیست.
اکنون که با دستور ایجاد جدول مجازی FTS آشنا شدیم، روش ایجاد آن در برنامههای مبتنی بر EF Core نیز دقیقا به همین صورت است:
private static void createFtsTables(ApplicationDbContext context) { // For SQLite FTS // Note: This can be added to the `protected override void Up(MigrationBuilder migrationBuilder)` method too. context.Database.ExecuteSqlRaw(@"CREATE VIRTUAL TABLE IF NOT EXISTS ""Chapters_FTS"" USING fts5(""Text"", ""Title"", content=""Chapters"", content_rowid=""Id"");"); }
به روز رسانی اطلاعات جدول مجازی FTS، توسط تریگرها
پس از اجرای دستورCREATE VIRTUAL TABLE فوق، SQLite پنج جدول را به صورت خودکار ایجاد میکند که در تصویر زیر قابل مشاهده هستند:
البته ما مستقیما با این جداول کار نخواهیم کرد و این جداول برای نگهداری اطلاعات ایندکسهای full-text موتور FTS5، توسط خود SQLite نگهداری و مدیریت میشوند.
اما ... نکتهی مهم اینجا است که جدول مجازی Chapters_FTS، هرچند به جدول اصلی Chapters توسط پارامتر content آن متصل شدهاست، اما تغییرات آنرا ردیابی نمیکند. یعنی هر نوع insert/update/delete ای که در جدول اصلی Chapters رخ میدهد، سبب ایندکس شدن اطلاعات جدید آن در جدول مجازی Chapters_FTS نمیشود و برای اینکار باید اطلاعات را مستقیما در جدول Chapters_FTS درج کرد.
روش پیشنهاد شدهی در مستندات رسمی آن، استفاده از تریگرهای پس از درج اطلاعات، پس از حذف اطلاعات و پس از به روز رسانی اطلاعات به صورت زیر است:
-- Create a table. And an external content fts5 table to index it. CREATE TABLE tbl(a INTEGER PRIMARY KEY, b, c); CREATE VIRTUAL TABLE fts_idx USING fts5(b, c, content='tbl', content_rowid='a'); -- Triggers to keep the FTS index up to date. CREATE TRIGGER tbl_ai AFTER INSERT ON tbl BEGIN INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c); END; CREATE TRIGGER tbl_ad AFTER DELETE ON tbl BEGIN INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c); END; CREATE TRIGGER tbl_au AFTER UPDATE ON tbl BEGIN INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c); INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c); END;
در ادامه سه تریگر بر روی جدول اصلی که ما به صورت متداولی با آن در برنامههای خود کار میکنیم، تعریف شدهاند. این تریگرها کار insert اطلاعات را در جدول مجازی ایجاد شده، به صورت خودکار انجام میدهند.
همانطور که مشاهده میکنید، یک rowid نیز در اینجا قابل تعریف است؛ rowid، ستون مخفی یک جدول مجازی FTS است و هرچند در حین ایجاد، آنرا ذکر نمیکنیم، اما جزئی از ساختار آن بوده و قابل کوئری گرفتن است.
نکتهی مهم: به فرمت دستورات به روز رسانی جدول مجازی FTS دقت کنید. حتی در حالت تریگرهای update و یا delete نیز در اینجا دستور insert، مشاهده میشوند. این فرمت دقیقا باید به همین نحو رعایت شود؛ در غیراینصورت اگر از دستورات delete و یا update معمولی بر روی این جدول مجازی استفاده کنید، دفعهی بعدی که برنامه را اجرا میکنید، خطای «این بانک اطلاعاتی تخریب شدهاست» را مشاهده کرده (database disk image is malformed) و دیگر نمیتوانید با فایل بانک اطلاعاتی خود کار کنید.
به روز رسانی اطلاعات جدول مجازی FTS توسط EF Core
using System; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace EFCoreSQLiteFTS.DataLayer { public static class EFChangeTrackerExtensions { public static List<(EntityState State, TEntity NewEntity, TEntity OldEntity)> GetChangedEntities<TEntity>(this DbContext dbContext) where TEntity : class, new() { if (!dbContext.ChangeTracker.AutoDetectChangesEnabled) { // ChangeTracker.Entries() only calls `Try`DetectChanges() behind the scene. dbContext.ChangeTracker.DetectChanges(); } return dbContext.ChangeTracker.Entries<TEntity>() .Where(IsEntityChanged) .Select(entityEntry => (entityEntry.State, entityEntry.Entity, createWithValues<TEntity>(entityEntry.OriginalValues))) .ToList(); } private static bool IsEntityChanged(EntityEntry entry) { return entry.State == EntityState.Added || entry.State == EntityState.Modified || entry.State == EntityState.Deleted || entry.References.Any(r => r.TargetEntry?.Metadata.IsOwned() == true && IsEntityChanged(r.TargetEntry)); } private static T createWithValues<T>(PropertyValues values) where T : new() { var entity = new T(); foreach (var prop in values.Properties) { var value = values[prop.Name]; if (value is PropertyValues) { throw new NotSupportedException("nested complex object"); } else { prop.PropertyInfo.SetValue(entity, value); } } return entity; } } }
علت نیاز به نمونهی اصل و سپس تغییر کردهی موجودیتها، به نحوهی تعریف تریگرهای مخصوص به به روز رسانی FTS بر میگردد. اگر دقت کرده باشید در این تریگرها، new.a و همچنین old.a را داریم که برای شبیه سازی آنها دقیقا باید به اطلاعات یک رکورد، در پیش و پس از به روز رسانی آن، دسترسی یافت.
ب) تعریف تریگرهای SQL توسط سیستم tracking؛ به همراه عملیات نرمال سازی اطلاعات
using System.Collections.Generic; using System.Data; using System.Text.RegularExpressions; using EFCoreSQLiteFTS.Entities; using Microsoft.EntityFrameworkCore; namespace EFCoreSQLiteFTS.DataLayer { public static class FtsNormalizer { private static readonly Regex _htmlRegex = new Regex("<[^>]*>", RegexOptions.Compiled); public static string NormalizeText(this string text) { if (string.IsNullOrWhiteSpace(text)) { return string.Empty; } // Remove html tags text = _htmlRegex.Replace(text, string.Empty); // TODO: add other normalizers here, such as `remove diacritics`, `fix Persian Ye-Ke` and so on ... return text; } } public static class UpdateFtsTriggers { public static void UpdateChapterFTS( this DbContext context, List<(EntityState State, Chapter NewEntity, Chapter OldEntity)> changedChapters) { var database = context.Database; try { database.BeginTransaction(IsolationLevel.ReadCommitted); foreach (var (State, NewEntity, OldEntity) in changedChapters) { var chapterNew = NewEntity; var chapterOld = OldEntity; var normalizedNewText = chapterNew.Text.NormalizeText(); var normalizedOldText = chapterOld.Text.NormalizeText(); var normalizedNewTitle = chapterNew.Title.NormalizeText(); var normalizedOldTitle = chapterOld.Title.NormalizeText(); switch (State) { case EntityState.Added: if (shouldSkipAddedChapter(chapterNew)) { continue; } database.ExecuteSqlRaw("INSERT INTO Chapters_FTS(rowid, Text, Title) values({0}, {1}, {2});", chapterNew.Id, normalizedNewText, normalizedNewTitle); break; case EntityState.Modified: if (shouldSkipModifiedChapter(chapterNew, chapterOld)) { continue; } // This format is important! Otherwise we will get `SQLite Error 11: 'database disk image is malformed'.` error! database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS(Chapters_FTS, rowid, Text, Title) VALUES('delete', {0}, {1}, {2}); ", chapterOld.Id, normalizedOldText, normalizedOldTitle); database.ExecuteSqlRaw("INSERT INTO Chapters_FTS(rowid, Text, Title) values({0}, {1}, {2});", chapterNew.Id, normalizedNewText, normalizedNewTitle); break; case EntityState.Deleted: // This format is important! Otherwise we will get `SQLite Error 11: 'database disk image is malformed'.` error! database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS(Chapters_FTS, rowid, Text, Title) VALUES('delete', {0}, {1}, {2}); ", chapterOld.Id, normalizedOldText, normalizedOldTitle); break; } } } finally { database.CommitTransaction(); } } private static bool shouldSkipAddedChapter(Chapter chapterNew) { // TODO: add your logic to avoid indexing this item return false; } private static bool shouldSkipModifiedChapter(Chapter chapterNew, Chapter chapterOld) { // TODO: add your logic to avoid indexing this item return chapterNew.Text == chapterOld.Text && chapterNew.Title == chapterOld.Title; } } }
همچنین در اینجا متد NormalizeText را نیز مشاهده میکند که بر روی ستونهای متنی اعمال شدهاست. کار آن پاکسازی تگهای یک متن HTML ای است و نگهداری اطلاعات صرفا متنی آن. در اینجا اگر نیاز بود میتوان منطقهای پاکسازی اطلاعات دیگری را نیز اعمال کرد.
اکنون که این اطلاعات به صورت پاکسازی شده در جدول مجازی درج میشوند، زمانیکه بر روی آنها جستجویی صورت میگیرد، دیگر شامل جستجوی بر روی تگهای HTML ای نیست و دقت بسیار بیشتری دارد.
ج) اتصال به سیستم
پس از تعریف متدهای الحاقی GetChangedEntities و UpdateChapterFTS، اکنون روش اتصال آنها به DbContext برنامه، با بازنویسی متد SaveChanges آن است:
namespace EFCoreSQLiteFTS.DataLayer { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions options) : base(options) { } public DbSet<Chapter> Chapters { get; set; } public DbSet<User> Users { get; set; } public override int SaveChanges() { var changedChapters = this.GetChangedEntities<Chapter>(); this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again. var result = base.SaveChanges(); this.ChangeTracker.AutoDetectChangesEnabled = true; this.UpdateChapterFTS(changedChapters); return result; } } }
در قسمت بعدی، روش کوئری گرفتن از این جدول مجازی FTS را بررسی میکنیم.
همانطور که قول داده بودم، به اصول GRASP میپردازیم.
اصول GRASP-General Responsibility Assignment
Software Principles
این اصول به بررسی نحوه تقسیم وظایف بین کلاسها و مشارکت اشیاء برای به انجام رساندن یک مسئولیت میپردازند. اینکه هر کلاس در ساختار نرم افزار چه وظیفهای دارد و چگونه با کلاسهای دیگر مشارکت میکند تا یک عملکرد به سیستم اضافه گردد. این اصول به چند بخش تقسیم میشوند:
- کنترلر ( Controller )
- ایجاد کننده ( Creator )
- انسجام قوی ( High Cohesion )
- واسطه گری ( Indirection )
- دانای اطلاعات ( Information Expert )
- اتصال ضعیف ( Low Coupling )
- چند ریختی ( Polymorphism )
- حفاظت از تاثیر تغییرات ( Protected Variations )
- مصنوع خالص ( Pure Fabrication )
Controller
این الگو بیان میکند که مسئولیت پاسخ به رویدادهای (Events ) یک سناریوی محدود مانند یک مورد کاربردی ( Use Case ) باید به عهده یک کلاس غیر UI باشد. کنترلر باید کارهایی را که نیاز است در پاسخ رویداد انجام شود، به دیگران بسپرد و نتایج را طبق درخواست رویداد بازگرداند. در اصل، کنترلر دریافت کننده رویداد، راهنمای مسیر پردازش برای پاسخ به رویداد و در نهایت برگرداننده پاسخ به سمت مبداء رویداد است. در زیر مثالی را میبینیم که رویداد اتفاق افتاده توسط واسط گرافیکی به سمت یک handler (که متدی است با ورودیِ فرستنده و آرگمانهای مورد نیاز) در کنترلر فرستاده میشود. این روش event handling، در نمونههای وب فرم و ویندوز فرم دیده میشود. به صورتی خود کلاسهای .Net وظیفه Event Raising از سمت UI با کلیک روی دکمه را انجام میدهد:
public class UserController { protected void OnClickCreate(object sender, EventArgs e) { // call validation services // call create user services } }
در مثال بعد عملیات مربوط به User در یک WebApiController پاسخ داده میشود. در اینجا به جای استفاده از Event Raising برای کنترل کردن رویداد، از فراخوانی یک متد در کنترلر توسط درخواست HttpPost انجام میگیرد. در اینجا نیاز است که در سمت کلاینت درخواستی را ارسال کنیم:
public class UserWebApiController { [HttpPost] public HttpResponseMessage Create(UserViewModel user) { // call validation services // call create user services } }
Creator :
این اصل میگوید شیء ای میتواند یک شیء دیگر را بسازد ( instantiate ) که: (اگر کلاس B بخواهد کلاس A را instantiate کند)
- کلاس B شیء از کلاس A را در خود داشته باشد؛
- یا اطلاعات کافی برای instantiate کردن از A را داشته باشد؛
- یا به صورت نزدیک با A در ارتباط باشد؛
- یا بخواهد شیء A را ذخیره کند.
از آنجایی که این اصل بدیهی به نظر میرسد، با مثال نقض، درک بهتری را نسبت به آن میتوان پیدا کرد:
// سازنده public class B { public static A CreateA(string name, string lastName, string job) { return new A() { Name =name, LastName = lastName, Job = job }; } } // ایجاد شونده public class A { public string Name { get; set; } public string LastName { get; set; } public string Job { get; set; } } public class Context { public void Main() { var name = "Rasoul"; var lastName = "Abbasi"; var job = "Developer"; var obj = B.CreateA(name, lastName, job); } }
و اما چرا این مثال، اصل Creator را نقض میکند. در مثال میبینید که کلاس B، یک شیء از نوع A را در متد Main کلاس Context ایجاد میکند. کلاس B فقط یک متد برای تولید A دارد و در عملیات تولید A هیچ منطق خاصی را پیاده سازی نمیکند.کلاس B شیء ای را از کلاس A ، در خود ندارد، با آن ارتباط نزدیک ندارد و آنرا ذخیره نمیکند. با اینکه کلاس B اطلاعات کافی را برای تولید A از ورودی میگیرد، ولی این کلاس Context است که اطلاعات کافی را ارسال مینماید. اگر در کلاس B منطقی اضافه بر instance گیریِ ساده وجود داشت (مانند بررسی صحت و اعتبار سنجی)، میتوانستیم بگوییم کلاس B از یک مجموعه عملیات instance گیری با خبر است که کلاس Context نباید از آن خبر داشته باشد. لذا اکنون هیچ دلیلی وجود ندارد که وظیفه تولید A را در Context انجام ندهیم و این مسئولیت را به کلاس B منتقل کنیم. این مورد ممکن است در ذهن شما با الگوی Factory تناقض داشته باشد. ولی نکته اصلی در الگو Factory انجام عملیات instance گیری با توجه به منطق برنامه است؛ یعنی وظیفهای که کلاس Context نباید از آن خبر داشته باشد را به کلاس Factory منتقل میکنیم. در غیر اینصورت ایجاد کلاس Factory بی معنا خواهد بود (مگر به عنوان افزایش انعطاف پذیری معماری که بتوان به راحتی نوع پیاده سازی یک واسط را تغییر داد).
High Cohesion :
این اصل اشاره به یکی از اصول اساسی طراحی نرم افزار دارد. انسجام واحدهای نرم افزاری باعث افزایش خوانایی، سهولت اشکال زدایی، قابلیت نگهداری و کاهش تاثیر زنجیرهای تغییرات میشود. طبق این اصل، مسئولیتهای هر واحد باید مرتبط باشد. لذا اجزایی کوچک با مسئولیتهای منسجم و متمرکز بهتر از اجزایی بزرگ با مسئولیتهای پراکنده است. اگر واحدهای سازنده نرم افزار انسجام ضعیفی داشته باشند، درک همکاریها، استفاده مجدد آنها، نگه داری نرم افزار و پاسخ به تغییرات سختتر خواهد شد.
در مثال زیر نقض این اصل را مشاهده میکنیم:
class Controller { public void CreateProduct(string name, int categoryId) { } public void EditProduct(int id, string name) { } public void DeleteProduct(int id) { } public void CreateCategory(string name) { } public void EditCategory(int id, string name) { } public void DeleteCategory(int id) { } }
همانطور که میبینید، کلاس
کنترلر ما، مسئولیت مدیریت Product و Category را بر عهده دارد. بزرگ شدن این کلاس، باعث سختتر شدن
خواندن کد و رفع اشکال میگردد. با جداسازی کنترلر مربوط به Product از Category میتوان انسجام را بالا برد.
Indirection :
این اصل بیان میکند که با تعریف یک واسط بین دو مولفه نرم افزاری میتوان میزان اتصال نرم افزار را کاهش داد. بدین ترتیب وظیفه هماهنگی ارتباط دو مؤلفه، به عهده این واسط خواهد بود و نیازی نیست دادههای ورودی و خروجی دو مؤلفه، هماهنگ باشند. در اینجا واسط، از وابستگی بین دو مؤلفه با پنهان کردن ضوابط هر مؤلفه از دیگری و ایجاد وابستگی ضعیف خود با دو مؤلفه، باعث کاهش اتصال کلی طراحی میگردد.
الگوهای Adapter و Delegate و همچنین نقش کنترلر در الگوی معماری MVC از این اصل پیروی میکنند.
class SenderA { public Mediator mediator { get; } public SenderA() { mediator = new Mediator(); } public void Send(string message, string reciever) { mediator.Send(message, reciever); } } class SenderB { public Mediator mediator { get; } public SenderB() { mediator = new Mediator(); } public void Send(string message) { } } public class RecieverA { public void DoAction(string message) { // انجام عملیات بر اساس پیغام دریافت شده switch (message) { case "create": break; case "delete": break; default: break; } } } public class RecieverB { public void DoAction(string message) { // انجام عملیات بر اساس پیغام دریافت شده switch (message) { case "edit": break; case "rollback": break; default: break; } } } class Mediator { internal void Send(string message, string reciever) { switch (reciever) { case "A": var recieverObjA = new RecieverA(); recieverObjA.DoAction(message); break; case "B": var recieverObjB = new RecieverB(); recieverObjB.DoAction(message); break; default: break; } } } class IndirectionContext { public void Main() { var senderA = new SenderA(); senderA.Send("rollback", "B"); var senderB = new SenderA(); senderB.Send("create", "A"); } }
در این مثال کلاس Mediator به عنوان واسط ارتباطی بین کلاسهای Sender و Receiver قرار گرفته و نقش تحویل پیغام را دارد.
در مقاله بعدی، به بررسی سایر اصول GRASP خواهم پرداخت.