using System; using System.Collections.Generic; using System.Linq; using System.Web.Http.Dependencies; using StructureMap; public class StructureMapDependencyResolver : IDependencyResolver { public IDependencyScope BeginScope() { return this; } public object GetService(Type serviceType) { if (serviceType.IsAbstract || serviceType.IsInterface || !serviceType.IsClass) return ObjectFactory.Container.TryGetInstance(serviceType); return ObjectFactory.GetInstance(serviceType); } public IEnumerable<object> GetServices(Type serviceType) { return ObjectFactory.GetAllInstances(serviceType).Cast<object>(); } public void Dispose() { } }
فرض کنید که میخواهیم یک برنامه برای یک فروشگاه نوشیدنی (مانند coffee shop) بنویسیم ، این فروشگاه در ابتدای کار ممکن است ، منوی سادهای جهت ارائه به مشتری داشته باشد. برای مثال ممکن است که فقط 3 یا 4 محصول داشته باشد. بنابراین ممکن است ما برنامهای را که میخواهیم برای این مشتری بنویسیم به صورت زیر طراحی کنیم:
که بسیار طبیعی و درست میباشد. اما حالا در نظر بگیرید که این فروشگاه در آینده ممکن است محصولات خود را افزایش بدهد و یا حالاتی که ممکن است این محصولات با هم ادغام شوند را در نظر بگیرید. برای مثال ممکن است شما بخواهید که قهوهتان را با شیر نوش جان کنید و یا ....
بنابراین تعداد این حالات را در نظر بگیرید که در آینده ممکن است چقدر زیاد بشود:
خوب پس چهکاری ما میتوانیم برای نگهداری این برنامه انجام بدهیم؟ یکی از راههایی که ممکن است به فکر ما برسد این است که روش بالا روش احمقانه ای است. چرا ما باید به همهی این کلاسها نیاز داشته باشیم. ما میتوانیم که چاشنیها را در کلاس اصلی نگهداری کنیم و کلاس محصولاتمان را از کلاس اصلی به ارث ببریم اجازه دهید تا این کار را با هم انجام بدهیم
خوب با این روش ما n کلاس تشکیل شده در رویکرد اول را فقط به 5 کلاس تبدیل کردیم. خوب این روشی بسیار ایدهال به نظر میرسد. اما ممکن است در آینده که تعداد چاشنیهای ما بالا میرود و همچنین تعداد محصولاتمان نیز ممکن است بیشتر شود مجبور شویم که تعداد این کلاسها را بیشتر کنیم، و یا فکر کنید که ما میخواهیم هریک از چاشنیهایمان، یک قیمت را نسبت بدهیم. بنابراین مجبوریم که تمامی اینها را در کلاس پایه اضافه کنیم؛ بله درست است، ما با کلاس پایهی حجیمی روبرو میشویم که بیشتر خواص و یا متدهای آن برای زیر کلاسهای دیگر مناسبت نیستند. خوب آیا روش بهتری برای جلوگیری از این مشکل داریم؟ بلی.
خوب ما به این مسئله به این صورت نگاه میکنیم که شروع میکنیم با نوشیدنیها و آنها را با چاشنیها در زمان اجرا تزیین (Decorate) میکنیم؛ نه کامپایل.
برای مثال اگر مشتری ما یک نوشیدنی DarkRoast با Mocha و Whip خواست، سپس ما :
1- یک شی از DarkRoast ایجاد میکنیم .
2- آن را با یک شی از Mocha تزئین میکنیم.
3- آن را با یک شی از Whip - تزیین میکنیم.
4- متد Cost() را صدا میزنیم و یک Delegation را برای اضافه کردن قیمت چاشنیها در نظر میگیریم.
بسیار خوب؛ اما ما عملیات تزئین یک شی را چگونه انجام میدهیم و delegation ما چگونه عمل میکند .
یک اشاره : به شیء تزئین کننده، مانند یک Wrappers فکر کنید. اجاز بدهید ببینم که چه طور این کار را انجام میدهیم.
1- یک شی از DarkRoast ایجاد میکنیم.
3- آن را با یک شی از Whip تزیین میکنیم
4- حالا زمان محاسبه قیمت محصول برای مشتری فرا رسیده است. ما این کار را را با صدا زدن بیرونیترین Decorator(Whip) انجام میدهیم و شی whip به کمک Delegate مابقی توابع cost را صدا میزند.
public abstract class Beverage { string description ="unknow beverage"; public String getDescription(){ return description; } public abstract double cost(); } public abstract class CondimentDecorator extends Beverage { public abstract string getDescription(); } public class Espersso extends Beverage{ public Espersso() { description="Espersso"; } public double cost(){ return 1.99; } } public class HouseBelend extends Beverage { public HouseBelend() { description="HouseBelend"; } public double cost() { return .89; } } public class mocha extends condimateDecorator { Beverage beverage; public mocha(Beverage beverage) { this.beverage=beverage; } public string getDescription(){ return beverage.getdescription() + "Mocha"; } public double cost(){ return .20 +beverage.cost } } // Now Use These classes in Final form Beverage beverage=new Espersso(); //Customers want a coffe with double milk and whip beverage=new mocha(beverage); beverage=new mocha(meverage); beverage=new whip(beverage); system.out.println(beverage.getDescription() + "$" +beverage.cost());
EF Code First #12
public class MyDbContextBase : DbContext, IUnitOfWork
البته اگر از MVC استفاده کنید این data annotation در سمت کلاینت هم به صورت خودکار اعمال میشود.
اکثر مواقع برنامه نویسان برای بازگشت چند مقدار توسط یک متد، به روشهایی مثل تعریف کلاسهای POCO یا پارامترهایی از نوع out متوسل میشوند. به وسیلهی Tupleها میتوان چند مقدار مختلف را به عنوان خروجی متد بازگشت داد:
private Tuple<string, string, int> GetPersonInfo() { return new Tuple<string, string, int>("Steve", "Jobs", 56); }
عدم نیاز به استفاده از کالکشنهای موقتی در متدها جهت نگهداری مقدار بازگشتی متد
اکثر برنامه نویسان، در متدهایی که لیستی از مقادیر را بازگشت میدهند، از یک متغییر موقتی استفاده میکنند:
private IEnumerable<int> GetNumbers() { var result = new List<int>(); for (int i = 0; i <= 100; i++) result.Add(i); return result; }
private IEnumerable<int> GetNumbers() { for (int i = 0; i <= 100; i++) { yield return i; } }
ملزم کردن نوع پایه یک کلاس Generic به رعایت قوانین پیاده سازی خاص
Genericها میتوانند عملکرد یکسانی را برای نوعهای دادهای متفاوت، پیاده سازی کنند. با توجه به ماهیت Genericها ممکن است در سناریوهایی لازم باشد تا نوع دادهی اولیهای که قرار است Generic پیاده سازی شود، از قوانین پیاده سازی خاصی پیروی کند. به صورت زیر میتوان نوع پایهی یک Generic را ملزم به رعایت قوانین خاص پیاده سازی به واسطه یک Interface کرد:
public interface ICar { string GetName(); string GetManufacturerCompany(); } private class GenricClass<T> where T : ICar { }
public class Audi : ICar { public string GetName() { throw new NotImplementedException(); } public string GetManufacturerCompany() { throw new NotImplementedException(); } } private static void Main(string[] args) { var invalidTest = new GenricClass<int>(); var validTest = new GenricClass<Audi>(); }
namespace TestApp { public static class App { public static string SiteRootUrl; } public class TestApplication : HttpApplication { protected void Application_BeginRequest(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(App.SiteRootUrl)) { App.SiteRootUrl = Request.Url.GetLeftPart(UriPartial.Authority) + Request.ApplicationPath; } } protected void Application_End() { // use App.SiteRootUrl } } }
بررسی آماری واژههای بکار رفته در شاهنامه
مرحله اول: ایجاد ایندکس
using System; using System.Collections.Generic; using System.IO; using Lucene.Net.Analysis.Standard; using Lucene.Net.Documents; using Lucene.Net.Index; using Lucene.Net.Store; namespace ShaahnamehAnalysis { public static class CreateIndex { static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_CURRENT; static HashSet<string> getStopWords() { var result = new HashSet<string>(); var stopWords = new[] { "به", "با", "از", "تا", "و", "است", "هست", "هستم", "هستیم", "هستید", "هستند", "نیست", "نیستم", "نیستیم", "نیستند", "اما", "یا", "این", "آن", "اینجا", "آنجا", "بود", "باد", "برای", "که", "دارم", "داری", "دارد", "داریم", "دارید", "دارند", "چند", "را", "ها", "های", "می", "هم", "در", "باشم", "باشی", "باشد", "باشیم", "باشید", "باشند", "اگر", "مگر", "بجز", "جز", "الا", "اینکه", "چرا", "کی", "چه", "چطور", "چی", "چیست", "آیا", "چنین", "اینچنین", "نخست", "اول", "آخر", "انتها", "صد", "هزار", "میلیون", "ملیون", "میلیارد", "ملیارد", "یکهزار", "تریلیون", "تریلیارد", "میان", "بین", "زیر", "بیش", "روی", "ضمن", "همانا", "ای", "بعد", "پس", "قبل", "پیش", "هیچ", "همه", "واما", "شد", "شده", "شدم", "شدی", "شدیم", "شدند", "یک", "یکی", "نبود", "میکند", "میکنم", "میکنیم", "میکنید", "میکنند", "میکنی", "طور", "اینطور", "آنطور", "هر", "حال", "مثل", "خواهم", "خواهی", "خواهد", "خواهیم", "خواهید", "خواهند", "داشته", "داشت", "داشتی", "داشتم", "داشتیم", "داشتید", "داشتند", "آنکه", "مورد", "کنید", "کنم", "کنی", "کنند", "کنیم", "نکنم", "نکنی", "نکند", "نکنیم", "نکنید", "نکنند", "نکن", "بگو", "نگو", "مگو", "بنابراین", "بدین", "من", "تو", "او", "ما", "شما", "ایشان", "ی", "ـ", "هایی", "خیلی", "بسیار", "1", "بر", "l", "شود", "کرد", "کرده", "نیز", "خود", "شوند", "اند", "داد", "دهد", "گشت", "ز", "گفت", "آمد", "اندر", "چون", "بد", "چو", "همی", "پر", "سوی", "دو", "گر", "بی", "گرد", "زین", "کس", "زان", "جای", "آید" }; foreach (var item in stopWords) result.Add(item); return result; } public static void CreateShaahnamehIndex(string file = "shaahnameh.txt") { var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex")); var analyzer = new StandardAnalyzer(_version, getStopWords()); using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED)) { var section = string.Empty; foreach (var line in File.ReadAllLines(file)) { int result; if (int.TryParse(line, out result)) { var postDocument = new Document(); postDocument.Add(new Field("Id", result.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); postDocument.Add(new Field("Body", section, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS)); writer.AddDocument(postDocument); section = string.Empty; } else section += line; } writer.Optimize(); writer.Commit(); writer.Close(); directory.Close(); } } } }
با ایجاد ایندکسهای لوسین پیشتر در این سایت آشنا شدهاید . روش کار نیز همانند سابق است. اطلاعات خود را، به هر فرمتی که تهیه شده باید تبدیل به اشیاء Document لوسین کرد. برای مثال در اینجا فقط یک فایل txt داریم که تشکیل شده است از تمام صفحات. به ازای هر صفحه، یک شیء Document تهیه و نوشته خواهد شد. همچنین در تهیه ایندکس از یک سری از واژههای بسیار متداول مانند «از»، «به»، «اندر»، (stopWords) صرفنظر شده است.
مرحله دوم: ایجاد ابر واژهها
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Lucene.Net.Index; using Lucene.Net.Store; namespace ShaahnamehAnalysis { [DebuggerDisplay("{Frequency}, {Text}")] public class Tag { public string Text { set; get; } /// <summary> /// The frequency of a term is defined as the number of /// documents in which a specific term appears. /// </summary> public int Frequency { set; get; } } public static class WordsCloud { /// <summary> /// Create Words Cloud /// </summary> /// <param name="threshold">every term that appears in more than x Body</param> public static IList<Tag> Create(int threshold = 200) { var path = Environment.CurrentDirectory + "\\LuceneIndex"; var results = new List<Tag>(); var field = "Body"; IndexReader indexReader = IndexReader.Open(FSDirectory.Open(path ), true); var termFrequency = indexReader.Terms(); while (termFrequency.Next()) { if (termFrequency.DocFreq() >= threshold && termFrequency.Term.Field == field) { results.Add(new Tag { Text = termFrequency.Term.Text, Frequency = termFrequency.DocFreq() }); } } return results.OrderByDescending(x => x.Frequency).ToList(); } } }
پس از اینکه ایندکس لوسین تهیه شد، میتوان به مداخل موجود در آن توسط متد indexReader.Terms دسترسی یافت.
نکته جالب آن فراهم بودن DocFreq هر واژه ایندکس شده است (فرکانس تکرار واژه؛ تعداد اشیاء Document ایی که واژه مورد نظر در آنها تکرار شده است). برای مثال در اینجا اگر واژهای 200 بار یا بیشتر در صفحات مختلف شاهنامه تکرار شده باشد، به عنوان یک واژه پر اهمیت انتخاب شده و به ابر واژههای نهایی اضافه میگردد.
مرحله سوم: استفاده از نتایج
using System; using System.Diagnostics; using System.IO; using System.Linq; namespace ShaahnamehAnalysis { class Program { static void Main(string[] args) { CreateIndex.CreateShaahnamehIndex(); var wordsCloudList = WordsCloud.Create(); var data = wordsCloudList.Select(x => x.Text + ", " + x.Frequency) .Aggregate((s1, s2) => s1 + Environment.NewLine + s2); var output = "ShaahnamehAnalysis.txt"; File.WriteAllText(output, data); Process.Start(output); } } }
که نتیجه 15 مورد اول آن به صورت زیر است:
واژه | فرکانس
شاه, 1191
دل, 1088
سر, 1070
کار, 840
لشکر, 801
تخت, 755
روز, 745
ایران, 740
جهان, 724
مرد, 660
دست, 630
تاج, 623
نزدیک, 623
گیتی, 585
راه, 584
فایلهای کامل این مثال را از اینجا میتوانید دریافت کنید:
ShaahnamehAnalysis.zip
ابتدا مثال کامل ذیل را در نظر بگیرید:
using System; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.Migrations; using System.Linq; using System.Linq.Expressions; namespace Sample { public abstract class BaseEntity { public int Id { set; get; } } public class Receipt : BaseEntity { public int TotalPrice { set; get; } } public class MyContext : DbContext { public DbSet<Receipt> Receipts { get; set; } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { if (!context.Receipts.Any()) { for (int i = 0; i < 20; i++) { context.Receipts.Add(new Receipt { TotalPrice = i }); } } base.Seed(context); } } public static class EFUtils { public static IList<T> LoadEntities<T>(this DbContext ctx, Expression<Func<T, bool>> predicate) where T : class { return ctx.Set<T>().Where(predicate).ToList(); } public static IList<T> LoadData<T>(this DbContext ctx, Func<T, bool> predicate) where T : class { return ctx.Set<T>().Where(predicate).ToList(); } } public static class Test { public static void RunTests() { startDB(); using (var context = new MyContext()) { var list1 = context.LoadEntities<Receipt>(x => x.TotalPrice == 10); var list2 = context.LoadData<Receipt>(x => x.TotalPrice == 20); } } private static void startDB() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>()); // Forces initialization of database on model changes. using (var context = new MyContext()) { context.Database.Initialize(force: true); } } } }
نکته اصلی مورد بحث، کلاس کمکی EFUtils است که در آن دو متد الحاقی LoadEntities و LoadData تعریف شدهاند. در متد LoadEntities، امضای متد شامل Expression Func است و در متد LoadData فقط Func ذکر شده است.
در ادامه اگر برنامه را توسط فراخوانی متد RunTests اجرا کنیم، به نظر شما خروجی SQL حاصل از list1 و list2 چیست؟
احتمالا شاید عنوان کنید که هر دو یک خروجی SQL دارند (با توجه به اینکه بدنه متدهای LoadEntities و LoadData دقیقا/یا به نظر یکی هستند) اما یکی از پارامتر 10 استفاده میکند و دیگری از پارامتر 20. تفاوت دیگری ندارند.
اما ... اینطور نیست!
خروجی SQL متد LoadEntities در متد RunTests به صورت زیر است:
SELECT [Extent1].[Id] AS [Id], [Extent1].[TotalPrice] AS [TotalPrice] FROM [dbo].[Receipts] AS [Extent1] WHERE 10 = [Extent1].[TotalPrice]
SELECT [Extent1].[Id] AS [Id], [Extent1].[TotalPrice] AS [TotalPrice] FROM [dbo].[Receipts] AS [Extent1]
چرا؟
Func اشارهگری است به یک متد و Expression Func بیانگر ساختار درختی عبارت lambda نوشته شده است. این ساختار درختی صرفا بیان میکند که عبارت lambda منتسب، چه کاری را قرار است یا میتواند انجام دهد؛ بجای انجام واقعی آن.
public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate); public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
بنابراین هرچند بدنه دو متد LoadEntities و LoadData به ظاهر یکی هستند، اما بر اساس نوع ورودی Where ایی که دریافت میکنند، اگر Expression Func باشد، EF فرصت آنالیز و ترجمه عبارت ورودی را خواهد یافت اما اگر Func باشد، ابتدا باید کل اطلاعات را به صورت یک لیست IEnumerable دریافت و سپس سمت کلاینت، خروجی نهایی را فیلتر کند.
اگر برنامه را اجرا کنید نهایتا هر دو لیست یک و دو، بر اساس شرط عنوان شده عمل خواهند کرد و فیلتر خواهند شد. اما در حالت اول این فیلتر شدن سمت بانک اطلاعاتی است و در حالت دوم کل اطلاعات بارگذاری شده و سپس سمت کاربر فیلتر میشود (که کارآیی پایینی دارد).
نتیجه گیری
به امضای متد Where ایی که در حال استفاده است دقت کنید. همینطور در مورد Sum ، Count و یا موارد مشابه دیگری که predicate قبول میکنند.
public class Order { public decimal Price() { var primaryBasePrice = 10000; var branchDiscountBasePrice = 9000; var taxPercentage = 9; // calculate price } }
public class Order { public decimal Price() { return new PriceCalculator(this).Compute(); } } public class PriceCalculator { private Order orderInstance ; public PriceCalculator(Order order) { orderInstance = order; } public decimal Compute() { var primaryBasePrice = 10000; var branchDiscountBasePrice = 9000; var taxPercentage = 9; // calculate price } }
مراحل انجام این بازسازی کد
- کلاس جدیدی بسازید و نام آن را بر اساس نام متد مورد نظر خود انتخاب نمایید.
- به کلاس ساخته شده فیلدی برای نگه داری شیء کلاس مرجع (در مثال ما Order) متد اصلی ایجاد کنید.
- به کلاس جدید سازندهای برای پاس دادن شیء کلاس مرجع (در مثال ما Order) ایجاد کنید.
- در کلاس جدید متدی با نام مناسب (در مثال ما Compute) ایجاد کنید.
- بدنه متد مورد نظر را به بدنه متد ایجاد شده در کلاس جدید کپی کنید و تمامی رفرنسها به اعضای کلاس مرجع را با رفرنس به شیء موجود آن تغییر دهید.
- کد را کامپایل کنید.
- فراخوانی متد اصلی را با فراخوانی متد شیء کلاس جدید ایجاد شده تغییر دهید.
چرا به ابزارهای مدیریت حالت نیاز داریم؟
به محض رد شدن از مرز پیاده سازی امکانات اولیهی یک برنامه، نیاز به ابزارهای مدیریت حالت نمایان میشوند؛ خصوصا زمانیکه نیاز است با اطلاعات قابل توجهی سر و کار داشت. مهمترین دلیل استفادهی از یک ابزار مدیریت حالت، مدیریت منطق تجاری برنامه است. منطق نمایشی برنامه مرتبط است به نحوهی نمایش اجزای آن در صفحه؛ مانند نمایش یک صفحهی مودال، تغییر رنگ عناصر با عبور کرسر ماوس از روی آنها و در کل منطقی که مرتبط و یا وابستهی به هدف اصلی برنامه نیست. از سوی دیگر منطق تجاری برنامه مرتبط است با مدیریت، تغییر و ذخیره سازی اشیاء تجاری مورد نیاز آن؛ مانند اطلاعات حساب کاربری شخص و دریافت اطلاعات برنامه از یک API که مختص به برنامهی خاص ما است و به همین دلیل نیاز به ابزاری برای مدیریت بهینهی آن وجود دارد. برای مثال اینکه در کجا باید منطق تجاری و نمایشی را به هم متصل کرد، میتواند چالش بر انگیر باشد. چگونه باید اطلاعات کاربر را ذخیره کرد؟ چگونه React باید متوجه شود که اطلاعات ما تغییر کردهاست و در نتیجهی آن کامپوننتی را مجددا رندر کند؟ یک ابزار مدیریت حالت، تمام این مسایل را به نحو یکدستی در سراسر برنامه، مدیریت میکند.
اگر از یک ابزار مدیریت حالت استفاده نکنیم، مجبور خواهیم شد تمام اطلاعات منطق تجاری را در داخل state کامپوننتها ذخیره کنیم که توصیه نمیشود؛ چون مقیاس پذیر نیست. برای مثال فرض کنید قرار است تمام اطلاعات state را داخل یک کامپوننت ذخیره کنیم. هر زمانیکه بخواهیم این state را از طریق یک کامپوننت فرزند تغییر دهیم، نیاز خواهد بود این اطلاعات را به والد آن کامپوننت ارسال کنیم که اگر از تعداد زیادی کامپوننت تو در تو تشکیل شده باشد، زمانبر و به همراه کدهای تکراری زیادی خواهد بود. همچنین اینکار سبب رندر مجدد کل برنامه با هر تغییری در state آن میشود که غیرضروری بوده و کارآیی برنامه را کاهش میدهد. به علاوه در این بین مشخص نیست هر قسمت از state، از کدام کامپوننت تامین شدهاست. به همین جهت نیاز به روشی برای مدیریت حالت در بین کامپوننتهای برنامه وجود دارد.
داشتن تنها یک محل برای ذخیره سازی state در برنامه
همانطور که در قسمت 8 ترکیب کامپوننتها در سری React 16x بررسی کردیم، هر کامپوننت در React، دارای state خاص خودش است و این state از سایر کامپوننتها کاملا مستقل و ایزولهاست. این مورد با بزرگتر شدن برنامه و برقراری ارتباط بین کامپوننتها، مشکل ایجاد میکند. برای مثال اگر بخواهیم دکمهای را در صفحه قرار داده و توسط این دکمه درخواست صفر شدن مقدار هر کدام از شمارشگرها را صادر کنیم، با صفر کردن value هر کدام از این کامپوننتها، اتفاقی رخ نمیدهد. چون state محلی این کامپوننتها، با سایر اجزای صفحه به اشتراک گذاشته نمیشود و باید آنرا تبدیل به یک controlled component کرد، بطوریکه دارای local state خاص خودش نیست و تمام دادههای دریافتی را از طریق this.props دریافت میکند و هر زمانیکه قرار است دادهای تغییر کند، رخدادی را به والد خود صادر میکند. بنابراین این کامپوننت به طور کامل توسط والد آن کنترل میشود. تازه این روش در مورد کامپوننتهایی صدق میکند که رابطهی والد و فرزندی بین آنها وجود دارد. اگر چنین رابطهای وجود نداشت، باید state را به یک سطح بالاتر انتقال داد. برای مثال باید state کامپوننت Counters را به والد آن که کامپوننت App است، منتقل کرد. پس از آن چون کامپوننتهای ما، از کامپوننت App مشتق میشوند، اکنون میتوان این state را به تمام فرزندان App توسط props منتقل کرد و به اشتراک گذاشت. این مورد هم مانند مثال انتقال اطلاعات کاربر لاگین شدهی به سیستم، به تمام زیر قسمتهای برنامه، نیاز به ارسال اطلاعات از طریق props یک کامپوننت، به کامپوننت بعدی را دارد و به همین ترتیب برای مابقی که به props drilling مشهور است و روش پسندیدهای نیست.
Redux چیست؟ ذخیره سازی کل درخت state یک برنامه، در یک محل. به این ترتیب به یک شیء جاوا اسکریپتی بزرگ خواهیم رسید که در برگیرندهی تمام state برنامهاست. یکی از مزایای آن امکان serialize و deserialize کل این شیء، به سادگی است. برای مثال توسط متد JSON.stringify میتوان آنرا در جائی ذخیره کرد و سپس آنرا به صورت یک شیء جاو اسکریپتی در زمانی دیگر بازیابی کرد. یکی از مزایای آن، امکان بازیابی دقیق شرایط کاربری است که دچار مشکل شدهاست و سپس دیباگ و رفع مشکل او، در زمانی دیگر.
تاریخچهای از سیستمهای مدیریت حالت
همه چیز با AngularJS 1x شروع شد که از data binding دو طرفه پشتیبانی میکرد. هرچند این روش برای همگام نگه داشتن View و مدل برنامه، مفید است، اما در Viewهای پیچیده، برنامه را کند میکند. در همین زمان فیسبوک، روش مدیریت حالتی را به نام Flux ارائه داد که از data binding یک طرفه پشتیبانی میکرد. به این معنا که در این روش، همواره اطلاعات از View به مدل، جریان پیدا میکند. کار کردن با آن سادهاست؛ چون نیازی نیست حدس زده شود که اکنون جریان اطلاعات از کدام سمت است. اما مشکل آن عدم هماهنگی model و view، در بعضی از حالات است. Flux از این جهت به وجود آمد که مدیریت حالت در برنامههای React آن زمان، پیچیده بود و مقیاس پذیری کمی داشت (پیش از ارائهی Context و Hooks). در کل Flux صرفا یکسری الگوی مدیریت حالت را بیان میکند و یک کتابخانهی مجزا نیست. بر مبنای این الگوها و قراردادها، میتوان کتابخانههای مختلفی را ایجاد کرد. از این رو در سال 2015، کتابخانههای زیادی مانند Reflux, Flummox, MartyJS, Alt, Redux و غیره برای پیاده سازی آن پدید آمدند. در این بین، کتابخانهی Redux ماندگار شد و پیروز این نبرد بود!
توابع خالص و ناخالص (Pure & Impure Functions)
پیش از شروع بحث، نیاز است با یکسری از واژهها مانند توابع خالص و ناخالص آشنا شد. این نکات از این جهت مهم هستند که Redux فقط با توابع خالص کار میکند.
توابع خالص: تعدادی آرگومان را دریافت کرده و بر اساس آنها، مقداری را باز میگردانند.
// Pure const add = (a, b) => { return a + b; }
توابع ناخالص: این نوع توابع سبب تغییراتی در متغیرهایی خارج از میدان دید خود میشوند و یا به همراه یک سری اثرات جانبی (side effects) مانند تعامل با دنیای خارج (وجود یک console.log در آن تابع و یا دریافت اطلاعاتی از یک API خارجی) هستند.
// Impure const b; const add = (a) => { return a + b; }
// Impure const add = (a, b) => { console.log('lolololol'); return a + b; }
// Impure const add = (a, b) => { Api.post('/add', { a, b }, (response) => { // Do something. }); };
روشهایی برای جلوگیری از تغییرات در اشیاء در جاوا اسکریپت
ایجاد تغییرات در آرایهها و اشیاء (Mutating arrays and objects) نیز ناخالصی ایجاد میکند؛ از این جهت که سبب تغییراتی در دنیای خارج (خارج از میدان دید تابع) میشویم. به همین جهت نیاز به روشهایی وجود دارد که از این نوع تغییرات جلوگیری کرد:
// Copy object const original = { a: 1, b: 2 }; const copy = Object.assign({}, original);
برای مثال در React، برای انجام رندر نهایی، در پشت صحنه کار مقایسهی اشیاء صورت میگیرد. به همین جهت اگر همان شیءای را که ردیابی میکند تغییر دهیم، دیگر نمیتواند به صورت مؤثری فقط قسمتهای تغییر کردهی آنرا تشخیص داده و کار رندر را فقط بر اساس آنها انجام دهد و مجبور خواهد شد کل یک شیء را بارها و بارها رندر کند که اصلا بهینه نیست. به همین جهت، ایجاد تغییرات مستقیم در شیءای که به state آن انتساب داده میشود، مجاز نیست.
متد Object.assign، چندین شیء را نیز میتواند با هم یکی کند و شیء جدیدی را تشکیل دهد:
// Extend object const original = { a: 1, b: 2 }; const extension = { c: 3 }; const extended = Object.assign({}, original, extension);
// Copy object const original = { a: 1, b: 2 }; const copy = { ...original };
// Extend object const original = { a: 1, b: 2 }; const extension = { c: 3 }; const extended = { ...original, ...extension };
روشهایی برای جلوگیری از تغییرات در آرایهها در جاوا اسکریپت
متد slice آرایهها نیز بدون ذکر آرگومانی، یک کپی از آرایهی اصلی را ایجاد میکند:
// Copy array const original = [1, 2, 3]; const copy = [1, 2, 3].slice();
// Copy array const original = [1, 2, 3]; const copy = [ ...original ];
// Extend array const original = [1, 2, 3]; const extended = original.concat(4); const moreExtended = original.concat([4, 5]);
معادل قطعه کد فوق در ES-6 و به همراه spread operator آن به صورت زیر است:
// Extend array const original = [1, 2, 3]; const extended = [ ...original, 4 ]; const moreExtended = [ ...original, ...extended, 5 ];
مفاهیم ابتدایی Redux
در Redux برای ایجاد تغییرات در شیء کلی state، از مفهومی به نام dispatch actions استفاده میشود. action در اینجا به معنای رخدادن چیزی است؛ مانند کلیک بر روی یک دکمه و یا دریافت اطلاعاتی از یک API. در این حالت مقایسهای بین وضعیت قبلی state و وضعیت فعلی آن صورت میگیرد و تغییرات مورد نیاز جهت اعمال به UI، محاسبه خواهند شد.
اصلیترین جزء Redux، تابعی است به نام Reducer. این تابع، یک تابع خالص است و دو آرگومان را دریافت میکند:
تابع Reducer، بر اساس action و یا رخدادی، ابتدا کل state برنامه را دریافت میکند و سپس خروجی آن بر اساس منطق این تابع، یک state جدید خواهد بود. اکنون که این state جدید را داریم، برنامهی React ما میتواند به تغییرات آن گوش فرا داده و بر اساس آن، UI را به روز رسانی کند. به این ترتیب کار اصلی مدیریت state، به خارج از برنامهی React منتقل میشود.
در این تصویر، تابع action creator را هم ملاحظه میکند که کاملا اختیاری است. یک action میتواند یک رشته و یا یک عدد باشد. با پیچیده شدن برنامه، نیاز به ارسال یکسری متادیتا و یا اطلاعات بیشتری از اکشن رسیدهاست. کار action creator، ایجاد شیء action، به صورت یک دست و یکنواخت است تا دیگر نیازی به ایجاد دستی آن نباشد.
مزایای کار با Redux
- داشتن یک مکان مرکزی برای ذخیره سازی کلی حالت برنامه (به آن «source of truth» و یا store هم گفته میشود): به این ترتیب مشکل ارسال خواص در بین کامپوننتهای عمیق و چند سطحی، برطرف شده و هر زمانیکه نیاز بود، از آن اطلاعاتی را دریافت و یا با قالب خاصی، آنرا به روز رسانی میکنند.
- رسیدن به بهروز رسانیهای قابل پیش بینی state: هرچند در حالت کار با Redux، یک شیء بزرگ جاوا اسکریپتی، کل state برنامه را تشکیل میدهد، اما امکان کار مستقیم با آن و تغییرش وجود ندارد. به همین جهت است که برای کار با آن، باید رویدادی را از طریق actionها به تابع Reducer آن تحویل داد. چون Reducer یک تابع خالص است، با دریافت یک سری ورودی مشخص، همواره یک خروجی مشخص را نیز تولید میکند. به همین جهت قابلیت ضبط و تکرار را پیدا میکند؛ همان بحث serialize و deseriliaze، توسط ابزاری مانند: logrocket. به علاوه قابلیت undo و redo را نیز میتوان به این ترتیب پیاده سازی کرد (state جدید محاسبه شده، مشخص است، کل state قبلی را نیز داریم یا میتوان ذخیره کرد و سپس برای undo، آنرا جایگزین state جدید نمود). افزونهی redux dev tools نیز قابلیت import و export کل state را به همراه دارد.
- چون تابع Reducer، یک تابع خالص است و همواره خروجیهای مشخصی را به ازای ورودیهای مشخصی، تولید میکند، آزمایش کردن، پیاده سازی و حتی logging آن نیز سادهتر است. در این بین حتی یک افزونهی مخصوص نیز برای دیباگ آن تهیه شدهاست: redux-devtools-extension. تابع خالص، تابعی است که به همراه اثرات جانبی نیست (side effects)؛ به همین جهت عملکرد آن کاملا قابل پیش بینی بوده و آزمون پذیری آن به دلیل نداشتن وابستگیهای خارجی، بسیار بالا است.
Context API خود React چطور؟
در قسمت 33 سری React 16x، مفهوم React Context را بررسی کردیم. پس از معرفی آن با React 16.3، مقالات زیادی منتشر شدند که ... Redux مردهاست (!) و یا بجای Redux از React context استفاده کنید. اما واقعیت این است که React Redux در پشت صحنه از React context استفاده میکند و تابع connect آن دقیقا به همین زیر ساخت متصل میشود.
کار با Redux مزایایی مانند کارآیی بالاتر، با کاهش رندرهای مجدد کامپوننتها، دیباگ سادهتر با افزونههای اختصاصی و همچنین سفارشی سازی، مانند نوشتن میانافزارها را به همراه دارد. اما شاید واقعا نیازی به تمام این امکانات را هم نداشته باشید؛ اگر هدف، صرفا انتقال سادهتر اطلاعات بوده و برنامهی مدنظر نیز کوچک است. React Context برخلاف Redux، نگهدارندهی state نیست و بیشتر هدفش محلی برای ذخیره سازی اطلاعات مورد استفادهی در چندین و چند کامپوننت تو در تو است. هرچند شبیه به Redux میتوان اشارهگرهایی از متدها را به استفاده کنندگان از آن ارسال کرد تا سبب بروز رویدادها و اکشنهایی در کامپوننت تامین کنندهی Contrext شوند (یا یک کتابخانهی ابتدایی شبیه به Redux را توسط آن تهیه کرد). بنابراین برای انتخاب بین React Context و Redux باید به اندازهی برنامه، تعداد نفرات تیم، آشنایی آنها با مفاهیم Redux دقت داشت.
PM> Install-Package snap.structuremap
StructureMap (≥ 2.6.4.1) CommonServiceLocator.StructureMapAdapter (≥ 1.1.0.3) SNAP (≥ 1.8) fasterflect (≥ 2.1.2) Castle.Core (≥ 3.1.0) CommonServiceLocator (≥ 1.0)
تنظیمات SNAP
namespace Framework.UI.Asp { public class Global : HttpApplication { void Application_Start(object sender, EventArgs e) { initSnap(); initStructureMap(); } private static void initSnap() { SnapConfiguration.For<StructureMapAspectContainer>(c => { // Tell Snap to intercept types under the "Framework.ServiceLayer..." namespace. c.IncludeNamespace("Framework.ServiceLayer.*"); // Register a custom interceptor (a.k.a. an aspect). c.Bind<Framework.ServiceLayer.Aspects.AuthorizationInterceptor>() .To<Framework.ServiceLayer.Aspects.AuthorizationAttribute>(); }); } void Application_EndRequest(object sender, EventArgs e) { ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects(); } private static void initStructureMap() { var thread = StructureMap.Pipeline.Lifecycles.GetLifecycle(InstanceScope.HttpSession); ObjectFactory.Configure(x => { x.For<IUserManager>().Use<EFUserManager>(); x.For<IAuthorizationManager>().LifecycleIs(thread) .Use<EFAuthorizationManager>().Named("_AuthorizationManager"); x.For<Framework.DataLayer.IUnitOfWork>() .Use<Framework.DataLayer.Context>(); x.SetAllProperties(y => { y.OfType<IUserManager>(); y.OfType<Framework.DataLayer.IUnitOfWork>(); y.OfType<Framework.Common.Web.IPageHelpers>(); }); }); } } }
namespace Framework.ServiceLayer.Aspects { public class AuthorizationInterceptor : MethodInterceptor { public override void InterceptMethod(IInvocation invocation, MethodBase method, Attribute attribute) { var AuthManager = StructureMap.ObjectFactory .GetInstance<Framework.ServiceLayer.UserManager.IAuthorizationManager>(); var FullName = GetMethodFullName(method); if (!AuthManager.IsActionAuthorized(FullName)) throw new Common.Exceptions.UnauthorizedAccessException(""); invocation.Proceed(); // the underlying method call } private static string GetMethodFullName(MethodBase method) { var TypeName = (((System.Reflection.MemberInfo)(method)).DeclaringType).FullName; return TypeName + "." + method.Name; } } public class AuthorizationAttribute : MethodInterceptAttribute { }
namespace Snap { public abstract class MethodInterceptor : IAttributeInterceptor, IInterceptor, IHideBaseTypes { protected MethodInterceptor(); public int Order { get; set; } public Type TargetAttribute { get; set; } public virtual void AfterInvocation(); public virtual void BeforeInvocation(); public void Intercept(IInvocation invocation); public abstract void InterceptMethod(IInvocation invocation, MethodBase method, Attribute attribute); public bool ShouldIntercept(IInvocation invocation); } }
یک نکته
private void Application_PreRequestHandlerExecute(object source, EventArgs e) { var page = HttpContext.Current.Handler as BasePage; // The Page handler if (page == null) return; WireUpThePage(page); WireUpAllUserControls(page); var UsrCod = HttpContext.Current.Session["UsrCod"]; if (UsrCod != null) { var _AuthorizationManager = ObjectFactory .GetNamedInstance<Framework.ServiceLayer.UserManager.IAuthorizationManager>("_AuthorizationManager"); ((Framework.ServiceLayer.UserManager.EFAuthorizationManager)_AuthorizationManager) .AuditUserId = UsrCod.ToString(); } }
namespace Framework.ServiceLayer.UserManager { public class EFUserManager : IUserManager { IUnitOfWork _uow; IDbSet<User> _users; public EFUserManager(IUnitOfWork uow) { _uow = uow; _users = _uow.Set<User>(); } [Framework.ServiceLayer.Aspects.Authorization] public List<User> GetAll() { return _users.ToList<User>(); } } }