بررسی تازههای TypeScript 3.0
انتشار TypeScript 1.5 Alpha
Exception و خوانایی کد
تکه کد زیر را در نظر بگیرید: یک Action معمولی در Asp.Net MVC که یک نام را دریافت کرده و یک کارمندرا ایجاد میکند:
public ActionResult CreateEmployee(string name) { try { ValidateName(name); // ادامه کدها return View("با موفقیت ثبت شد"); } catch (ValidationException ex) { return View("خطا", ex.Message); } } private void ValidateName(string name) { if (string.IsNullOrWhiteSpace(name)) throw new ValidationException("نام نمیتواند خالی باشد"); if (name.Length > 100) throw new ValidationException("نام نمیتواند طولانی باشد"); }
در این قطعه کد، در متد ValidateName، در صورت معتبر نبودن ورودی، یک Exception رخ میدهد و بلاک کد try/catch، این exception را دریافت کرده و خطای مناسبی را به کاربر نشان خواهد داد. تا اینجا ظاهرا همه چیز مرتب است و مشکلی ندارد! احتمالا کدهای مشابه به این کد را زیاد دیدهاید. در اینجا متد ValidateName، صادق نیست. در قسمت اول، در مورد Honesty صحبت کردیم. به عبارت سادهتر شما از امضای این متد نمیتوانید به نوع خروجی و کاری که قرار است انجام دهد، پی ببرید. در واقع شما همیشه باید پیاده سازی متد را گوشهای، در ذهن خود داشته باشید و برای اطمینان از کاری که متد انجام میدهد، همیشه باید به بدنهی متد برگردیم. اگر بهخاطر داشته باشید، توابع برنامه نویسی را به توابع ریاضی تشبیه کردیم. پس میتوانیم بگوییم:
به عبارت دیگر وقتی از exceptionها برای کنترل flow برنامه استفاده میکنید، مشابه کاری را انجام میدهید که دستور GOTO انجام میداد. این دستور در روشهای قبل از برنامه نویسی ساخت یافته وجود داشت و توسط یک دانشمند هلندی به نام آقای دیکسترا حذف شد. وقتی از دستور GOTO یا JUMP استفاده میکنیم، فهمیدن flow برنامه پیچیدگیهای زیادی را خواهد داشت. چراکه فراخوانی قطعههای کد و متدها، وابستگی شدیدی خواهند داشت و البته میتوان گفت استفاده از exceptionها برای کنترل جریان برنامه، میتوانند از GOTO هم بدتر باشند؛ چرا که exception میتواند از لایههای مختلف کد نیز عبور کند.
امیدوارم تا اینجا به یک عقیدهی مشترک رسیده باشیم. خوب راهکار چیست؟ تصور کنید که تکه کد بالا را به صورت زیر تبدیل کنیم:
public ActionResult CreateEmployee(string name) { string error = ValidateName(name); if (error != string.Empty) return View("خطا", error); // ادامه کدها return View("با موفقیت ثبت شد"); } private string ValidateName(string name) { if (string.IsNullOrWhiteSpace(name)) return "نام نمیتواند خالی باشد"; if (name.Length > 100) return "طول نام نمیتواند بیشتر از 100 کاراکتر باشد"; return string.Empty; }
با refactor ای که انجام دادیم، متد ValidateName را به یک تابع ریاضی تبدیل کردیم. به این معنا که هر آنچه را که از امضای متد، مشخص است، انجام میدهد و در این حالت چیزی مخفی نیست. توجه داشته باشید که این راهکار نهایی ما نیست و لطفا مقاله را تا انتها بخوانید!
موارد استفاده Exception
با همهی بدیهایی که از Exceptionها گفتیم، با این حساب پس چه زمانی از آن استفاده کنیم؟
- Exceptionها واقعا برای موارد استثنائی هستند.
- Exceptionها برای شرایطی هستند که به معنای واقعی یک باگ باشند.
- منتظر رخ دادن Exception نباشیم!
در توضیح مورد سوم، در اعتبار سنجی دادههای کاربر (Validation) انتظار دادهی نادرستی را میتوان داشت، پس نمیتوانیم آن را یک حالت استثنایی بدانیم. معماری زیر را در نظر بگیرید
دیتایی که به API ما ارسال خواهد شد، همیشه شامل عملیات Filter یا به عبارتی Validation است و از آنجایی که میتوان انتظار استفادهی نادرست یا دیتای نادرست را داشت، نمیتوانیم این را حالتی از استثنائات در نظر بگیریم؛ ولی بر خلاف آن، وقتی در دامین پروژه و ارتباط بین دامینهای مختلف، دیتایی رد و بدل میشود که معتبر نیست، میتوانیم آن را جزء استثناءها در نظر بگیریم. به مثال زیر دقت کنید:
public ActionResult UpdateEmployee(int employeeId, string name) { string error = ValidateName(name); if (error != string.Empty) return View("Error", error); Employee employee = GetEmployee(employeeId); employee.UpdateName(name); } public class Employee { public void UpdateName(string name){ if (name == null) throw new ArgumentNullException(); // ادامه کدها } }
در قطعه کد بالا تصور این است که کلاس Employee و متد UpdateName خارج از دامین میباشند. همانطورکه مشاهده میکنید، ما در action controller، از خالی نبودن نام اطمینان حاصل کردیم و سپس آن را به متد UpdateName ارجاع دادیم. ولی اگه به بدنهی متد UpdateName دقت کنید، میبینید که مجددا از خالی نبودن نام اطمینان حاصل کردهایم و در صورت خالی بودن، یک Exception را صادر میکنیم! به این مدل چک کردنها در دامینهای مختلف، معمولا guard clause گفته میشود و یک نوع قرارداد بین برنامه نویس هاست. اگر طبق تعریفی که بالاتر ارائه کردیم هم چک کنیم، میتوانیم حدس بزنیم که خالی بودن نام، نشان یک باگ در نرم افزار است!
مفهوم fail fast
تا اینجا متوجه شدیم که از exceptionها باید در شرایط استثنائی استفاده کنیم. خوب با توجه به این مساله، چه طور میتوانیم آنها را Handle کنیم؟ این سؤال ما را به مفهومی به نام fail fast میرساند. این مفهوم به ما میگوید:
- کار جاری را به محض یک اتفاق استثنائی باید متوقف کنیم.
- رعایت این نکته در نهایت ما را به یک نرم افزار پایدار خواهد رساند.
برای درک هر چه بهتر این موضوع، بیایید به عکس این حالت نگاه کنیم؛ اصطلاحا Fail Silently.
متد زیر را ببینید:
public void ProcessItems(List<Item> items) { foreach (Item item in items) { try { Process(item); } catch (Exception ex) { Logger.Log(ex); } } }
در قطعه کد بالا، در نگاه اول احتمالا حس نرم افزار پایدارتر و بدون خطا را خواهیم داشت. اما در واقع اینطور نیست. احتمال اینکه خطا از چشم برنامه نویس به دور باشد و بعد از اجرا باعث شود که یکپارچگی دادهها را به هم بریزد وجود دارد. در واقع هیچ راهی برای زمانیکه این عملیات نباید انجام شود، در نظر گرفته نشدهاست. طبق صحبتهایی که بالاتر داشتیم، شرایط غیر منتظره، در واقع یک باگ در نرم افزار است و هیچ مزیتی در جلوگیری از وقوع این باگ بدون حل مشکل نیست!
به صور خلاصه مهمترین مزیت Fail Fast را میتوانیم به صورت زیر خلاصه کنیم:
- مسیر رسیدن به خطاها سر راستتر میشود.
- نرم افزار به پایداری مناسبی خواهد رسید.
- از اعتبار دیتای ذخیره شده اطمینان خواهیم داشت.
کجا exceptionها را به دام بیندازیم؟
در یکی از حالتهای زیر:
- لاگ کردن
- متوقف کردن عملیات
- هیچ گاه در بلاک catch هیچ منطقی را پیاده نکنید.
حالت دیگر در استفاه از کتابخانههای دیگران (3rd parties) است. به طور مثال در استفاده از EF ممکن است به دلیل عدم برقراری ارتباط با دیتابیس، خطایی را دریافت کنید. در این حالت با توجه به نکات فوق، با این استثنائات برخورد کنید:
- جلوی این نوع استثنائات را در پایینترین حد ممکن در کد خود بگیرید.
- Exception هایی را catch کنید که میدانید در حالت استثناء، چه کاری را میتوانید انجام دهید.
این به این معنی میباشد که به صورت کلی همه نوع Exception ای را به صورت کلی نگیرید و نوع Exception اختصاصی را در بلاک catch قرار دهید. الان که قرار شد در بعضی از حالتها جلوی استثنائات را بگیریم، خوب است ببینیم چطور باید اینکار را انجام بدیم.
قطعه کد زیر را در نظر بگیرید:
public void CreateCustomer(string name) { Customer customer = new Customer(name); bool result = SaveCustomer(customer); if (!result) { MessageBox.Show("Error connecting to the database. Please try again later."); } } private bool SaveCustomer(Customer customer) { try { using (MyContext context = new MyContext()) { context.Customers.Add(customer); context.SaveChanges(); } return true; } catch (DbUpdateException ex) { if (ex.Message == "Unable to open the DB connection") return false; else throw; } }
همانطور که مشاهده میکنید، در حالتیکه خطایی از نوع DbUpdateException رخ میدهد، مقدار بازگشتی متد را برابر با false میکنیم. اما مشکلی که وجود دارد این است که اینکار به اندازهی کافی خوانا نیست. همچنین honest بودن متد را نقض کردهایم. به علاوه مشکل بزرگتر دیگر این است که ما با بازگرداندن یک مقدار bool، میتوانیم به متد بالاتر اطلاع بدهیم که کار مورد نظر انجام شده یا نه، اما در مورد دلیل انجام نشدن آن، هیچ کاری نمیتوانیم بکنیم. پیشنهاد من برای مقدار بازگشتی متدهایی که احتمال انجام نشدن کاری در آنها میرود، استفاده از یک نوع اختصاصی میباشد.
در اینجا من این نوع را با نام کلاس Result معرفی میکنم. انتظاری که از این نوع اختصاصی داریم:
- Honest بودن متد را نگه دارد.
- خروجی متد را به همراه وضعیت اجرا شدن برگرداند.
- شکل یکسانی را برای خطاها داشته باشد.
- فقط جلوی خطاهای غیر منتظره را بگیرد.
برای مثال کد بالا را به شکل زیر refactor میکنیم:
private Result SaveCustomer(Customer customer) { try { using (var context = new MyContext()) { context.Customers.Add(customer); context.SaveChanges(); } return Result.Ok(); } catch (DbUpdateException ex) { if (ex.Message == "Unable to open the DB connection") Result.Fail(ErrorType.DatabaseIsOffline); if (ex.Message.Contains("IX_Customer_Name")) return Result.Fail(ErrorType.CustomerAlreadyExists); throw; } }
به عبارتی با این روش میتوانیم از انجام شدن/نشدن عملیات اطمینان حاصل کنیم و خروجی/دلیل انجام نشدن را نیز میتوانیم برگردانیم.
اگر به امضای متدهای زیر نگاه کنیم، میتوانیم آنها را طبق الگوی CQS دستهبندی کنیم:
به عنوان نمونه یک پیاده سازی از این کلاس را در اینجا قرار دادهام. قطعا میتوانیم پیاده سازیهای بهتری را از این کلاس داشته باشیم. خوشحال میشوم که نظرات خود رو با ما به اشتراک بگذارید. امیدوارم که این قسمت و صحبتهایی که در مورد استثنائات داشتیم، توانسته باشد دیدگاه جدیدی را به کدهایتان بدهد. در ادامهی این سری مطالب، مفاهیم پارادایم برنامه نویسی تابعی را بیشتر مورد بررسی قرار خواهیم داد.
بررسی زبان Go برای توسعه دهندگان #C
A Tour of Go (golang) for the C# Developer
Learning other programming languages enhances our work in our primary language. From the perspective of a C# developer, the Go language (golang) has many interesting ideas. Go is opinionated on some things (such as where curly braces go and what items are capitalized). Declaring an unused variable causes a compile failure; the use of "blank identifiers" (or "discards" in C#) are common. Concurrency is baked right in to the language through goroutines and channels. Programming by exception is discouraged; it's actually called a "panic" in Go. Instead, errors are treated as states to be handled like any other data state. We'll explore these features (and others) by building an application that uses concurrent operations to get data from a service. These ideas make us think about the way we program and how we can improve our day-to-day work (in C# or elsewhere).
0:00 Welcome to Go
2:40 Step 1: Basics
12:20 Step 2: Calling a web service
23:35 Step 3: Parsing JSON
36:26 Step 4: "for" loops
41:00 Step 5: Interfaces and methods
50:05 Step 6: Time and Args
55:10 Step 7: Concurrency
1:07:10 Step 8: Errors
1:14:40 Step 9: Concurrency and errors
1:24:35 Where to go next
renderInput(name, label, type = "text", correspond) { const { data, errors } = this.state; return ( <Input name={name} type={type} label={label} value={data[name]} onChange={this.handleChange} error={errors[name]} correspond={correspond} /> ); }
<form onSubmit={this.handleSubmit}> {this.renderInput("username", "Username")} {this.renderInput("password", "Password", "password")} {this.renderInput( "confirmPassword", "Confirm Password", "password", "password" )} {this.renderInput("name", "Name")} {this.renderButton("Register")} </form>
state = { data: { username: "", password: "", name: "", confirmPassword: "" }, errors: {}, }; schema = { username: Joi.string() .required() .email({ minDomainSegments: 2, tlds: { allow: ["com", "net", "info"] } }) .label("Username"), password: Joi.string().required().min(5).label("Password"), name: Joi.string().required().label("Name"), confirmPassword: Joi.any().valid(Joi.ref("password")).required().messages({ "any.only": "با رمز عبور مطابقت ندارد", }), };
validateProperty = ({ name, value, attributes }) => { const userInputObject = { [name]: value }; const schemaMap = { [name]: this.schema[name] }; if (attributes.correspond) { const correspondFieldName = attributes.correspond.value; userInputObject[correspondFieldName] = this.state.data[ correspondFieldName ]; schemaMap[correspondFieldName] = this.schema[correspondFieldName]; } const propertySchema = Joi.object(schemaMap); const { error } = propertySchema.validate(userInputObject, { abortEarly: true, }); return error ? error.details[0].message : null; };
این سه نوع استثناء شامل موارد DbEntityValidationException، DbUpdateConcurrencyException و DbUpdateException هستند که به صورت خلاصه به شکل زیر باید تعریف شوند:
try { context.SaveChanges(); } catch (DbEntityValidationException validationException) { //... } catch (DbUpdateConcurrencyException concurrencyException) { //... } catch (DbUpdateException updateException) { //... }
توضیحات تکمیلی
در حالت DbEntityValidationException به جزئیات خطاهای حاصل از اعتبار سنجی اطلاعات خواهیم رسید. برای مثال اگر قرار است طول فیلدی 30 حرف باشد و کاربر 40 حرف را وارد کرده است، نام خاصیت و همچنین پیغام خطای درنظر گرفته شده را دقیقا در اینجا میتوان دریافت کرد و به نحو مقتضی به کاربر نمایش داد:
catch (DbEntityValidationException validationException) { foreach (var error in validationException.EntityValidationErrors) { var entry = error.Entry; foreach (var err in error.ValidationErrors) { Debug.WriteLine(err.PropertyName + " " + err.ErrorMessage); } } }
نوع استثنای DbUpdateConcurrencyException به مسایل همزمانی و به روز رسانی یک رکورد توسط دو یا چند کاربر در شبکه مرتبط میشود که در قسمت سوم سری EF code first با معرفی ویژگیهای ConcurrencyCheck و Timestamp در مورد آن بحث شد. در اینجا به کلیه موجودیتهای تداخل دار توسط خاصیت concurrencyException.Entries خواهیم رسید و همچنین به کمک متد GetDatabaseValues میتوان موارد جدید ثبت شده مرتبط با این موجودیت تداخل دار را از بانک اطلاعاتی نیز دریافت کرد:
catch (DbUpdateConcurrencyException concurrencyException) { //بررسی مورد اول var dbEntityEntry = concurrencyException.Entries.First(); var dbPropertyValues = dbEntityEntry.GetDatabaseValues(); }
و یا کلا ممکن است حین به روز رسانی بانک اطلاعاتی مشکلی رخ داده باشد که در اینجا عموما پیغام حاصل را باید در InnerException تولیدی یافت و همچنین در اینجا لیست موجودیتهای مشکل دار نیز قابل دریافت و بررسی هستند:
catch (DbUpdateException updateException) { if (updateException.InnerException != null) Debug.WriteLine(updateException.InnerException.Message); foreach (var entry in updateException.Entries) { Debug.WriteLine(entry.Entity); } }
بنابراین بررسی catch exception کلی در EF Code first مناسب نبوده و نیاز است بیشتر به جزئیات ذکر شده، وارد و دقیق شد.
یک نکته:
بهتر است یک کلاس پایه عمومی مشتق شده از DbContext را ایجاد و متد SaveChanges آن را تحریف کرد. سپس سه حالت فوق را به آن اعمال نمود. اکنون میتوان از این کلاس پایه بارها استفاده کرد بدون اینکه نیازی به تکرار کدهای آن در هرجایی که قرار است از متد SaveChanges استفاده شود، باشد. شبیه به این کار را در قسمت 14 سری EF code first مشاهده نمودهاید.