if (variable == null) { variable = expression; // C# 1..7 }
variable ??= expression; // C# 8
if (variable == null) { variable = expression; // C# 1..7 }
variable ??= expression; // C# 8
ما طراحی میکنیم تا علاوه بر نیازهای عملیاتی، به نیازهای غیر عملیاتی (Non Functional Requirements) نیز فکر کنیم؛ در حالیکه در زمان برنامه نویسی صرفا به Functionality فکر میکنیم.
کتاب Object Oriented Design Heuristics اولین کتاب در زمینه طراحی و توسعه شیء گرا میباشد. خواندن آن برای برنامه نویسان در هر رده ای که هستند، مفید خواهد بود و میتوانند از این Heuristicها (قواعد شهودی) به عنوان ابزاری برای تبدیل شدن به یک توسعه دهنده برتر، استفاده کنند.
در این کتاب بیشتر، بهبود طراحی شیء گرا هدف قرار داده شدهاست و در این راستا بیش از 60 دستورالعمل که هیچ وابستگی به زبان خاصی هم ندارند، ارائه شده است. قواعد شهودی در واقع قوانین سخت گیرانهای نیستند. بلکه میتوان آنها را به عنوان یک مکانیزم هشدار در نظر گرفت که در زمان نیاز حتی میتوان آنها را نقض کرد.
پیشنهاد میکنم حداقل برای اینکه ادبیات فنی خود را سامان ببخشید و با ادبیات یکسانی باهم صحبت کنیم، این کتاب را مطالعه کنید.
پارادایم شیء گرا از مفاهیم کلاس و آبجکت، به عنوان بلوکهای ساختاری پایهای در شکل گیری یک مدل سازگار و استوار برای تحلیل، طراحی و پیاده سازی نرم افزار، استفاده میکند.
این مفاهیم را با یک مثال واقعی، بهتر میتوان شرح داد. یک اتاق پر از جمعیت را درنظر بگیرید؛ اگر شما میپرسیدید «چه تعداد از حاضرین در این اتاق میتوانند یک ساعت زنگدار(alarm clock ) را با در دست داشتن تمام قطعات آن، بسازند؟» در بهترین حالت یک یا دو نفر تمایل داشتند دست خود را بالا ببرند. اگر در همین اتاق میپرسیدید، «چه تعداد از حاضرین در این اتاق میتوانند یک ساعت زنگدار را برای ساعت 9 صبح تنظیم کنند؟» بدون شک بیشتر جمعیت تمایل داشتند دست خود را بالا ببرند.
آیا نامعقول نیست که این تعداد جمعیت زیاد، ادعا دارند که میتوانند از ساعت زنگدار استفاده کنند، درحالیکه حتی نمیتوانند یک ساعت زنگدار بسازند؟ پاسخ بی درنگ برای این سوال «البته که نه! سوال شما نامعقول است» میباشد.
در دنیای واقعی خیلی چیزها هستند که ما میتوانیم از آنها استفاده کنیم، بدون آنکه دانشی درباره پیاده سازی آنها داشته باشیم؛ مانند: یخچالها، اتومبیلها، دستگاههای فتوکپی، کامپیوترها و غیره. چون آنها برای استفاده شدن از طریق واسط عمومی خودشان، تعریف و طراحی شدهاند. لذا حتی بدون داشتن دانشی از پیاده سازی آنها، استفاده از آنها آسان میباشد. این واسط عمومی وابسته به دستگاه مورد نظر است. اما جزئیات پیاده سازی دستگاه را از دید کاربرانش پنهان میکند. این استراتژی طراحی، چیزی است که به سازنده اجازه میدهد بدون آنکه کاربران رنجیده شوند، با آزادی عمل، 60 مؤلفه کوچک استفاده شده در ساخت ساعت زنگدار را تعویض کند.
مثال دیگری از واسط عمومی در مقابل جزئیات پیاده سازی، میتواند در حوزه اتومبیلها دیده شود. زمانیکه تولید کنندگان اتومبیل از سیستمهای احتراق مکانیکی به سمت سیستمهای احتراق الکترونیکی کوچ کردند، تعداد خیلی کمی از کاربران اتومبیلها نگران این موضوع بودند. اما چرا؟ چون واسط عمومی آنها مانند سابق ماند و تنها پیاده سازی تغییر کرد. فرض کنید که شما به قصد خرید اتومبیل به یک فروشنده اتومبیل مراجعه میکنید و فروشنده یک سوئیچ را به شما داده و از شما میخواهد برای تست آن را برانید. بعد از تلاشی که برای استارت زدن داشتید، فروشنده اعلام میکند که در این مدل برای استارت زدن باید ابتدا کاپوت را بالا زده و دکمه قرمز را فشار دهید. در این حالت، بدلیل اینکه واسط عمومی اتومبیل دستخوش تغییر بوده است، باعث ناراحتی شما خواهد شد.
این فلسفه، دقیقا یکی از ایدههای پایهای در پارادایم شیءگرا میباشد. تمام جزئیات پیاده سازی در سیستم شما باید در پشت یک واسط عمومی مستحکم و سازگار، از کاربران آنها پنهان باشد. نیاز کاربران، دانستن درباره واسط عمومی میباشد؛ اما هرگز مجاز به دیدن جزئیات پیاده سازی آنها نیستند. با این روش، پیاده ساز میتواند به هرشکلی که مناسب است، پیاده سازی را تغییر دهد؛ درحالیکه واسط عمومی مانند سابق میباشد. به عنوان مسافری که مکرر سفر میکنم، به شما اطمینان میدهم که استفاده از ساعتهای زنگدار با وجود عدم اطلاع از پیاده سازی آنها، فواید عظیمی دارند. در هتلهای زیادی که از دسته بندیهای گستردهای از ساعتها مانند الکتریکی، قابل کوک (windup)، باتری خور، در هر دو مدل دیجیتال و آنالوگ استفاده میکنند، اقامت کردهام. یکبار هم اتفاق نیفتادهاست در حالیکه در هواپیما نشسته باشم، نگران این باشم که قادر نخواهم بود از ساعت زنگی اتاقم در هتل استفاده کنم.
بیشتر خوانندگان این کتاب، با وجود اینکه در نزدیکی آنها شاید ساعت زنگداری هم نباشد، ولی منظور بنده را با عبارت «ساعت زنگدار» متوجه شدند. به چه دلیل؟ شما در زندگی خودتان ساعتهای زنگدار زیادی را میبینید و متوجه میشوید که همه آنها از یکسری خصوصیات مشترک مانند زمان، یک زمان هشدار و طراحیای که مشخص میکند هشدار روشن یا خاموش است، بهره میبرند. همچنین متوجه میشوید که همه ساعتهای زنگداری که دیدهاید امکان تنظیم کردن زمان، تنظیم زمان هشدار و روشن و خاموش کردن هشدار را به شما میدهند. در نتیجه، شما الان مفهومی را به نام «ساعت زنگدار» دارید که مفهومی را از داده و رفتار، در یک بسته بندی مرتب برای همه ساعتهای زنگدار، تسخیر میکند. این مفهوم به عنوان یک Class (کلاس) شناخته میشود. یک ساعت زنگدار فیزیکی که شما در دست خود آن را نگه داشتهاید، یک Object (وهله، Instance) ای از کلاس ساعت زنگدار میباشد. رابطه بین مفهوم کلاس و وهله، Instantiation Relationship (وهله سازی) نام دارد. به یک object، ساعت زنگدار وهله سازی شده (Instantiated) از کلاس ساعت زنگدار گفته میشود؛ در حالیکه از کلاس ساعت زنگدار به عنوان تعمیم (Generalization) از همه objectهای کلاس ساعت زنگدار که شما با آنها روبرو شدهاید، یاد میشود.
اگر من به شما میگفتم که ساعت زنگدارم از روی پاتختی (میز کوچک کنار تخت که دارای کشو میباشد) من پرید، من را گاز گرفت، سپس گربهی همسایه را دنبال کرد، قطعا مرا دیوانه به حساب میآوردید. اگر به شما میگفتم که سگ من کارهای مشابهای را انجام میدهد، کاملا منطقی میبود. چون نام یک کلاس تنها به مجموعهای از خواص اشاره نمیکند، بلکه رفتارهای موجودیت (entity) را نیز مشخص میکند. این رابطه دوسویه بین داده و رفتار، اساس پارادایم شیء گرا میباشد.
یک کلاس را می توان با 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 نیز باید تغییر کنند. حال اگر دادهها پنهان بودند و عمومی نبودند، در نتیجه فقط لازم بود پیاده سازهای این کلاس، کد خود را تغییر دهند.
using System; using System.Web; namespace OPMLCleaner { public static class UrlNormalization { public static bool AreTheSameUrls(this string url1, string url2) { url1 = url1.NormalizeUrl(); url2 = url2.NormalizeUrl(); return url1.Equals(url2); } public static bool AreTheSameUrls(this Uri uri1, Uri uri2) { var url1 = uri1.NormalizeUrl(); var url2 = uri2.NormalizeUrl(); return url1.Equals(url2); } public static string[] DefaultDirectoryIndexes = new[] { "default.asp", "default.aspx", "index.htm", "index.html", "index.php" }; public static string NormalizeUrl(this Uri uri) { var url = urlToLower(uri); url = limitProtocols(url); url = removeDefaultDirectoryIndexes(url); url = removeTheFragment(url); url = removeDuplicateSlashes(url); url = addWww(url); url = removeFeedburnerPart(url); return removeTrailingSlashAndEmptyQuery(url); } public static string NormalizeUrl(this string url) { return NormalizeUrl(new Uri(url)); } private static string removeFeedburnerPart(string url) { var idx = url.IndexOf("utm_source=", StringComparison.Ordinal); return idx == -1 ? url : url.Substring(0, idx - 1); } private static string addWww(string url) { if (new Uri(url).Host.Split('.').Length == 2 && !url.Contains("://www.")) { return url.Replace("://", "://www."); } return url; } private static string removeDuplicateSlashes(string url) { var path = new Uri(url).AbsolutePath; return path.Contains("//") ? url.Replace(path, path.Replace("//", "/")) : url; } private static string limitProtocols(string url) { return new Uri(url).Scheme == "https" ? url.Replace("https://", "http://") : url; } private static string removeTheFragment(string url) { var fragment = new Uri(url).Fragment; return string.IsNullOrWhiteSpace(fragment) ? url : url.Replace(fragment, string.Empty); } private static string urlToLower(Uri uri) { return HttpUtility.UrlDecode(uri.AbsoluteUri.ToLowerInvariant()); } private static string removeTrailingSlashAndEmptyQuery(string url) { return url .TrimEnd(new[] { '?' }) .TrimEnd(new[] { '/' }); } private static string removeDefaultDirectoryIndexes(string url) { foreach (var index in DefaultDirectoryIndexes) { if (url.EndsWith(index)) { url = url.TrimEnd(index.ToCharArray()); break; } } return url; } } }
<?xml version="1.0" encoding="utf-8"?> <opml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="1.0"> <body> <outline text="آی تی ایرانی"> <outline type="rss" text="فید کلی آخرین نظرات، مطالب، اشتراکها و پروژههای .NET Tips" title="فید کلی آخرین نظرات، مطالب، اشتراکها و پروژههای .NET Tips" xmlUrl="https://www.dntips.ir/Feed/LatestChanges" htmlUrl="https://www.dntips.ir/" /> </outline> </body> </opml>
using System.Xml.Serialization; namespace OPMLCleaner { [XmlType(TypeName="outline")] public class Opml { [XmlAttribute(AttributeName="text")] public string Text { get; set; } [XmlAttribute(AttributeName = "title")] public string Title { get; set; } [XmlAttribute(AttributeName = "type")] public string Type { get; set; } [XmlAttribute(AttributeName = "xmlUrl")] public string XmlUrl { get; set; } [XmlAttribute(AttributeName = "htmlUrl")] public string HtmlUrl { get; set; } } }
var document = XDocument.Load("it-92-03-01.opml"); var results = (from node in document.Descendants("outline") where node.Attribute("htmlUrl") != null && node.Parent.Attribute("text") != null && node.Parent.Attribute("text").Value == "آی تی ایرانی" select new Opml { HtmlUrl = (string)node.Attribute("htmlUrl"), Text = (string)node.Attribute("text"), Title = (string)node.Attribute("title"), Type = (string)node.Attribute("type"), XmlUrl = (string)node.Attribute("xmlUrl") }).ToList();
using System.Collections.Generic; namespace OPMLCleaner { public class OpmlCompare : EqualityComparer<Opml> { public override bool Equals(Opml x, Opml y) { return UrlNormalization.AreTheSameUrls(x.HtmlUrl, y.HtmlUrl); } public override int GetHashCode(Opml obj) { return obj.HtmlUrl.GetHashCode(); } } }
var distinctResults = results.Distinct(new OpmlCompare()).ToList();
public static class TestDependencyScope { private static IContainer _currentNestedContainer; public static void Begin() { if (_currentNestedContainer != null) throw new Exception("Cannot begin test dependency scope. Another dependency scope is still in effect."); _currentNestedContainer = IoC.Container.GetNestedContainer(); } public static IContainer CurrentNestedContainer { get { if (_currentNestedContainer == null) throw new Exception($"Cannot access the {nameof(CurrentNestedContainer)}. There is no dependency scope in effect."); return _currentNestedContainer; } } public static void End() { if (_currentNestedContainer == null) throw new Exception("Cannot end test dependency scope. There is no dependency scope in effect."); _currentNestedContainer.Dispose(); _currentNestedContainer = null; } }
public class ContainerPerTestCaseAttribute : Attribute, ITestAction { public void BeforeTest(ITest test) { TestDependencyScope.Begin(); } public void AfterTest(ITest test) { TestDependencyScope.End(); } public ActionTargets Targets => ActionTargets.Test; }
[PopulateHttpContext] [ContainerPerTestCase] [Transactional] [TestFixture] public class IntegratedTestBase { [SetUp] public void EachTestSetUp() { BeforeEachTest(); } [TearDown] public void EachTestTearDown() { AfterEachTest(); } protected virtual void BeforeEachTest() { } protected virtual void AfterEachTest() { } protected void UsingUnitOfWork(Action<IUnitOfWork> action) { IoC.Container.Using((IUnitOfWork uow)=> { uow.DisableAllFilters(); action(uow); }); } protected T UsingUnitOfWork<T>(Func<IUnitOfWork, T> func) { var uow = IoC.Resolve<IUnitOfWork>(); uow.DisableAllFilters(); using (uow) { var result = func(uow); uow.SaveChanges(); return result; } } protected async Task<T> UsingUnitOfWorkAsync<T>(Func<IUnitOfWork, Task<T>> func) { var uow = IoC.Resolve<IUnitOfWork>(); uow.DisableAllFilters(); using (uow) { var result = await func(uow).ConfigureAwait(false); await uow.SaveChangesAsync().ConfigureAwait(false); return result; } } }
namespace ProjectName.ServiceLayer.IntegrationTests { public static class Testing { private static IContainer Container => TestDependencyScope.CurrentNestedContainer; public static T Resolve<T>() { return Container.GetInstance<T>(); } public static object Resolve(Type type) { return Container.GetInstance(type); } public static void Inject<T>(T instance) where T : class { Container.Inject(instance); } } } //in test classes using static ProjectName.ServiceLayer.IntegrationTests.Testing; namespace ProjectName.ServiceLayer.IntegrationTests { public class RoleServiceTests : IntegratedTestBase { private IRoleService _service; protected override void BeforeEachTest() { _service = Resolve<IRoleService>(); } } }
@model ProjectName.ViewModels.Identity.RegisterViewModel <label asp-for="PhoneNumber"></label>
@using System.Reflection @using System.Linq.Expressions @using System.ComponentModel.DataAnnotations @typeparam T @if (ChildContent is null) { <label>@Label</label> } else { <label> @Label @ChildContent </label> } @code { [Parameter, EditorRequired] public Expression<Func<T>> DisplayNameFor { get; set; } = default!; [Parameter] public RenderFragment? ChildContent { get; set; } private string Label => GetDisplayName(); private string GetDisplayName() { var expression = (MemberExpression)DisplayNameFor.Body; var value = expression.Member.GetCustomAttribute(typeof(DisplayAttribute)) as DisplayAttribute; return value?.Name ?? expression.Member.Name; } }
private LoginDto Login { get; } = new();
<CustomDisplayName DisplayNameFor="@(() => Login.UserName)" />
<CustomDisplayName DisplayNameFor="@(() => Login.UserName)"> <span class="text-danger">(*)</span> </CustomDisplayName>
@using System.Reflection @using System.Linq.Expressions; @using System.ComponentModel.DataAnnotations; @typeparam T @if (ChildContent == null) { <label>@Label</label> } else { <label> @Label @ChildContent </label> } @code { [Parameter] public Expression<Func<T>> DisplayNameFor { get; set; } = default!; [Parameter] public RenderFragment? ChildContent { get; set; } [Inject] public IStringLocalizerFactory LocalizerFactory { get; set; } = default!; private string Label => GetDisplayName(); private string GetDisplayName() { var expression = (MemberExpression)DisplayNameFor.Body; var displayAttribute = expression.Member.GetCustomAttribute(typeof(DisplayAttribute)) as DisplayAttribute; if (displayAttribute is {ResourceType: not null }) { // Try to dynamically create an instance of the specified resource type var resourceType = displayAttribute.ResourceType; var localizer = LocalizerFactory.Create(resourceType); return localizer[displayAttribute.Name ?? expression.Member.Name]; } return displayAttribute?.Name ?? expression.Member.Name; } }
public class LoginDto { [Display(Name = nameof(LoginDtoResource.UserName), ResourceType = typeof(LoginDtoResource))] [Required] [MaxLength(100)] public string UserName { get; set; } = default!; //... }
[Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> InputAttributes { get; set; } = new();
@if (ChildContent is null) { <label @attributes="InputAttributes"> @Label </label> } else { <label @attributes="InputAttributes"> @Label @ChildContent </label> }
<CustomDisplayName DisplayNameFor="@(() => Login.UserName)" class="form-label" id="test" for="UserName" />
public class DiskSmtpClient : SmtpClient { public DiskSmtpClient(IOptionsSnapshot<MailKitOptions> mailOptionsSnapshot) { if (mailOptionsSnapshot.Value.SpecifiedPickupDirectory) { SpecifiedPickupDirectory = true; PickupDirectoryLocation = mailOptionsSnapshot.Value.PickupDirectoryLocation; } } public bool SpecifiedPickupDirectory { get; set; } public string PickupDirectoryLocation { get; set; } public override Task SendAsync(MimeMessage message, CancellationToken cancellationToken = new CancellationToken(), ITransferProgress progress = null) { if (!SpecifiedPickupDirectory) return base.SendAsync(message, cancellationToken, progress); return SaveToPickupDirectory(message, PickupDirectoryLocation); } private async Task SaveToPickupDirectory(MimeMessage message, string pickupDirectory) { using (var stream = new FileStream($@"{pickupDirectory}\email-{Guid.NewGuid().ToString("N")}.eml", FileMode.CreateNew)) { await message.WriteToAsync(stream); } } public override Task ConnectAsync(string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = new CancellationToken()) { if (!SpecifiedPickupDirectory) return base.ConnectAsync(host, port, options, cancellationToken); return Task.CompletedTask; } public override Task DisconnectAsync(bool quit, CancellationToken cancellationToken = new CancellationToken()) { if (!SpecifiedPickupDirectory) return base.DisconnectAsync(quit, cancellationToken); return Task.CompletedTask; } }
services.AddTransient<DiskSmtpClient>();
var email = "mail@dotnettips.info"; var subject = "subject"; var message = "message"; var emailMessage = new MimeMessage(); emailMessage.From.Add(new MailboxAddress("DNT", "do-not-reply@dotnettips.info")); emailMessage.To.Add(new MailboxAddress("", email)); emailMessage.Subject = subject; emailMessage.Body = new TextPart(TextFormat.Html) { Text = message }; _client.SpecifiedPickupDirectory = true; _client.PickupDirectoryLocation = "c:\\mail"; _client.LocalDomain = "dotnettips.info"; await _client.ConnectAsync("smtp.relay.uri", 25, SecureSocketOptions.None).ConfigureAwait(false); await _client.SendAsync(emailMessage).ConfigureAwait(false); await _client.DisconnectAsync(true).ConfigureAwait(false);
services.Configure<MailKitOptions>(options => Configuration.GetSection("MailKitOptions").Bind(options));
"MailKitOptions": { "SpecifiedPickupDirectory": true, "PickupDirectoryLocation": "c:\\mail" }
public class MailKitOptions { public bool SpecifiedPickupDirectory { get; set; } public string PickupDirectoryLocation { get; set; } }