الف) تزریق وابستگیها در سازنده کلاس
ب) تزریق وابستگیها در خواص عمومی کلاسها یا Setters injection
حالت الف متداولترین است و بیشتر زمانی کاربرد دارد که کار وهله سازی یک کلاس را میتوان راسا انجام داد. اما در فرمها یا یوزرکنترلهای ASP.NET Web forms به صورت پیش فرض کار وهله سازی فرمها و یوزرکنترلها توسط موتور ASP.NET انجام میشود و در این حالت اگر بخواهیم از تزریق وابستگیها استفاده کنیم، مدام به همان روش معروف Service locator و استفاده از container.Resolve در تمام قسمتهای برنامه میرسیم که آنچنان روش مطلوبی نیست.
اما ... در ASP.NET Web forms میتوان وهله سازی فرمها را نیز تحت کنترل قرار داد، که برای آن دو روش زیر وجود دارند:
الف) یک کلاس مشتق شده را از کلاس پایه PageHandlerFactory تهیه کنیم. این کلاس را پیاده سازی کرده و نهایتا بجای وهله ساز پیش فرض فرمهای موتور داخلی ASP.NET، در فایل وب کانفیگ برنامه استفاده کنیم. یک نمونه از پیاده سازی آنرا در اینجا میتوانید مشاهده کنید.
مشکلی که این روش دارد سازگاری آن با حالت Full trust است. یعنی برنامه شما در یک هاست Medium trust (اغلب هاستهای خوب) اجرا نخواهد شد.
ب) روش دوم، استفاده از یک Http Module است برای اعمال Setter injectionها، به صورت خودکار. اکنون که حالت الف را همه جا نمیتوان بکار برد یا به عبارتی نمیتوان وهله سازی فرمها را راسا در دست گرفت، حداقل میتوان خواص عمومی اشیاء صفحه تولید شده را مقدار دهی کرد که در ادامه، این روش را بررسی میکنیم.
تهیه ماژول انجام Setters injection به صورت خودکار در برنامههای ASP.NET Web forms به کمک StructureMap
پیشنیاز این بحث، مطلب «استفاده از StructureMap به عنوان یک IoC Container» میباشد که پیشتر مطالعه کردید (در حد نحوه نصب StructureMap و آشنایی با تنظیمات اولیه آن)
using System.Collections; using System.Web; using System.Web.UI; using StructureMap; namespace DI05.Core { /// <summary> /// تسهیل در کار تزریق خودکار وابستگیها در سطح فرمها و یوزرکنترلها /// </summary> public class StructureMapModule : IHttpModule { public void Dispose() { } public void Init(HttpApplication app) { app.PreRequestHandlerExecute += (sender, e) => { var page = HttpContext.Current.Handler as Page; // The Page handler if (page == null) return; WireUpThePage(page); WireUpAllUserControls(page); }; } private static void WireUpAllUserControls(Page page) { // در اینجا هم کار سیم کشی یوزر کنترلها انجام میشود page.InitComplete += (initSender, evt) => { var thisPage = (Page)initSender; foreach (Control ctrl in getControlTree(thisPage)) { // فقط یوزر کنترلها بررسی شدند // اگر نیاز است سایر کنترلهای قرار گرفته روی فرم هم بررسی شوند شرط را حذف کنید if (ctrl is UserControl) { ObjectFactory.BuildUp(ctrl); } } }; } private static void WireUpThePage(Page page) { ObjectFactory.BuildUp(page); // برقراری خودکار سیم کشیها در سطح صفحات } private static IEnumerable getControlTree(Control root) { foreach (Control child in root.Controls) { yield return child; foreach (Control ctrl in getControlTree(child)) { yield return ctrl; } } } } }
پس از تهیه ماژول فوق، باید آنرا در فایل وب کانفیگ برنامه معرفی کرد:
<?xml version="1.0"?> <configuration> <system.web> <compilation debug="true" targetFramework="4.0" /> <httpModules> <add name="StructureMapModule" type="DI05.Core.StructureMapModule"/> </httpModules> </system.web> <system.webServer> <modules runAllManagedModulesForAllRequests="true"> <add name="StructureMapModule" type="DI05.Core.StructureMapModule"/> </modules> <validation validateIntegratedModeConfiguration="false" /> </system.webServer> </configuration>
مثالی از نحوه استفاده از StructureMapModule تهیه شده
فرض کنید لایه سرویس برنامه دارای اینترفیسها و کلاسهای زیر است:
namespace DI05.Services { public interface IUsersService { string GetUserEmail(int id); } } namespace DI05.Services { public class UsersService: IUsersService { public string GetUserEmail(int id) { //فقط جهت بررسی تزریق وابستگیها return "test@test.com"; } } }
using System; using StructureMap; using DI05.Services; namespace DI05 { public class Global : System.Web.HttpApplication { private static void initStructureMap() { ObjectFactory.Initialize(x => { x.For<IUsersService>().Use<UsersService>(); x.SetAllProperties(y => { y.OfType<IUsersService>(); }); }); } protected void Application_Start(object sender, EventArgs e) { initStructureMap(); } void Application_EndRequest(object sender, EventArgs e) { ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects(); } } }
مرحله آخر هم استفاده از سرویسهای برنامه به شکل زیر است:
using System; using DI05.Services; namespace DI05 { public partial class Default : System.Web.UI.Page { public IUsersService UsersService { set; get; } protected void Page_Load(object sender, EventArgs e) { lblEmail1.Text = string.Format("From Default Page: {0}", UsersService.GetUserEmail(1)); } } }
دریافت مثال کامل قسمت جاری
DI05.zip
یک نکتهی تکمیلی
برای ارتقاء نکات مطلب جاری به نگارش سوم StructureMap نیاز است موارد ذیل را لحاظ کنید:
الف) نصب بستهی وب آن
PM> Install-Package structuremap.web
ج) x.SetAllProperties را به x.Policies.SetAllProperties ویرایش کنید.
Dynamic Semantics
Objectها علاوه بر داده و رفتار به عنوان توصیفات ثابت، در زمان اجرا دارای یک Local State (a snapshot) از مقادیر داینامیک مربوط به اعضای دادهای خود، میباشند. مجموعه تمام حالاتی که وهلههای یک کلاس میتوانند بین آنها گذر (transition) داشته باشد، dynamic semantics مربوط به کلاس نامیده میشود و به وهلههای کلاس این امکان را میدهند تا به یک پیغام مشابه رسیده و در زمانهای مختلف از چرخه زندگی خود، به اشکال مختلف پاسخ دهند.
Method junk for the class X if (local state #1) then do something else if (local state #2) then do something different End Method
بخش اصلی هر طراحی شیء گرا، dynamic semantics وهلهها میباشد. dynamic semantics هر کلاسی باید در قالب یک دیاگرام state-transition مستند شود. شکل زیر dynamic semantics پروسههای موجود در یک سیستم عامل را در قابل یک دیاگرام حالت نمایش میدهد. این پروسهها توانایی این را دارند که در هر کدام از حالات: runnable، current process، blocked، sleeping و یا در حالت exited، قرار داشته باشند. همچنین به عنوان مثال، یک پروسه زمانی میتواند در حالت current process قرار گیرد که حتما قبلا در حالت runnable قرار داشته باشد. این اطلاعات برای ایجاد تست برای کلاسها و وهلههای آنها میتواند مفید واقع شود.
شکل 2.8 State-transition diagram notation
برخی از طراحان به طور تصادفی، dynamic semantics یک کلاس را به عنوان static semantics آن کلاس مدل میکنند. به عنوان مثال اگر color یکی از اعضای داده ای (data member) کلاس توپ باشد و بعد از وهله سازی از کلاس توپ، color آن بازهم قابل تغییر باشد، منظور اینکه توپ آبی به عنوان یک وهله از کلاس توپ در زمان حیات خود تغییر رنگ دهد، اصطلاحا میگویند: color جزء dynamic semantics کلاس توپ میباشد. با توجه به توضحیاتی که داده شد، حال اگر طراحی برای هر رنگ توپ یک کلاس جدا در نظر گرفته باشد، dynamic semantics را به عنوان static semantics مدل کرده و به احتمال زیاد ما را به سمت ایجاد مشکل Class Proliferation (ازدیاد کلاس ها) سوق خواهد داد.
Abstract Classes
به سوالات زیر توجه کنید:
- آیا هرگز میوه خوردهاید؟
- آیا هرگز پیش غذا خوردهاید؟
- آیا هرگز دسر خوردهاید؟
حال با توجه به سوالات «مزه غذا چطور بود؟ دسری که خوردید، چه تعداد کالری داشت؟ هزینه پیش غذایی که خوردید چقدر بود» پاسخ چه خواهد بود؟
من (نویسنده) ادعا میکنم که هیچ کسی تا به حال میوه نخورده است. بیشتر مردم، سیب، موز و پرتقال خوردهاند؛ میوهی قرمز رنگی به ارزش 3 پوند را نخوردهاند.
شبیه به این مسئله برای زمانی است که گارسون رستوران از شما سوال میکند: «برای شام چه چیزی میل دارید» و شما جواب میدهید: «یک پیش غذا، یک غذای اصلی و یک دسر». در این حالت چون شما دقیقا مشخص نکردهاید چه نوعی میخواهید، گارسون، مات و مبهوت خواهد ماند. همه میدانیم که چیزی تحت عنوان میوه، پیش غذا و یا وهله دسر در واقعیت وجود ندارد؛ بله این عبارات اطلاعات مفیدی را تسخیر میکنند. اگر من در دستم یک ساعت زنگی گرفته و از شما میپرسیدم: «نظرتان در مورد میوه من چیست؟»؛ بدون شک فکر میکردید من دیوانه شدهام. حال اگر در دستم سیبی گرفته و سوال قبلی را میپرسیدم، این بار از نظر شما من یک شخص عاقل بودم.
با وجود اینکه نمیتوان از میوه وهله سازی کرد، اما اطلاعات مفیدی را تسخیر میکند. در واقع میوه، یک کلاسی (concept) است که دانشی از نحوه وهله سازی وهله هایش به وسیله Type پیاده ساز خود، ندارد.
کلاسی که دانشی از نحوه وهله سازی وهلههای خود ندارد، abstract class (کلاس مجرد یا انتزاعی) نامیده میشود.
کلاسی که دانش نحوه وهله سازی وهلههای خود دارد، concrete class نامیده میشود.
در پارادایم شیء گرا، مهمترین استفاده از کلاسهای انتزاعی در مباحث ارث بری مطرح میشود.
Roles Versus Classes
مطمئن باشید انتزاع هایی را که مدل میکنید کلاس بوده و نه نقشهایی که وهلههای آنها بازی میکنند. (Be sure the abstractions that you model are classes and not simply the roles objects play)آیا مادر و پدر به عنوان یک کلاس هستند یا نقشهایی هستند که وهلههای کلاس شخص، بازی میکند؟ پاسخ این سوال وابسته به دامینی (domain) است که طراح در حال مدل سازی آن میباشد. اگر در دامین مورد نظر، مادر و پدر رفتارهای مختلفی دارند، احتمالا باید به عنوان کلاسهای جدا مدل شوند. اگر رفتارهای یکسانی دارند، در نتیجه نقشهای مختلفی هستند که وهلههای کلاس شخص بازی میکنند. به عنوان مثال، میتوان کلاس خانواده را متشکل از وهلهای از کلاس پدر، وهلهای از کلاس مادر و مجموعهای از وهلههای کلاس فرزند در نظر گرفت. در مقابل ممکن است کلاس خانواده را متشکل از وهلهای از کلاس شخص به عنوان پدر، وهلهای از کلاس شخص به عنوان مادر و آرایهای از وهلههای شخص به عنوان فرزندان، مدل کنید. قرار گرفتن در وضیعتی که هر نقش، بخشی از رفتاریهای شخص را مورد استفاده قرار میدهد، کافی نیست و باید مطمئن شوید که رفتارها واقعا متفاوت میباشند. همچنین باید به یاد داشته باشید که زمانیکه وهلهای از بخشی از رفتارهای کلاس خود استفاده میکند، نیز مشکلی وجود ندارد و لازم نیست کلاسهای دیگری را به خاطر این موضوع در طراحی خود در نظر بگیرید.
شکل 2.9 Two views of a family
برخی از طراحان به این شکل تست میکنند که اگر عضوی از واسط عمومی را نمیتوان برای نقش مورد نظر مورد استفاده قرار داد، این موضوع نشان از این دارد که باید برای نقش مورد نظر در طراحی خود کلاس جداگانهای را در نظر داشته باشند. اگر هم عضو مذکور قابل استفاده نباشد، کلاس یکسانی برای نقشهای مختلف استفاده خواهد شد. به عنوان مثال، اگر عملیات ()go_into_labor جزء عملیاتی میباشد که مادر انجام میدهد، در حالیکه پدر چنین عملیاتی را نمیتواند انجام دهد، در این حالت نیز لازم است مادر به عنوان کلاس جداگانهای در نظر گرفته شود. اگر در دامین دیگری، عوض کردن پوشاک را تنها مادر انجام میدهد، در این حالت مادر نقشی از کلاس شخص میباشد، چرا که پدر هم توانایی انجام این عملیات را دارد.
قواعد شهودی فصل دوم
همه دادهها باید در داخل کلاس خود پنهان شده باشند. (All data should be hidden within its class)
استفاده کنندگان از کلاس باید به واسط عمومی آن وابسته باشند، اما یک کلاس نباید به استفاده کنندگان خود، وابسته باشد. (Users of a class must be dependent on its public interface, but a class should not be dependent on its users)
تعداد پیغامهای موجود در قرارداد یک کلاس را کمینه سازید. (Minimize the number of messages in the protocol of a class)
پیاده سازی یک واسط عمومی یکسان کمینه برای همه کلاسها (Implement a minimal public interface that all classes understand [e.g., operations such as copy (deep versus shallow), equality testing, pretty printing, parsing from an ASCII description, etc.].)
جزئیات پیاده سازی، مانند توابع خصوصی common-code ( توابعی که کد مشترک سایر متدهای کلاس را در بدنه خود دارند) را در واسط عمومی یک کلاس قرار ندهید. (Do not put implementation details such as common-code private functions into the public interface of a class)
واسط عمومی کلاس را با اقلامی که یا استفاده کنندگان از کلاس توانایی استفاده از آن را نداشته و یا تمایلی به استفاده از آنها ندارند، آمیخته نکنید. (Do not clutter the public interface of a class with items that users of that class are not able to use or are not interested in using )
اتصال و پیوستگی مابین کلاسها باید از نوع Nil یا Export باشد؛ به این معنی که یک کلاس فقط از واسط عمومی کلاس دیگر استفاده کند یا کاری با آن نداشته باشد. (Classes should only exhibit nil or export coupling with other classes, that is, a class should only use operations in the public interface of another class or have nothing to do with that class.)
یک کلاس باید یک و تنها یک Key Abstraction را تسخیر نماید. (A class should capture one and only one key abstraction)
داده و رفتار مرتبط را در یک جا (کلاس) نگه دارید. (Keep related data and behavior in one place)
اطلاعات نامرتبط به هم را در کلاسهای جدا از هم قرار دهید. ((Spin off nonrelated information into another class (i.e., noncommunicating behavior)
مطمئن باشید انتزاع هایی را که مدل میکنید کلاس بوده و نه نقشهایی که وهلههای آنها بازی میکنند. (Be sure the abstractions that you model are classes and not simply the roles objects play)
In this article, we’ll take a close look at some of the changes we made on this very site — running on JAMStack with React — to optimize the web performance and improve the Core Web Vitals metrics. With some of the mistakes we’ve made, and some of the unexpected changes that helped boost all the metrics across the board.
در این مقاله حالتهایی را که الگوی طراحی Null Object، قادر به تشخیص آنها نیست را به وسیله الگوی طراحی Special case رفع میکنیم.
گاهی از اوقات پیش خواهد آمد که ما تسلیم شده و شیءای را برمیگردانیم که خودش نشان دهنده خطاست. اما همه شکستها یکسان نیستند. در ابتدا، کاربر ممکن است واجد شرایط انجام خرید نباشد. اما اگر واجد شرایط باشد، آیتم مورد نظر ممکن است در انبار نباشد. در صورت موجود بودن در انبار، حساب کاربر ممکن است مبلغ کافی نداشته باشد.
بجای برگشت دادن شیء Null در تمام این موارد، ما میتوانیم نتیجه را اصلاح کنیم و اساسا هر بار یک شیء مختلف را بازگردانیم. اینها هنوز هم نوعی از اشیاء Null هستند؛ ولی اینبار دارای معانی هستند. یکی از انها برای «حساب کاربری ناکافی» است، یکی دیگر برای «سایت در دست تعمیر و نگهداری» است و یا یکی دیگر از آنها «موجود نبودن در انبار» خواهد بود.
چنین اشیائی به موارد خاص ( Special Case ) اشاره میکنند. ما میتوانیم اشیاء مورد خاص ( Special Case ) را به عنوان نتایج واقعی عملیات بسازیم. فقط اگر تمام بررسیهای کسب و کار به پایان برسند و عملیات موفقیت آمیز باشد، آنگاه شیء واقعی نتیجه را باز میگردانیم.
نمونهای از پیاده سازی موارد خاص
این بخشی از رابط کاربری است که توسط سرویسهای برنامه کاربردی اجرا میشود:
public interface IApplicationServices { ... IReceiptViewModel LoggedInUserPurchase(string itemName); }
لایه نمایش انتظار دارد که لایه نرم افزار یک ویو مدل به خصوصی را برای آن تولید کند. در حال حاضر ما یک سناریوی موفقیت آمیز را داریم که در آن ویو مدل شامل اطلاعات واقعی از خرید است و چندین سناریوی شکست.
اگرعملیات خرید با هر کدام از شرایط زیر مواجه شود به شکست میانجامد:
1) سایت در دست تعمیر و نگهداری باشد.
2) کاربر ثبت نشده و یا فعال نیست.
3) آیتمی موجود نیست و یا وجود ندارد.
4) موجودی کاربر کم باشد.
public class DownForMaintenance: IReceiptViewModel { }
public class InvalidUser: IReceiptViewModel { public string UserName { get; private set; } public InvalidUser(string userName) { this.UserName = userName; } }
public class OutOfStock: IReceiptViewModel { public string UserName { get; private set; } public string ItemName { get; private set; } public OutOfStock(string userName, string itemName) { this.UserName = userName; this.ItemName = itemName; } }
public class InsufficientFunds: IReceiptViewModel { public string UserName { get; private set; } public decimal Amount { get; private set; } public string ItemName { get; private set; } public InsufficientFunds(string userName, decimal amount, string itemName) { this.UserName = userName; this.Amount = amount; this.ItemName = itemName; } }
public class ApplicationServices: IApplicationServices { ... public IReceiptViewModel LoggedInUserPurchase(string itemName) { if (IsDownForMaintenance()) return new DownForMaintenance(); return this.domain.Purchase(Session.LoggedInUserName, itemName); } private bool IsDownForMaintenance() { return File.Exists("maintenance.lock"); } }
public class DomainServices: IDomainServices { public IReceiptViewModel Purchase(string userName, string itemName) { User user = this.userRepository.Find(userName); if (user == null) return new InvalidUser(userName); Account account = this.accountRepository.FindByUser(user); return this.Purchase(user, account, itemName); } private IReceiptViewModel Purchase(User user, Account account, string itemName) { Product item = this.productRepository.Find(itemName); if (item == null) return new OutOfStock(user.UserName, itemName); ReceiptDto receipt = user.Purchase(item); MoneyTransaction transaction = account.Withdraw(receipt.Price); if (transaction == null) return new InsufficientFunds(user.UserName, receipt.Price, itemName); return receipt; } }
10) Practical .NET: Powerful JavaScript With Upshot and Knockout
The Microsoft JavaScript Upshot library provides a simplified API for
retrieving data from the server and caching it at the client for reuse. Coupled
with Knockout, the two JavaScript libraries form the pillars of the Microsoft
client-side programming model.
9) On VB: Database Synchronization with the Microsoft Sync
Framework
The Microsoft Sync Framework is a highly flexible framework
for synchronizing files and data between a client and a master data store. With
great flexibility often comes complexity and confusion, however.
8) C# Corner: Performance Tips for Asynchronous Development in
C#
Visual Studio Async is a powerful development framework, but it's
important to understand how it works to avoid performance hits.
7) 2 Great JavaScript Data-Binding Libraries
JavaScript
libraries help you build powerful, data-driven HTML5 apps.
6) On VB: Entity Framework Code-First Migrations
Code First
Migrations allow for database changes to be implemented all through code.
Through the use of Package Manager Console (PMC), commands can be used to
scaffold database changes.
5) C# Corner: The New Read-Only Collections in .NET 4.5
Some
practical uses for the long-awaited interfaces, IReadOnlyList and
IReadOnlyDictionary, in .NET Framework 4.5.
4) C# Corner: Building a Windows 8 RSS Reader
Eric Vogel
walks through a soup-to-nuts demo for building a Metro-style RSS reader.
3) C# Corner: The Build Pattern in .NET
How to separate
complex object construction from its representation using the Builder design
pattern in C#.
2) Inside Visual Studio 11: A Guided Tour
Visual Studio 2012
(code-named Visual Studio 11 then) is packed with new features to help you be a
more efficient, productive developer. Here's your guided tour.
1) HTML5 for ASP.NET Developers
The technologies bundled as
HTML5 finally support what developers have been trying to get HTML to do for
decades.
ما طراحی میکنیم تا علاوه بر نیازهای عملیاتی، به نیازهای غیر عملیاتی (Non Functional Requirements) نیز فکر کنیم؛ در حالیکه در زمان برنامه نویسی صرفا به Functionality فکر میکنیم.
کتاب Object Oriented Design Heuristics اولین کتاب در زمینه طراحی و توسعه شیء گرا میباشد. خواندن آن برای برنامه نویسان در هر رده ای که هستند، مفید خواهد بود و میتوانند از این Heuristicها (قواعد شهودی) به عنوان ابزاری برای تبدیل شدن به یک توسعه دهنده برتر، استفاده کنند.
در این کتاب بیشتر، بهبود طراحی شیء گرا هدف قرار داده شدهاست و در این راستا بیش از 60 دستورالعمل که هیچ وابستگی به زبان خاصی هم ندارند، ارائه شده است. قواعد شهودی در واقع قوانین سخت گیرانهای نیستند. بلکه میتوان آنها را به عنوان یک مکانیزم هشدار در نظر گرفت که در زمان نیاز حتی میتوان آنها را نقض کرد.
پیشنهاد میکنم حداقل برای اینکه ادبیات فنی خود را سامان ببخشید و با ادبیات یکسانی باهم صحبت کنیم، این کتاب را مطالعه کنید.
Introduction to Classes and Objects
پارادایم شیء گرا از مفاهیم کلاس و آبجکت، به عنوان بلوکهای ساختاری پایهای در شکل گیری یک مدل سازگار و استوار برای تحلیل، طراحی و پیاده سازی نرم افزار، استفاده میکند.
این مفاهیم را با یک مثال واقعی، بهتر میتوان شرح داد. یک اتاق پر از جمعیت را درنظر بگیرید؛ اگر شما میپرسیدید «چه تعداد از حاضرین در این اتاق میتوانند یک ساعت زنگدار(alarm clock ) را با در دست داشتن تمام قطعات آن، بسازند؟» در بهترین حالت یک یا دو نفر تمایل داشتند دست خود را بالا ببرند. اگر در همین اتاق میپرسیدید، «چه تعداد از حاضرین در این اتاق میتوانند یک ساعت زنگدار را برای ساعت 9 صبح تنظیم کنند؟» بدون شک بیشتر جمعیت تمایل داشتند دست خود را بالا ببرند.
آیا نامعقول نیست که این تعداد جمعیت زیاد، ادعا دارند که میتوانند از ساعت زنگدار استفاده کنند، درحالیکه حتی نمیتوانند یک ساعت زنگدار بسازند؟ پاسخ بی درنگ برای این سوال «البته که نه! سوال شما نامعقول است» میباشد.
در دنیای واقعی خیلی چیزها هستند که ما میتوانیم از آنها استفاده کنیم، بدون آنکه دانشی درباره پیاده سازی آنها داشته باشیم؛ مانند: یخچالها، اتومبیلها، دستگاههای فتوکپی، کامپیوترها و غیره. چون آنها برای استفاده شدن از طریق واسط عمومی خودشان، تعریف و طراحی شدهاند. لذا حتی بدون داشتن دانشی از پیاده سازی آنها، استفاده از آنها آسان میباشد. این واسط عمومی وابسته به دستگاه مورد نظر است. اما جزئیات پیاده سازی دستگاه را از دید کاربرانش پنهان میکند. این استراتژی طراحی، چیزی است که به سازنده اجازه میدهد بدون آنکه کاربران رنجیده شوند، با آزادی عمل، 60 مؤلفه کوچک استفاده شده در ساخت ساعت زنگدار را تعویض کند.
مثال دیگری از واسط عمومی در مقابل جزئیات پیاده سازی، میتواند در حوزه اتومبیلها دیده شود. زمانیکه تولید کنندگان اتومبیل از سیستمهای احتراق مکانیکی به سمت سیستمهای احتراق الکترونیکی کوچ کردند، تعداد خیلی کمی از کاربران اتومبیلها نگران این موضوع بودند. اما چرا؟ چون واسط عمومی آنها مانند سابق ماند و تنها پیاده سازی تغییر کرد. فرض کنید که شما به قصد خرید اتومبیل به یک فروشنده اتومبیل مراجعه میکنید و فروشنده یک سوئیچ را به شما داده و از شما میخواهد برای تست آن را برانید. بعد از تلاشی که برای استارت زدن داشتید، فروشنده اعلام میکند که در این مدل برای استارت زدن باید ابتدا کاپوت را بالا زده و دکمه قرمز را فشار دهید. در این حالت، بدلیل اینکه واسط عمومی اتومبیل دستخوش تغییر بوده است، باعث ناراحتی شما خواهد شد.
این فلسفه، دقیقا یکی از ایدههای پایهای در پارادایم شیءگرا میباشد. تمام جزئیات پیاده سازی در سیستم شما باید در پشت یک واسط عمومی مستحکم و سازگار، از کاربران آنها پنهان باشد. نیاز کاربران، دانستن درباره واسط عمومی میباشد؛ اما هرگز مجاز به دیدن جزئیات پیاده سازی آنها نیستند. با این روش، پیاده ساز میتواند به هرشکلی که مناسب است، پیاده سازی را تغییر دهد؛ درحالیکه واسط عمومی مانند سابق میباشد. به عنوان مسافری که مکرر سفر میکنم، به شما اطمینان میدهم که استفاده از ساعتهای زنگدار با وجود عدم اطلاع از پیاده سازی آنها، فواید عظیمی دارند. در هتلهای زیادی که از دسته بندیهای گستردهای از ساعتها مانند الکتریکی، قابل کوک (windup)، باتری خور، در هر دو مدل دیجیتال و آنالوگ استفاده میکنند، اقامت کردهام. یکبار هم اتفاق نیفتادهاست در حالیکه در هواپیما نشسته باشم، نگران این باشم که قادر نخواهم بود از ساعت زنگی اتاقم در هتل استفاده کنم.
بیشتر خوانندگان این کتاب، با وجود اینکه در نزدیکی آنها شاید ساعت زنگداری هم نباشد، ولی منظور بنده را با عبارت «ساعت زنگدار» متوجه شدند. به چه دلیل؟ شما در زندگی خودتان ساعتهای زنگدار زیادی را میبینید و متوجه میشوید که همه آنها از یکسری خصوصیات مشترک مانند زمان، یک زمان هشدار و طراحیای که مشخص میکند هشدار روشن یا خاموش است، بهره میبرند. همچنین متوجه میشوید که همه ساعتهای زنگداری که دیدهاید امکان تنظیم کردن زمان، تنظیم زمان هشدار و روشن و خاموش کردن هشدار را به شما میدهند. در نتیجه، شما الان مفهومی را به نام «ساعت زنگدار» دارید که مفهومی را از داده و رفتار، در یک بسته بندی مرتب برای همه ساعتهای زنگدار، تسخیر میکند. این مفهوم به عنوان یک Class (کلاس) شناخته میشود. یک ساعت زنگدار فیزیکی که شما در دست خود آن را نگه داشتهاید، یک Object (وهله، Instance) ای از کلاس ساعت زنگدار میباشد. رابطه بین مفهوم کلاس و وهله، Instantiation Relationship (وهله سازی) نام دارد. به یک object، ساعت زنگدار وهله سازی شده (Instantiated) از کلاس ساعت زنگدار گفته میشود؛ در حالیکه از کلاس ساعت زنگدار به عنوان تعمیم (Generalization) از همه objectهای کلاس ساعت زنگدار که شما با آنها روبرو شدهاید، یاد میشود.
اگر من به شما میگفتم که ساعت زنگدارم از روی پاتختی (میز کوچک کنار تخت که دارای کشو میباشد) من پرید، من را گاز گرفت، سپس گربهی همسایه را دنبال کرد، قطعا مرا دیوانه به حساب میآوردید. اگر به شما میگفتم که سگ من کارهای مشابهای را انجام میدهد، کاملا منطقی میبود. چون نام یک کلاس تنها به مجموعهای از خواص اشاره نمیکند، بلکه رفتارهای موجودیت (entity) را نیز مشخص میکند. این رابطه دوسویه بین داده و رفتار، اساس پارادایم شیء گرا میباشد.
- هویت خود (ممکن است آدرس آن در حافظه باشد) - its own identity
- خواص کلاس خود (معمولا استاتیک) و مقادیر این خواص (معمولا پویا) - attributes of its class
- رفتار کلاس خود (از دید پیاده ساز) - behavior of its class
- واسط منتشر شده کلاس خود (از دید استفاده کننده) - published interface of its class
یک کلاس را می توان با record definition (ساختار داده پایه، struct) و لیستی از عملیاتی که مجاز به کار بر روی این record definition هستند، پیاده سازی کرد. در زبانهای رویهای (Procedural) یافتن وابستگی دادهها در یک تابع معین، آسان میباشد. این کار را میتوان به سادگی با بررسی کردن جزئیات پیاده سازی تابع و مشاهده نوع داده پارامترهای آن، مقادیر بازگشتی و متغییرهای محلیای که تعریف شدهاند، انجام داد. اگر قصد شما پیدا کردن وابستگیهای تابعی بر روی یک داده میباشد، باید همه کد را بررسی کرده و به دنبال توابعی باشید که به داده شما وابسته هستند. در مدل شیء گرا، هر دو نوع وابستگی (داده به رفتار و رفتار به داده) به راحتی در دسترس میباشند. وهلهها، متغیرهایی از یک نوع داده کلاس هستند. جزئیات داخلی آنها باید فقط برای لیست توابع مرتبط با کلاسهایشان آشکار باشد. این محدودیت دسترسی به جزئیات داخلی وهلهها، Information Hiding نامیده میشود. اختیاری بودن این بحث در خیلی از زبانهای شیء گرا ما را به سمت اولین قاعده شهودی هدایت میکند.
همه دادهها باید در داخل کلاس خود پنهان شده باشند. (All data should be hidden within its class)
با نقض این قاعده، امکان نگهداری را هم از دست میدهید. اجبار به پنهان کردن اطلاعات در مراحل طراحی و پیاده سازی، بخش عظیمی از فواید پارادایم شیء گرا میباشد. اگر داده به صورت عمومی تعریف شده باشد، تشخیص اینکه کدام بخش از عملیات (functionality) سیستم به آن داده وابسته است، سخت و مشکل خواهد بود. در واقع، نگاشت تغییرات داده به عملیات سیستم، همانند طراحی و پیاده سازی در دنیای action-oriented میباشد. ما مجبور میشویم برای تشخیص اینکه کدام عملیات به داده مورد نظر ما وابسته است، تمام عملیات سیستم را بررسی کنیم، تا به این ترتیب متوجه شویم.
برخی اوقات، یک توسعه دهنده استدلال میکند «نیاز دارم این بخش از داده را عمومی تعریف کنم زیرا ....» در این وضعیت، توسعه دهنده باید از خود سوال کند «کاری که تلاش دارم با این داده انجام دهم چیست و چرا کلاس این عملیات را خودش برای من انجام نمیدهد؟» در همه موارد این کلاس است که به سادگی عملیات ضروری را فراموش کردهاست. کمی بر روی شکل 2.2 فکر کنید. توسعه دهنده به صورت تصادفی فکر کرده است که عضو byte_offset را برای مجاز ساختن دسترسی تصادفی I/O، به صورت عمومی تعریف کند. اما چیزی که واقعا برای انجام این کار به آن نیاز داشت، تعریف یک operation بود (در زبان سی، توابع fseek و ftell برای ممکن کردن دسترسی تصادفی I/O، موجود هستند).
مراقب توسعه دهندههایی که جسورانه میگویند: «ما میتوانیم این بخش از داده را تغییر دهیم، زیرا هیچوقت تغییر نخواهد کرد!» باشید. طبق قانون برنامه نویسی مورفی، اولین بخشی که نیاز به تغییر خواهد داشت همین بخش از داده است.
به عنوان مثال دیگر برای روشنتر شدن بحث، کلاس Point را که پیاده سازی آن به روش مختصات دکارتی میباشد، در نظر بگیرید. یک طراح بیتجربه ممکن است دلیل تراشی کند که ما میتوانیم دادههای X و Y را به صورت عمومی تعریف کنیم؛ چرا که هیچ موقع تغییر نخواهند کرد. فرض کنید نیاز جدیدی مبنی بر اینکه پیاده سازی Point به ناچار باید از دکارتی به قطبی تغییر کند، به دست شما برسد. به این صورت استفاده کنندگان از این کلاس Point نیز باید تغییر کنند. حال اگر دادهها پنهان بودند و عمومی نبودند، در نتیجه فقط لازم بود پیاده سازهای این کلاس، کد خود را تغییر دهند.
Class Coupling and Cohesion
تعدادی از قواعد شهودی هم، با Coupling و Cohesion به ترتیب مابین و درون کلاسها، سروکار دارند. تلاش ما در راستای افزایش Cohesion درون کلاسها و سست کردن و کاهش Coupling مابین کلاسها میباشد. این قواعد شهودی همین اهداف را در پارادایم action-oriented، در ارتباط با توابع دارند. هدف از Tight Cohesion (انسجام و چسبندگی قوی) در توابع، انسجام بالا و ارتباط نزدیک مابین کدهای موجود در تابع، میباشد. هدفی که Loose Coupling (اتصال سست و ضعیف، وابستگی ضعیف) در بین توابع دنبال میکند، اشاره دارد به اینکه اگر تابعی قصد استفاده از تابع دیگری را داشته باشد، باید وارد شدن و خروج از آن، از یک نقطه صورت گیرد. این مباحث منجربه مطرح شدن قواعد شهودی از جمله: «یک تابع باید طوری سازماندهی شود که تنها یک دستور return داشته باشد»، در پارادایم action-oriented میشود.
ما در پارادایم شیء گرا، اهداف خود از Loose Coupling و Tight Cohesion را در سطح کلاس مطرح میکنیم. 5 شکل اصلی Coupling مابین کلاسها به شرح زیر میباشد:
- Ni Coupling
- Export Coupling
- Overt Coupling
- Covert Coupling
- Surreptitious Coupling
بهترین حالتی که دو کلاس به طور مطلق هیچ وابستگی به یکدیگر ندارند. در این صورت میتوان یکی کلاسها را حذف کرد، بدون اینکه تأثیری بر روی سایر آنها داشته باشد. البته وجود برنامهای کاربردی با این نوع اتصال ممکن نخواهد بود. بهترین چیزی که میشود با این نوع اتصال ایجاد کرد، Class Libraryای میباشد که شامل مجموعه ای از کلاسهای مستقل بوده، به طوری که هیچ تأثیری بر روی یکدیگر ندارند.
در این شکل از اتصال، یک کلاس به واسط عمومی کلاس دیگر وابسته میباشد؛ به این معنی که از عملیاتی که کلاس مورد نظر در واسط عمومی خود قرار داده است، استفاده میکند.
این نوع اتصال زمانی رخ میدهد که یک کلاس از جزئیات پیاده سازی کلاس دیگر با داشتن اجازه دسترسی از جانب آن، استفاده کند. به عنوان مثال، مکانیزم کلاسهای friend در زبان سی پلاس پلاس، که امکان این را میدهد کلاس X اجازه دوستی به کلاس Y را اعطا کند و در این صورت کلاس Y میتواند به جزئیات پیاده سازی خصوصی کلاس X دسترسی داشته باشد.
این نوع اتصال هم به مانند Overt میباشد؛ با این تفاوت که هیچ اجازه دسترسی به کلاس Y داده نشده است. اگر زبانی داشته باشیم که به کلاس Y اجازه دهد خود را به عنوان دوست کلاس X معرفی کند، در این صورت نوع اتصال بین دو کلاس X و Y از نوع Covert میباشد. به عنوان مثال واقعی، میتوان به استفاده از Reflection در دات نت اشاره کرد.
آخرین نوع اتصال که بدترین حالت هم محسوب میشود، مربوط است به زمانیکه کلاس X به هر طریقی که شده از جزئیات داخلی کلاس Y آگاه باشد و از اعضای عمومی دادهای (public data member) آن کلاس استفاده کند. منظور این است که با تغییر این دادههای کلاس متوجه میشود که بر روی عملیات b از کلاس چه تأثیری میگذارد و با اتکاء به این دستاورد، جزئیات داخلی خود را پیاده سازی میکند و یک اتصال پنهان را با کلاس Y ایجاد کرده است. در این حالت یک وابستگی قوی به صورت پنهان مابین رفتاری از کلاس Y و پیاده سازی کلاس X ایجاد شده است.
اتصال و پیوستگی مابین کلاسها باید از نوع Nil یا Export باشد؛ به این معنی که یک کلاس فقط از واسط عمومی کلاس دیگر استفاده کند یا کاری با آن نداشته باشد. (Classes should only exhibit nil or export coupling with other classes, that is, a class should only use operations in the public interface of another class or have nothing to do with that class.)بجز این دو نوع اتصال، بقیه شکلهای اتصال به طریقی اجازه دسترسی به جزئیات پیاده سازی کلاسها را اعطا میکنند. در نتیجه باعث ایجاد وابستگی مابین پیاده سازی دو کلاس میشوند. این وابستگی ما بین پیاده سازیها به محض نیاز به تغییر پیاده سازی یکی از کلاسها ، باعث به وجود آمدن مشکلات نگهداری خواهند شد.
Cohesion درون کلاسها سعی بر این دارد که مطمئن شود تمام اجزای یک کلاس به شدت باهم مرتبط هستند. تعدادی از قواعد شهودی نیز در ادامه بر این خصوصیت دلالت دارند.
قاعده شهودی 2.8
یک کلاس باید یک و تنها یک Key Abstraction را تسخیر نماید. (A class should capture one and only one key abstraction)یک Key Abstraction به عنوان یک Entity در Domain Model تعریف میشود و اغلب در غالب اسم در اسناد و مشخصات نیازمندیها ظاهر میشوند. هر کدام از آنها باید فقط به یک کلاس نگاشت پیدا کنند. اگر این نگاشت به بیش از یک کلاس انجام گیرد، در نتیجه احتمالا طراح هر تابع را به عنوان یک کلاس تسخیر کرده است. اگر بیش از یک Key Abstraction به یک کلاس نگاشت پیدا کرده باشد، پس احتمالا طراح یک سیستم متمرکز را طراحی کرده است. این کلاسها Vague Classes نامیده میشوند و باید آنها در دو کلاس یا بیشتر، تسخیر شوند.
قاعده شهودی 2.9
داده و رفتار مرتبط را در یک جا (کلاس) نگه دارید. (Keep related data and behavior in one place)در واقع هدفی که این قاعده به دنبال آن میباشد این است که هر دو جزء تشکیل دهنده یک Key Abstraction ، یعنی همان داده و رفتار، باید توسط فقط یک کلاس تسخیر شوند. با نقض این قاعده، توسعه دهنده باید با قرار داد (Convention) خاصی برنامه نویسی کند.
طراح باید کلاسهایی را که مرتبا دادههای مورد نیاز خود را با متدهای get از سایر کلاسها دریافت میکنند، شناسایی کند. زیرا این نوع کلاسها این قاعده شهودی را نقض کردهاند.
مثال واقعی
یا برعکس آن ضد الگوی Anemic Domain Model که ناقض این قاعده میباشد.
در قسمت اول اشاره کردیم این قواعد را به راحتی میتوان در صورت نیاز نقض کرد. بعضی از مواقع نیاز به طراحی فیزیکی است که باعث تغییر در طراحی منطقی شده و چه بسا میتواند باعث نقض هر کدام از این قواعد شهودی نیز شود. اگر به بخش پروژههای سایت رجوع کنید این نقض کاملا مشهود (DomainClasses و ServiceLayer موجود در طراحی فیزیکی آنها) میباشد (بیشتر از Anemic Domain Model استفاده شده است)؛ ولی نمیتوان گفت که این کار اشتباه است.
قاعده شهودی 2.10
اطلاعات نامرتبط به هم را در کلاسهای جدا از هم قرار دهید. ((Spin off nonrelated information into another class (i.e., noncommunicating behavior)
هدف از این قاعده این است که اگر کلاسی داریم که یکسری از متدهایش با بخشی از داده و یکسری دیگر با بخش دیگر دادهها کار میکنند، در واقع شما دو Key Abstraction را به یک کلاس نگاشت کرده اید (Vague Class) و باید آنها را به کلاسهای جدا نگاشت کنید.
به کلاس Dictionary در تصویر زیر توجه کنید.
برای تعداد کمی داده، بهترین پیاده سازی با استفاده از List و در مقابل برای تعداد داده زیاد بهترین پیاده سازی، استفاده از HashTable میباشد. هر یک از این پیاده سازیها، به متدهایی برای add و find کلمات نیاز دارند. طراحی سمت چپ تصویر نشان از نقض این قاعده شهودی دارد.
در طرح سمت چپ، استفاده کننده باید بداند که چقدر داده وارد کند. از طرفی نمایش جزئیات پیاده سازی در نام کلاس هم ایده خوبی نیست (طرح سمت چپ). بهترین راه حل که در مقالات آینده به آنها خواهیم رسید، بحث استفاده از ارث بری میباشد. به این ترتیب، با استفاده از یک کلاس Dictionary که نمایش جزئیات داخلی خود را مخفی کرده و در شرایط لازم نمایش جزئیات داخلی خود را تغییر دهد. منظور این است که استفاده کننده درگیر جزئیات داخلی آن نشود و این جزئیات که کدام نوع PDict یا HDict استفاده خواهد شد، از دید او مخفی باشد.
منابع و مآخذ مرتبط با کتابخانهی Angular Material
در اینجا مآخذ اصلی کار با این کتابخانه را ملاحظه میکنید که شامل اصول طراحی متریال و مخازن اصلی توسعهی آن میباشند:
Material Design Specification
- https://material.io/design
Angular Material
- https://material.angular.io
- https://github.com/angular/material2
مفاهیم پایهی طراحی متریال
چرا «زیبایی» رابط کاربری مهم است؟
در ابتدای معرفی کتابخانهی Angular Material عنوان شد که این مجموعه به همراه تعدادی کامپوننت «زیبا» است. بنابراین این سؤال مطرح میشود که چرا و یا تا چه اندازه «زیبایی» رابط کاربری اهمیت دارد؟ مهمترین دلیل آن بهبود تجربهی کاربری است. بر اساس تحقیقاتی که بر روی کاربران بسیاری صورت گرفتهاست، مشخص شدهاست کاربران، با رابطهای کاربری زیبا نتایج بهتری را از لحاظ کاهش زمان اتمام کار و تعداد خطاهای مرتبط دریافت میکنند.
اما ... طراحی برنامههای زیبا مشکل است. به همین جهت استفاده از کتابخانههای غنی مانند طراحی متریال که این امر را سهولت میبخشند، ضروری است. طراحی متریال یک زبان کامل طراحی برنامههای زیبا است. توسط گوگل طراحی شدهاست و دو هدف اصلی را دنبال میکند:
- وفاداری به اصول کلاسیک طراحی رابط کاربری
- ارائهی تجربهی کاربری یکدست و هماهنگ، در بین وسایل و اندازههای صفحات نمایشی مختلف
اصول پایهی طراحی متریال نیز شامل موارد زیر است:
- «متریال» یک متافور است و بر اساس مطالعهی نحوهی کار با کاغذ، مرکب و ارتباط بین اشیاء در دنیای واقعی پدید آمدهاست.
- اشیاء در دنیای واقعی دارای ارتباطهای ابعادی و حجمی هستند. برای مثال دو برگهی کاغذ یک فضا را اشغال نمیکنند. طراحی متریال برای نمایش این ارتباط سه بعدی بین اشیاء، از نور و سایه استفاده میکند.
- در دنیای واقعی، اشیاء از درون یکدیگر رد نمیشوند. این مورد در طراحی متریال نیز صادق است.
- طراحی متریال به همراه جعبهی رنگ مخصوص و بکارگیری فضاهای خالی و عناوین درشت بسیار مشخص، واضح و عمدی است.
- طراحی متریال به همراه حرکت و پویانمایی، جهت ارائهی مفاهیم مختلف به کاربر، جهت درک بهتر او از برنامه است.
برپایی پیشنیازهای ابتدایی کار با Angular Material
پیش از ادامهی بحث فرض بر این است که آخرین نگارش Angular CLI را نصب کردهاید و اگر پیشتر آنرا نصب کردهاید، یکبار دستور ذیل را اجرا کنید تا تمام وابستگیهای سراسری نصب شدهی در سیستم به صورت خودکار به روز رسانی شوند:
npm update -g
ng new MaterialAngularClient --routing
cd MaterialAngularClient ng serve -o
افزودن کتابخانهی Angular Material به برنامه
در طول این سری از سایت https://material.angular.io زیاد استفاده خواهیم کرد. همواره به روزترین روش افزودن کتابخانهی Angular Material به یک برنامهی موجود را در آدرس https://material.angular.io/guide/getting-started میتوانید مشاهده کنید که خلاصهی آن به صورت زیر است:
البته در Angular 6 روش تفصیلی نصب فوق که شامل 6 مرحلهاست، به صورت زیر هم خلاصه شدهاست:
ng add @angular/material
npm install --save @angular/material @angular/cdk npm install --save @angular/animations npm install --save hammerjs
- همانطور که عنوان شد، طراحی متریال مبتنی بر حرکت و پویانمایی است. به همین جهت تعدادی از کامپوننتهای آن نیاز به بستهی angular/animations را دارند که توسط دستور دوم نصب میشود.
- دستور سوم نیز کامپوننتهای slide و slider را پشتیبانی میکند (Gesture Support). البته پس نصب این وابستگی، نیاز است به فایل src/main.ts مراجعه کرده و یک سطر زیر را نیز افزود:
import "hammerjs";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule, AppRoutingModule ] }) export class AppModule { }
مدیریت بهتر import کامپوننتهای Angular Material
در ادامه به ازای هر کامپوننت Angular Material باید ماژول آنرا به لیست imports افزود که پس از مدتی به یک فایل app.module.ts بسیار شلوغ خواهیم رسید. برای مدیریت بهتر این فایل، از روش مطرح شدهی در مطلب «سازماندهی برنامههای Angular» استفاده خواهیم کرد.
به همین جهت دو پوشهی core و shared را درون پوشهی src/app ایجاد میکنیم:
محتویات فایل src\app\core\core.module.ts به صورت زیر است:
import { CommonModule } from "@angular/common"; import { NgModule, Optional, SkipSelf } from "@angular/core"; import { RouterModule } from "@angular/router"; @NgModule({ imports: [CommonModule, RouterModule], exports: [ // components that are used in app.component.ts will be listed here. ], declarations: [ // components that are used in app.component.ts will be listed here. ], providers: [ /* ``No`` global singleton services of the whole app should be listed here anymore! Since they'll be already provided in AppModule using the `tree-shakable providers` of Angular 6.x+ (providedIn: 'root'). This new feature allows cleaning up the providers section from the CoreModule. But if you want to provide something with an InjectionToken other that its class, you still have to use this section. */ ] }) export class CoreModule { constructor(@Optional() @SkipSelf() core: CoreModule) { if (core) { throw new Error("CoreModule should be imported ONLY in AppModule."); } } }
محتویات فایل src\app\shared\shared.module.ts نیز به صورت زیر است:
import { CommonModule } from "@angular/common"; import { HttpClientModule } from "@angular/common/http"; import { ModuleWithProviders, NgModule } from "@angular/core"; import { FormsModule } from "@angular/forms"; @NgModule({ imports: [ CommonModule, FormsModule, HttpClientModule ], entryComponents: [ // All components about to be loaded "dynamically" need to be declared in the entryComponents section. ], declarations: [ // common and shared components/directives/pipes between more than one module and components will be listed here. ], exports: [ // common and shared components/directives/pipes between more than one module and components will be listed here. CommonModule, FormsModule, HttpClientModule, ] /* No providers here! Since they’ll be already provided in AppModule. */ }) export class SharedModule { static forRoot(): ModuleWithProviders { // Forcing the whole app to use the returned providers from the AppModule only. return { ngModule: SharedModule, providers: [ /* All of your services here. It will hold the services needed by `itself`. */] }; } }
import { CoreModule } from "./core/core.module"; import { SharedModule } from "./shared/shared.module"; @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule, CoreModule, SharedModule.forRoot(), AppRoutingModule ] }) export class AppModule { }
import { CdkTableModule } from "@angular/cdk/table"; import { NgModule } from "@angular/core"; import { MatAutocompleteModule, MatButtonModule, MatButtonToggleModule, MatCardModule, MatCheckboxModule, MatChipsModule, MatDatepickerModule, MatDialogModule, MatExpansionModule, MatFormFieldModule, MatGridListModule, MatIconModule, MatInputModule, MatListModule, MatMenuModule, MatNativeDateModule, MatPaginatorModule, MatProgressBarModule, MatProgressSpinnerModule, MatRadioModule, MatRippleModule, MatSelectModule, MatSidenavModule, MatSliderModule, MatSlideToggleModule, MatSnackBarModule, MatSortModule, MatStepperModule, MatTableModule, MatTabsModule, MatToolbarModule, MatTooltipModule, } from "@angular/material"; @NgModule({ imports: [ MatAutocompleteModule, MatButtonModule, MatButtonToggleModule, MatCardModule, MatCheckboxModule, MatChipsModule, MatDatepickerModule, MatDialogModule, MatExpansionModule, MatFormFieldModule, MatGridListModule, MatIconModule, MatInputModule, MatListModule, MatMenuModule, MatNativeDateModule, MatPaginatorModule, MatProgressBarModule, MatProgressSpinnerModule, MatRadioModule, MatRippleModule, MatSelectModule, MatSidenavModule, MatSliderModule, MatSlideToggleModule, MatSnackBarModule, MatStepperModule, MatSortModule, MatTableModule, MatTabsModule, MatToolbarModule, MatTooltipModule, CdkTableModule ], exports: [ MatAutocompleteModule, MatButtonModule, MatButtonToggleModule, MatCardModule, MatCheckboxModule, MatChipsModule, MatDatepickerModule, MatDialogModule, MatExpansionModule, MatGridListModule, MatIconModule, MatInputModule, MatListModule, MatMenuModule, MatNativeDateModule, MatPaginatorModule, MatProgressBarModule, MatProgressSpinnerModule, MatRadioModule, MatRippleModule, MatSelectModule, MatSidenavModule, MatSliderModule, MatSlideToggleModule, MatSnackBarModule, MatStepperModule, MatSortModule, MatTableModule, MatTabsModule, MatToolbarModule, MatTooltipModule, CdkTableModule ] }) export class MaterialModule { }
سپس MaterialModule را نیز به قسمتهای imports و exports فایل src\app\shared\shared.module.ts اضافه خواهیم کرد:
import { MaterialModule } from "./material.module"; @NgModule({ imports: [ CommonModule, FormsModule, HttpClientModule, MaterialModule ], exports: [ // common and shared components/directives/pipes between more than one module and components will be listed here. CommonModule, FormsModule, HttpClientModule, MaterialModule ] }) export class SharedModule { }
تا اینجا جهت اطمینان از اجرای برنامه، دستور ng serve -o را از ابتدا اجرا کنید.
افزودن چند کامپوننت مقدماتی متریال به برنامه
بهترین روش کار با این مجموعه، بررسی مستندات آن در سایت https://material.angular.io/components است. برای مثال برای افزودن دکمه، به مستندات آن مراجعه کرده و بر روی دکمهی view source کلیک میکنیم:
سپس کدهای قسمت HTML آنرا به برنامه و فایل app.component.html اضافه خواهیم کرد:
<button mat-button>Click me!</button>
<mat-checkbox>Check me!</mat-checkbox>
البته شکل ظاهری آنها تا اینجا آنچنان مطلوب نیست. برای رفع این مشکل، نیاز است یک قالب را به این کنترلها و کامپوننتها اعمال کرد. به همین جهت فایل styles.css واقع در ریشهی برنامه را گشوده و قالب پیشفرض متریال را به آن اضافه میکنیم:
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
پس از اعمال قالب، اکنون است که شکل ظاهری کنترلهای آن بسیار بهتر شدهاند و همچنین کار با آنها به همراه پویانمایی نیز شدهاست:
افزودن آیکنهای متریال به برنامه
مرحلهی آخر این تنظیمات، افزودن آیکنهای متریال به برنامهاست. برای این منظور فایل src\index.html را گشوده و یک سطر ذیل را به head اضافه کنید:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<button mat-button> <mat-icon>face</mat-icon> Click me! </button> <mat-checkbox>Check me!</mat-checkbox>
لیست کامل این آیکنها را به همراه توضیحات تکمیلی آنها، در آدرس ذیل میتوانید ملاحظه کنید:
http://google.github.io/material-design-icons
البته چون ما نمیخواهیم این آیکنها را از وب بارگذاری کنیم، برای نصب محلی آنها ابتدا دستور زیر را در ریشهی پروژه صادر کنید:
npm install material-design-icons --save
همانطور که مشاهده میکنید، برای استفادهی از این فایلهای آیکن فونت محلی، تنها کافی است فایل material-icons.css را به برنامه معرفی کنیم. برای این منظور فایل angular.json را گشوده و قسمت styles آنرا به صورت زیر تکمیل میکنیم:
"styles": [ "node_modules/material-design-icons/iconfont/material-icons.css", "src/styles.css" ],
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-01.zip
برای اجرای آن نیز ابتدا فایل restore.bat و سپس فایل ng-serve.bat را اجرا کنید.
معرفی سرویس MatDialog
توسط سرویس MatDialog میتوان modal dialogs بستهی Angular Material را نمایش داد که به همراه طراحی متریال و پویانمایی مخصوص آن است.
let dialogRef = dialog.open(UserProfileComponent, { height: '400px’, width: '600px’ });
dialogRef.afterClosed().subscribe(result => { console.log(`Dialog result: ${result}`); }); dialogRef.close('value');
در این مثال اگر dialogRef را با متد close و پارامتر value فراخوانی کنیم، سبب بسته شدن این دیالوگ خواهیم شد. این پارامتر در قسمت Dialog result پیام دریافتی پس از بسته شدن دیالوگ نیز قابل دسترسی است.
کامپوننتهایی که توسط سرویس MatDialog نمایش داده میشوند، میتوانند توسط سرویس جنریک MatDialogRef، صفحهی دیالوگ باز شده را ببندند:
@Component({/* ... */}) export class YourDialog { constructor(public dialogRef: MatDialogRef<YourDialog>) { } closeDialog() { this.dialogRef.close('Value….!’); } }
نحوهی طراحی یک دیالوگ نیز به کمک تعدادی کامپوننت و دایرکتیو میسر است:
<h2 mat-dialog-title>Delete all</h2> <mat-dialog-content>Are you sure?</mat-dialog-content> <mat-dialog-actions> <button mat-button mat-dialog-close>No</button> <!-- Can optionally provide a result for the closing dialog. --> <button mat-button [mat-dialog-close]="true">Yes</button> </mat-dialog-actions>
ایجاد دکمهی نمایش دیالوگ افزودن تماسها و کاربران جدید
قبل از هر کاری نیاز است دکمهی افزودن یک کاربر جدید را به صفحه اضافه کنیم. برای اینکار یک منوی ویژه را در سمت راست، بالای صفحه ایجاد میکنیم. بنابراین ابتدا به مستندات toolbar و menu مراجعه میکنیم تا با نحوهی تعریف دکمهها و منوها به toolbar آشنا شویم. سپس فایل قالب toolbar\toolbar.component.html را به صورت زیر تکمیل میکنیم:
<mat-toolbar color="primary"> <button mat-button fxHide fxHide.xs="false" (click)="toggleSidenav.emit()"> <mat-icon>menu</mat-icon> </button> <span>Contact Manager</span> <span fxFlex="1 1 auto"></span> <button mat-button [matMenuTriggerFor]="menu"> <mat-icon>more_vert</mat-icon> </button> <mat-menu #menu="matMenu"> <button mat-menu-item>New Contact</button> </mat-menu> </mat-toolbar>
سپس ابتدا یک mat-button را با آیکن more_vert (آیکن علامت بیشتر عمودی) تعریف کردهایم:
این دکمه توسط ویژگی matMenuTriggerFor به template reference variable ایی به نام menu متصل شدهاست تا با کلیک بر روی آن، این mat-menu را نمایش دهد:
ایجاد دیالوگ افزودن تماسها و کاربران جدید
پس از تعریف دکمه و منویی که سبب نمایش عبارت افزودن یک تماس جدید میشوند، به رخداد کلیک آن متدی را جهت نمایش صفحهی دیالوگ جدید اضافه میکنیم:
<button mat-menu-item (click)="openAddContactDialog()">New Contact</button>
ng g c contact-manager/components/new-contact-dialog --no-spec
import { NewContactDialogComponent } from "./components/new-contact-dialog/new-contact-dialog.component"; @NgModule({ declarations: [ NewContactDialogComponent], entryComponents: [ NewContactDialogComponent ] }) export class ContactManagerModule { }
import { Component, EventEmitter, OnInit, Output } from "@angular/core"; import { MatDialog } from "@angular/material"; import { NewContactDialogComponent } from "../new-contact-dialog/new-contact-dialog.component"; @Component({ selector: "app-toolbar", templateUrl: "./toolbar.component.html", styleUrls: ["./toolbar.component.css"] }) export class ToolbarComponent implements OnInit { @Output() toggleSidenav = new EventEmitter<void>(); constructor(private dialog: MatDialog) { } ngOnInit() { } openAddContactDialog(): void { const dialogRef = this.dialog.open(NewContactDialogComponent, { width: "450px" }); dialogRef.afterClosed().subscribe(result => { console.log("The dialog was closed", result); }); } }
تکمیل قالب کامپوننت تماس جدید
در ادامه میخواهیم فرم افزودن یک تماس جدید را به همراه فیلدهای ورودی آن، به قالب new-contact-dialog.component.html اضافه کنیم:
<h2 mat-dialog-title>Add new contact</h2> <mat-dialog-content> <div fxLayout="column"> </div> </mat-dialog-content> <mat-dialog-actions> <button mat-button color="primary" (click)="save()"> <mat-icon>save</mat-icon> Save </button> <button mat-button color="primary" (click)="dismiss()"> <mat-icon>cancel</mat-icon> Cancel </button> </mat-dialog-actions>
import { Component, OnInit } from "@angular/core"; import { MatDialogRef } from "@angular/material"; @Component() export class NewContactDialogComponent implements OnInit { constructor( private dialogRef: MatDialogRef<NewContactDialogComponent> ) { } ngOnInit() { } save() { } dismiss() { this.dialogRef.close(null); } }
تا اینجا اگر برنامه را اجرا کنیم، به چنین شکلی خواهیم رسید:
تکمیل فیلدهای ورود اطلاعات فرم ثبت یک تماس جدید
تا اینجا ساختار فرم دیالوگ ثبت اطلاعات جدید را تکمیل کردیم. این فرم، به شیء user متصل خواهد شد. همچنین لیستی از avatars را هم جهت انتخاب، نمایش میدهد. به همین جهت این دو خاصیت عمومی را به کدهای کامپوننت آن اضافه میکنیم:
import { User } from "../../models/user"; @Component() export class NewContactDialogComponent implements OnInit { avatars = ["user1", "user2", "user3", "user4", "user5", "user6", "user7", "user8"]; user: User = { id: 0, birthDate: new Date(), name: "", avatar: "", bio: "", userNotes: null };
الف) فیلد نمایش و انتخاب avatar کاربر
<mat-form-field> <mat-select placeholder="Avatar" [(ngModel)]="user.avatar"> <mat-select-trigger> <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.avatar }} </mat-select-trigger> <mat-option *ngFor="let avatar of avatars" [value]="avatar"> <mat-icon svgIcon="{{avatar}}"></mat-icon> {{ avatar }} </mat-option> </mat-select> </mat-form-field>
در اینجا از کامپوننت mat-select برای انتخاب avatar کاربر استفاده شدهاست که نتیجهی نهایی انتخاب آن به خاصیت user.avatar متصل شدهاست.
گزینههای این لیست (mat-option) بر اساس آرایهی avatars که در کامپوننت تعریف کردیم، تامین میشوند که در اینجا از mat-icon برای نمایش آیکن مرتبط نیز استفاده شدهاست. در این مورد در قسمت قبل چهارم، بخش «بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material» بیشتر توضیح داده شدهاست.
کار mat-select-trigger، سفارشی سازی برچسب نمایشی این کنترل است.
ب) فیلد دریافت نام کاربر به همراه اعتبارسنجی آن
<mat-form-field> <input matInput placeholder="Name" #name="ngModel" [(ngModel)]="user.name" required> <mat-error *ngIf="name.invalid && name.touched">You must enter a name</mat-error> </mat-form-field>
در اینجا فیلد نام کاربر، به user.name متصل و همچنین توسط ویژگی required، پر کردن آن الزامی اعلام شدهاست. به همین جهت در تصویر فوق یک ستاره را نیز کنار آن مشاهده میکند که به صورت خودکار توسط Angular Material نمایش داده شدهاست.
از کامپوننت mat-error برای نمایش خطاهای اعتبارسنجی یک فیلد استفاده میشود که نمونهای از آنرا در اینجا با بررسی خواص invalid و touched فیلد نام که بر اساس ویژگی required فعال میشوند، مشاهده میکنید. بدیهی است در اینجا به هر تعدادی که نیاز است میتوان mat-error را قرار داد.
ج) فیلد دریافت تاریخ تولد کاربر توسط یک date picker
<mat-form-field> <input matInput [matDatepicker]="picker" placeholder="Born" [(ngModel)]="user.birthDate"> <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle> <mat-datepicker #picker></mat-datepicker> </mat-form-field>
در اینجا از کامپوننت mat-datepicker برای انتخاب تاریخ تولید یک شخص استفاده شدهاست و نتیجهی آن به خاصیت user.birthDate متصل خواهد شد.
برای افزودن آن ابتدا یک mat-datepicker را به mat-form-field اضافه میکنیم. سپس یک template reference variable را به آن نسبت خواهیم داد. از آن هم در فیلد ورودی با انتساب آن به ویژگی matDatepicker و هم در کامپوننت mat-datepicker-toggle که سبب نمایش آیکن انتخاب تقویم میشود، در ویژگی for آن استفاده خواهیم کرد.
د) فیلد چند سطری دریافت توضیحات و شرححال کاربر
<mat-form-field> <textarea matInput placeholder="Bio" [(ngModel)]="user.bio"></textarea> </mat-form-field>
در اینجا برای دریافت توضیحات چندسطری، از یک text area استفاده شدهاست که به خاصیت user.bio متصل است.
بنابراین همانطور که ملاحظه میکنید، روش طراحی فرمهای Angular Material ویژگیهای خاص خودش را دارد:
- دایرکتیو matInput را میتوان به المانهای استاندارد input و textarea اضافه کرد تا داخل mat-form-field نمایش داده شوند. این mat-form-field است که کار اعمال CSS ویژهی طراحی متریال را انجام میدهد و امکان نمایش پیامهای خطای اعتبارسنجی و پویانمایی ورود اطلاعات را سبب میشود.
- قسمت mat-dialog-content را توسط fxLayout به حالت ستونی تنظیم کردیم:
<mat-dialog-content> <div fxLayout="column"> </div> </mat-dialog-content>
برای مثال اگر خواستید المانهای فرم با فاصلهی بیشتری از هم قرار بگیرند، میتوان از fxLayoutGap استفاده کرد که در مورد آن در قسمت دوم «معرفی Angular Flex layout» بیشتر توضیح داده شد.
تکمیل سرویس کاربران جهت ذخیرهی اطلاعات تماس کاربر جدید
در ادامه میخواهیم با کلیک کاربر بر روی دکمهی Save، ابتدا این اطلاعات به سمت سرور ارسال و سپس در سمت سرور ذخیره شوند. پس از آن، Id این کاربر جدید به سمت کلاینت بازگشت داده شود، دیالوگ جاری بسته و در آخر این شیء جدید به لیست تماسهای نمایش دادهی شدهی در sidenav اضافه گردد.
الف) تکمیل سرویس Web API سمت سرور
در ابتدا متد Post را به Web API برنامه جهت ذخیره سازی اطلاعات User ارسالی از سمت کلاینت اضافه میکنیم. کدهای کامل آنرا از فایل پیوستی انتهای بحث میتوانید دریافت کنید:
namespace MaterialAspNetCoreBackend.WebApp.Controllers { [Route("api/[controller]")] public class UsersController : Controller { private readonly IUsersService _usersService; public UsersController(IUsersService usersService) { _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService)); } [HttpPost] public async Task<IActionResult> Post([FromBody] User user) { if (!ModelState.IsValid) { return BadRequest(ModelState); } await _usersService.AddUserAsync(user); return Created("", user); } } }
ب) تکمیل سرویس کاربران سمت کلاینت
سپس به فایل user.service.ts مراجعه کرده و دو تغییر زیر را به آن اضافه میکنیم:
@Injectable({ providedIn: "root" }) export class UserService { private usersSource = new BehaviorSubject<User>(null); usersSourceChanges$ = this.usersSource.asObservable(); constructor(private http: HttpClient) { } addUser(user: User): Observable<User> { const headers = new HttpHeaders({ "Content-Type": "application/json" }); return this.http .post<User>("/api/users", user, { headers: headers }).pipe( map(response => { const addedUser = response || {} as User; this.notifyUsersSourceHasChanged(addedUser); return addedUser; }), catchError((error: HttpErrorResponse) => throwError(error)) ); } notifyUsersSourceHasChanged(user: User) { this.usersSource.next(user); } }
return Created("", user);
بنابراین نیاز است از طریق این سرویس به کامپوننت sidenav، در مورد تغییرات لیست کاربران اطلاعات رسانی کنیم که روش کار آنرا پیشتر در مطلب «صدور رخدادها از سرویسها به کامپوننتها در برنامههای Angular» نیز مرور کردهایم. برای این منظور یک BehaviorSubject از نوع User را تعریف کردهایم که اشتراک به آن از طریق خاصیت عمومی usersSourceChanges میسر است. هر زمانیکه متد next آن فراخوانی شود، تمام مشترکین به آن، از افزوده شدن کاربر جدید، به همراه اطلاعات کامل آن مطلع خواهند شد.
ج) تکمیل متد save کامپوننت new-contact-dialog
پس از تکمیل سرویس کاربران جهت افزودن متد addUser به آن، اکنون میتوانیم از آن در کامپوننت دیالوگ افزودن اطلاعات تماس جدید استفاده کنیم:
import { UserService } from "../../services/user.service"; @Component() export class NewContactDialogComponent { user: User = { id: 0, birthDate: new Date(), name: "", avatar: "", bio: "", userNotes: null }; constructor( private dialogRef: MatDialogRef<NewContactDialogComponent>, private userService: UserService ) { } save() { this.userService.addUser(this.user).subscribe(data => { console.log("Saved user", data); this.dialogRef.close(data); }); } }
د) تکمیل کامپوننت sidenav جهت واکنش نشان دادن به افزوده شدن اطلاعات تماس جدید
اکنون که سرویس کاربران به صفحه دیالوگ افزودن اطلاعات یک تماس جدید متصل شدهاست، نیاز است بتوانیم اطلاعات کاربر جدید را به لیست تماسهای sidenav اضافه کنیم. به همین جهت به sidenav.component مراجعه کرده و مشترک usersSourceChanges سرویس کاربران خواهیم شد:
import { UserService } from "../../services/user.service"; @Component() export class SidenavComponent implements OnInit, OnDestroy { users: User[] = []; subscription: Subscription | null = null; constructor( private userService: UserService) { } ngOnInit() { this.subscription = this.userService.usersSourceChanges$.subscribe(user => { if (user) { this.users.push(user); } }); } ngOnDestroy() { if (this.subscription) { this.subscription.unsubscribe(); } } }
استفاده از کامپوننت Snackbar جهت نمایش موفقیت آمیز بودن ثبت اطلاعات
متد save کامپوننت دیالوگ یک تماس جدید را به صورت زیر تکمیل کردیم:
save() { this.userService.addUser(this.user).subscribe(data => { console.log("Saved user", data); this.dialogRef.close(data); });
openAddContactDialog(): void { const dialogRef = this.dialog.open(NewContactDialogComponent, { width: "450px" }); dialogRef.afterClosed().subscribe(result => { console.log("The dialog was closed", result); }); }
کدهای کامل این تغییرات را در ذیل مشاهده میکنید:
@Component() export class ToolbarComponent { @Output() toggleSidenav = new EventEmitter<void>(); constructor(private dialog: MatDialog, private snackBar: MatSnackBar, private router: Router) { } openAddContactDialog(): void { const dialogRef = this.dialog.open(NewContactDialogComponent, { width: "450px" }); dialogRef.afterClosed().subscribe((result: User) => { console.log("The dialog was closed", result); if (result) { this.openSnackBar(`${result.name} contact has been added.`, "Navigate").onAction().subscribe(() => { this.router.navigate(["/contactmanager", result.id]); }); } }); } openSnackBar(message: string, action: string): MatSnackBarRef<SimpleSnackBar> { return this.snackBar.open(message, action, { duration: 5000, }); } }
برای گشودن snackbar که نمونهای از آنرا در تصویر فوق ملاحظه میکنید، ابتدا نیاز است سرویس MatSnackBar را به سازندهی کلاس تزریق کرد. سپس توسط آن میتوان یک کامپوننت مستقل را همانند دیالوگها نمایش داد و یا میتوان یک متن را به همراه یک Action منتسب به آن، به کاربر نمایش داد؛ مانند متد openSnackBar که در کامپوننت فوق از آن استفاده میشود. این متد در رخداد پس از بسته شدن dialog، نمایش داده شدهاست.
پارامتر اول آن پیامی است که توسط snackbar نمایش داده میشود و پارامتر دوم آن، برچسب دکمه مانندی است کنار این پیام، که سبب انجام عملی خواهد شد و در اینجا به آن Action گفته میشود. برای مدیریت آن باید متد onAction را فراخوانی کرد و مشترک آن شد. در این حالت اگر کاربر بر روی این دکمهی action کلیک کند، سبب هدایت خودکار او به صفحهی نمایش جزئیات اطلاعات تماس کاربر خواهیم شد. به همین جهت سرویس Router نیز به سازندهی کلاس تزریق شدهاست تا بتوان از متد navigate آن استفاده کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-05.zip
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.