مقدمهای بر Distributed Tracing با OpenTelemetry در NET. 📡
اگر در حال ساخت یا نگهداری اپلیکیشنهای توزیعشده NET. هستید، درک نحوه رفتار آنها کلید تضمین قابلیت اطمینان و عملکرد است.
سیستمهای توزیعشده انعطافپذیری ارائه میدهند اما پیچیدگی را نیز به همراه دارند و عیبیابی را به یک سردرد تبدیل میکنند. 🤯 درک چگونگی جریان درخواستها در سیستم شما برای دیباگ کردن و بهینهسازی عملکرد، حیاتی است.
OpenTelemetry
یک فریمورک observability متنباز است که این امر را ممکن میسازد. ✨
در این مقاله، ما به عمق این موضوع میپردازیم که OpenTelemetry چیست، چگونه از آن در پروژههای NET. خود استفاده کنیم و چه بینشهای قدرتمندی را فراهم میکند.
معرفی OpenTelemetry
OpenTelemetry (OTel)
یک استاندارد متنباز و بیطرف نسبت به فروشندگان (vendor-neutral) برای ابزار دقیق (instrumenting) اپلیکیشنها به منظور تولید دادههای تلهمتری است. OpenTelemetry شامل APIها، SDKها، ابزارها و یکپارچهسازیهایی برای ایجاد و مدیریت این دادههای تلهمتری (traces, metrics, and logs) است.
دادههای تلهمتری شامل:
Traces (ردیابیها) 📈:
جریان درخواستها را در سیستمهای توزیعشده نشان میدهند و زمانبندیها و روابط بین سرویسها را نمایش میدهند.
Metrics (معیارها) 📊:
اندازهگیریهای عددی از رفتار سیستم در طول زمان (مانند تعداد درخواستها، نرخ خطا، استفاده از حافظه).
Logs (لاگها) 📝:
رکوردهای متنی از رویدادها با اطلاعات زمینهای غنی. لاگهای ساختاریافته.
OpenTelemetry
یک راه یکپارچه برای جمعآوری این دادهها فراهم میکند، که درک رفتار و سلامت اپلیکیشنهای توزیعشده پیچیده را آسانتر میکند.
ما میتوانیم دادههای تلهمتری که جمعآوری میکنیم را به سرویسی که قادر به پردازش آن است و یک اینترفیس برای تحلیل آن به ما ارائه میدهد، صادر (export) کنیم.
ما قصد داریم OpenTelemetry را طوری پیکربندی کنیم که traceها را مستقیماً به Jaeger صادر کند.
OpenTelemetry
کتابخانهها و SDKهایی برای افزودن کد (instrumentation) به اپلیکیشنهای NET. شما فراهم میکند. این instrumentationها به طور خودکار traces، metrics و logs مورد علاقه ما را ضبط میکنند.
ما قصد داریم پکیجهای NuGet زیر را نصب کنیم: 📦
هنگامی که این پکیجهای NuGet را نصب کردیم، زمان پیکربندی برخی سرویسها فرا میرسد.
🌐 AddAspNetCoreInstrumentation -
این ابزار دقیق (instrumentation) ASP.NET Core را فعال میکند.
📤 AddHttpClientInstrumentation -
این ابزار دقیق HttpClient را برای درخواستهای خروجی فعال میکند.
💾 AddEntityFrameworkCoreInstrumentation -
این ابزار دقیق EF Core را فعال میکند.
🔥 AddRedisInstrumentation -
این ابزار دقیق Redis را فعال میکند.
🐘 AddNpgsql -
این ابزار دقیق PostgreSQL را فعال میکند.
با پیکربندی تمام این ابزارهای دقیق، اپلیکیشن ما شروع به جمعآوری بسیاری از ردیابیهای (traces) ارزشمند در زمان اجرا خواهد کرد.
ما همچنین باید یک متغیر محیطی را برای exporter اضافه شده با AddOtlpExporter پیکربندی کنیم تا به درستی کار کند. ما میتوانیم OTEL_EXPORTER_OTLP_ENDPOINT را از طریق تنظیمات اپلیکیشن تنظیم کنیم. آدرس مشخص شده در اینجا به یک نمونه محلی Jaeger اشاره خواهد کرد.
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
Jaeger
یک پلتفرم متنباز و توزیعشده برای ردیابی است. Jaeger جریان درخواستها و دادهها را حین عبور از یک سیستم توزیعشده، ترسیم میکند.
در اینجا نحوه اجرای Jaeger داخل یک کانتینر Docker آمده است:
ما از ایمیج jaegertracing/all-in-one:latest استفاده میکنیم و پورت 4317 را برای پذیرش دادههای تلهمتری باز میکنیم. رابط کاربری Jaeger روی پورت 16686 در دسترس خواهد بود.
پس از نصب کتابخانههای OpenTelemetry و پیکربندی tracing در اپلیکیشنهایمان، میتوانیم چند درخواست برای تولید دادههای تلهمتری ارسال کنیم. سپس میتوانیم به Jaeger دسترسی پیدا کرده و شروع به تحلیل distributed traceهای خود کنیم.
ثبت نام یک کاربر جدید 👤
انتشار یک پیام با MassTransit 📨
بررسی اطلاعات اضافی trace 💾
traceهای توزیعشده پیچیده 🕸
یک راه یکپارچه برای جمعآوری این دادهها فراهم میکند، که درک رفتار و سلامت اپلیکیشنهای توزیعشده پیچیده را آسانتر میکند.
ما میتوانیم دادههای تلهمتری که جمعآوری میکنیم را به سرویسی که قادر به پردازش آن است و یک اینترفیس برای تحلیل آن به ما ارائه میدهد، صادر (export) کنیم.
ما قصد داریم OpenTelemetry را طوری پیکربندی کنیم که traceها را مستقیماً به Jaeger صادر کند.
افزودن OpenTelemetry به اپلیکیشنهای NET.🔧
OpenTelemetry
کتابخانهها و SDKهایی برای افزودن کد (instrumentation) به اپلیکیشنهای NET. شما فراهم میکند. این instrumentationها به طور خودکار traces، metrics و logs مورد علاقه ما را ضبط میکنند.
ما قصد داریم پکیجهای NuGet زیر را نصب کنیم: 📦
# Automatic tracing, metrics
Install-Package OpenTelemetry.Extensions.Hosting
# Telemetry data exporter
Install-Package OpenTelemetry.Exporter.OpenTelemetryProtocol
# Instrumentation packages
Install-Package OpenTelemetry.Instrumentation.Http
Install-Package OpenTelemetry.Instrumentation.AspNetCore
هنگامی که این پکیجهای NuGet را نصب کردیم، زمان پیکربندی برخی سرویسها فرا میرسد.
services
.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService(serviceName))
.WithTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddRedisInstrumentation()
.AddNpgsql();
tracing.AddOtlpExporter();
});
🌐 AddAspNetCoreInstrumentation -
این ابزار دقیق (instrumentation) ASP.NET Core را فعال میکند.
📤 AddHttpClientInstrumentation -
این ابزار دقیق HttpClient را برای درخواستهای خروجی فعال میکند.
💾 AddEntityFrameworkCoreInstrumentation -
این ابزار دقیق EF Core را فعال میکند.
🔥 AddRedisInstrumentation -
این ابزار دقیق Redis را فعال میکند.
🐘 AddNpgsql -
این ابزار دقیق PostgreSQL را فعال میکند.
با پیکربندی تمام این ابزارهای دقیق، اپلیکیشن ما شروع به جمعآوری بسیاری از ردیابیهای (traces) ارزشمند در زمان اجرا خواهد کرد.
ما همچنین باید یک متغیر محیطی را برای exporter اضافه شده با AddOtlpExporter پیکربندی کنیم تا به درستی کار کند. ما میتوانیم OTEL_EXPORTER_OTLP_ENDPOINT را از طریق تنظیمات اپلیکیشن تنظیم کنیم. آدرس مشخص شده در اینجا به یک نمونه محلی Jaeger اشاره خواهد کرد.
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
اجرای Jaeger به صورت محلی 🐳
Jaeger
یک پلتفرم متنباز و توزیعشده برای ردیابی است. Jaeger جریان درخواستها و دادهها را حین عبور از یک سیستم توزیعشده، ترسیم میکند.
در اینجا نحوه اجرای Jaeger داخل یک کانتینر Docker آمده است:
docker run -d -p 4317:4317 -p 16686:16686 jaegertracing/all-in-one:latest
ما از ایمیج jaegertracing/all-in-one:latest استفاده میکنیم و پورت 4317 را برای پذیرش دادههای تلهمتری باز میکنیم. رابط کاربری Jaeger روی پورت 16686 در دسترس خواهد بود.
Distributed Tracing (ردیابی توزیعشده)
پس از نصب کتابخانههای OpenTelemetry و پیکربندی tracing در اپلیکیشنهایمان، میتوانیم چند درخواست برای تولید دادههای تلهمتری ارسال کنیم. سپس میتوانیم به Jaeger دسترسی پیدا کرده و شروع به تحلیل distributed traceهای خود کنیم.
ثبت نام یک کاربر جدید 👤
انتشار یک پیام با MassTransit 📨
بررسی اطلاعات اضافی trace 💾
traceهای توزیعشده پیچیده 🕸
ثبت نام یک کاربر جدید 👤
در اینجا مثالی از ثبت نام یک کاربر جدید در سیستم آمده است. ما در حال دسترسی به سرویس API gateway (Evently.Gateway) هستیم، که درخواست را به سرویس Evently.Api پروکسی میکند. و میتوانید ببینید که سرویس Evently.Api چند درخواست HTTP را قبل از پایدار کردن یک رکورد جدید در دیتابیس، انجام میدهد.
خلاصه 📝
درک اپلیکیشنهای مدرن، به خصوص توزیعشده، میتواند واقعاً گیجکننده باشد. OpenTelemetry مانند داشتن دید اشعه ایکس 👁 به سیستم شماست.
در حالی که افزودن OpenTelemetry نیاز به مقداری کار اولیه دارد، آن را یک سرمایهگذاری در نظر بگیرید. این سرمایهگذاری زمانی که مشکلات بروز میکنند، به شدت نتیجه میدهد. به جای حدس و گمانهای آشفته، شما دادههای دقیقی برای تمرکز سریع بر روی مشکلات دارید.
🔖 هشتگها:
#OpenTelemetry #Distributed_Tracing
#Performance #MemoryManagement
📖 سری آموزشی کتاب C# 12 in a Nutshell
در برنامهنویسی شیءگرا، کپسولهسازی (Encapsulation) یعنی مخفی کردن جزئیات پیادهسازی و فقط نمایش دادن چیزهای ضروری. ابزار اصلی ما برای این کار در C# Access Modifiers هست.
این کلمات کلیدی، نگهبانان کد شما هستن و مشخص میکنن که هر کلاس یا عضو اون، از کجا قابل دسترسیه.
معرفی نگهبانان
دربهای کاملاً باز! هر کسی از هر جایی (چه داخل اسمبلی و چه بیرون) میتونه ببینه و استفاده کنه.
فقط خودیها! فقط کدهای داخل همون اسمبلی (پروژه) میتونن ببینن. این حالت پیشفرض برای کلاسهای غیر تودرتو است.
راز شخصی! فقط کدهای داخل همون کلاس یا struct میتونن ببینن. این حالت پیشفرض برای اعضای کلاسها (مثل فیلدها و متدها) هست.
فقط خانواده! فقط کدهای داخل همون کلاس و کلاسهای فرزندی که ازش ارثبری کردن، میتونن ببینن.
خودیها و خانواده! اجتماع protected و internal. یعنی هم از داخل اسمبلی جاری دیده میشه و هم توسط کلاسهای فرزند (حتی اگه تو یه اسمبلی دیگه باشن).
فقط خانوادهی خودی! اشتراک protected و internal. یعنی فقط توسط کلاسهای فرزندی که در همون اسمبلی هستن، دیده میشه. این سطح دسترسی از protected و internal به تنهایی، محدودتره.
فقط همین فایل! اعضایی که با file مشخص میشن، فقط در همون فایلی که تعریف شدن، قابل مشاهده هستن. این بیشتر برای Source Generatorها کاربرد داره.
نکات حرفهای (Pro Tips) 💡
Friend Assemblies:
گاهی وقتا میخواید به یه پروژه دیگه (مثل پروژه تست) اجازه بدید که به اعضای internal شما دسترسی داشته باشه. با اتریبیوت [assembly: InternalsVisibleTo("FriendAssemblyName")] در فایل AssemblyInfo.cs یا .csproj میتونید این کار رو انجام بدید.
Accessibility Capping (سقف دسترسی):
سطح دسترسی یک تایپ، سطح دسترسی اعضای public اون رو محدود میکنه. یعنی یه متد public داخل یه کلاس internal، در عمل internal حساب میشه.
انتخاب درست Access Modifier، یکی از مهمترین تصمیمها در طراحی APIهای تمیز و قابل نگهداریه.
🛡راهنمای کامل Access Modifiers در #C: چه کسی کد شما را میبیند؟
در برنامهنویسی شیءگرا، کپسولهسازی (Encapsulation) یعنی مخفی کردن جزئیات پیادهسازی و فقط نمایش دادن چیزهای ضروری. ابزار اصلی ما برای این کار در C# Access Modifiers هست.
این کلمات کلیدی، نگهبانان کد شما هستن و مشخص میکنن که هر کلاس یا عضو اون، از کجا قابل دسترسیه.
معرفی نگهبانان
🌍 public (عمومی):
دربهای کاملاً باز! هر کسی از هر جایی (چه داخل اسمبلی و چه بیرون) میتونه ببینه و استفاده کنه.
🏢 internal (داخلی):
فقط خودیها! فقط کدهای داخل همون اسمبلی (پروژه) میتونن ببینن. این حالت پیشفرض برای کلاسهای غیر تودرتو است.
🔐 private (خصوصی):
راز شخصی! فقط کدهای داخل همون کلاس یا struct میتونن ببینن. این حالت پیشفرض برای اعضای کلاسها (مثل فیلدها و متدها) هست.
👨👩👧 protected (محافظت شده):
فقط خانواده! فقط کدهای داخل همون کلاس و کلاسهای فرزندی که ازش ارثبری کردن، میتونن ببینن.
🤝 protected internal:
خودیها و خانواده! اجتماع protected و internal. یعنی هم از داخل اسمبلی جاری دیده میشه و هم توسط کلاسهای فرزند (حتی اگه تو یه اسمبلی دیگه باشن).
🤫 private protected:
فقط خانوادهی خودی! اشتراک protected و internal. یعنی فقط توسط کلاسهای فرزندی که در همون اسمبلی هستن، دیده میشه. این سطح دسترسی از protected و internal به تنهایی، محدودتره.
📄 file (از C# 11):
فقط همین فایل! اعضایی که با file مشخص میشن، فقط در همون فایلی که تعریف شدن، قابل مشاهده هستن. این بیشتر برای Source Generatorها کاربرد داره.
نکات حرفهای (Pro Tips) 💡
Friend Assemblies:
گاهی وقتا میخواید به یه پروژه دیگه (مثل پروژه تست) اجازه بدید که به اعضای internal شما دسترسی داشته باشه. با اتریبیوت [assembly: InternalsVisibleTo("FriendAssemblyName")] در فایل AssemblyInfo.cs یا .csproj میتونید این کار رو انجام بدید.
Accessibility Capping (سقف دسترسی):
سطح دسترسی یک تایپ، سطح دسترسی اعضای public اون رو محدود میکنه. یعنی یه متد public داخل یه کلاس internal، در عمل internal حساب میشه.
🤔 حرف حساب و تجربه شما
انتخاب درست Access Modifier، یکی از مهمترین تصمیمها در طراحی APIهای تمیز و قابل نگهداریه.
🔖 هشتگها:
#OOP #Encapsulation
مدیریت خطای تابعی (Functional) در NET. با الگوی Result ✅
چگونه باید خطاها را در کد خود مدیریت کنید؟
این موضوع بحثهای زیادی بوده است و من میخواهم نظر خود را به اشتراک بگذارم.
یک مکتب فکری استفاده از استثناها (exceptions) را برای کنترل جریان (flow control) پیشنهاد میکند. 🤯 این رویکرد خوبی نیست زیرا استدلال در مورد کد را دشوارتر میکند. فراخواننده (caller) باید جزئیات پیادهسازی و اینکه کدام استثناها را باید مدیریت کند، بداند.
استثناها برای شرایط استثنایی هستند.
امروز، میخواهم به شما نشان دهم چگونه مدیریت خطا را با استفاده از الگوی Result پیادهسازی کنید. ✨
این یک رویکرد تابعی برای مدیریت خطا است که کد شما را گویاتر میکند.
استثناها برای کنترل جریان ⚡️
استفاده از استثناها برای کنترل جریان، رویکردی برای پیادهسازی اصل fail-fast است.
به محض اینکه با خطایی در کد مواجه میشوید، یک استثنا پرتاب میکنید — که به طور موثر متد را خاتمه میدهد و فراخواننده را مسئول مدیریت استثنا میکند.
مشکل این است که فراخواننده باید بداند کدام استثناها را مدیریت کند. و این تنها از امضای متد مشخص نیست.
یک مورد استفاده رایج دیگر، پرتاب استثناها برای خطاهای اعتبارسنجی است.
در اینجا یک مثال در FollowerService آمده است: 👨💻
public sealed class FollowerService
{
private readonly IFollowerRepository _followerRepository;
public FollowerService(IFollowerRepository followerRepository)
{
_followerRepository = followerRepository;
}
public async Task StartFollowingAsync(
User user,
User followed,
DateTime createdOnUtc,
CancellationToken cancellationToken = default)
{
if (user.Id == followed.Id)
{
throw new DomainException("Can't follow yourself");
}
if (!followed.HasPublicProfile)
{
throw new DomainException("Can't follow non-public profile");
}
if (await _followerRepository.IsAlreadyFollowingAsync(
user.Id,
followed.Id,
cancellationToken))
{
throw new DomainException("Already following");
}
var follower = Follower.Create(user.Id, followed.Id, createdOnUtc);
_followerRepository.Insert(follower);
}
}
از استثناها برای شرایط استثنایی استفاده کنید ⚡️
یک قانون سرانگشتی که من دنبال میکنم این است که از استثناها برای شرایط استثنایی استفاده کنم. از آنجایی که شما از قبل انتظار خطاهای بالقوه را دارید، چرا آن را صریح نکنید؟
شما میتوانید تمام خطاهای اپلیکیشن را به دو گروه تقسیم کنید:
✅ خطاهایی که میدانید چگونه مدیریت کنید.
❓ خطاهایی که نمیدانید چگونه مدیریت کنید.
استثناها یک راهحل عالی برای خطاهایی هستند که نمیدانید چگونه مدیریت کنید. و شما باید آنها را در پایینترین سطح ممکن catch کرده و مدیریت کنید.
در مورد خطاهایی که میدانید چگونه مدیریت کنید چطور؟
شما میتوانید آنها را به روش تابعی با الگوی Result مدیریت کنید. این صریح است و به وضوح این نیت را بیان میکند که متد میتواند شکست بخورد. نقطه ضعف این است که فراخواننده باید به صورت دستی بررسی کند که آیا عملیات شکست خورده است یا نه.
بیان خطاها با استفاده از الگوی Result ✨
اولین چیزی که نیاز خواهید داشت، یک کلاس Error برای نمایش خطاهای اپلیکیشن است.
🔹️ Code -
نام منحصر به فرد برای خطا در اپلیکیشن.
🔹️ Denoscription -
شامل جزئیات توسعهدهنده-پسند در مورد خطا.
public sealed record Error(string Code, string Denoscription)
{
public static readonly Error None = new(string.Empty, string.Empty);
}
سپس، میتوانید کلاس Result را با استفاده از Error برای توصیف شکست، پیادهسازی کنید. این پیادهسازی بسیار ساده است و شما میتوانید ویژگیهای بسیار بیشتری به آن اضافه کنید. در اکثر موارد، شما همچنین به یک کلاس جنریک Result<T> نیاز دارید که یک مقدار را در داخل خود بپیچد.
در اینجا ظاهر کلاس Result آمده است: 🎁
public class Result
{
private Result(bool isSuccess, Error error)
{
if (isSuccess && error != Error.None ||
!isSuccess && error == Error.None)
{
throw new ArgumentException("Invalid error", nameof(error));
}
IsSuccess = isSuccess;
Error = error;
}
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public Error Error { get; }
public static Result Success() => new(true, Error.None);
public static Result Failure(Error error) => new(false, error);
}
تنها راه برای ایجاد یک نمونه Result، استفاده از متدهای استاتیک است:
🔹️ Success -
یک نتیجه موفقیتآمیز ایجاد میکند.
🔹️ Failure -
یک نتیجه شکست با Error مشخص شده ایجاد میکند.
اگر میخواهید از ساختن کلاس Result خودتان اجتناب کنید، نگاهی به کتابخانه FluentResults 📚 بیندازید.
بهکارگیری الگوی Result 🚀
حالا که کلاس Result را داریم، بیایید ببینیم چگونه آن را در عمل به کار ببریم.
در اینجا یک نسخه بازآرایی (refactor) شده از FollowerService آمده است. به چند نکته توجه کنید:
✅ دیگر هیچ استثنایی پرتاب نمیشود.
✅ نوع بازگشتی Result صریح است.
✅ مشخص است که متد کدام خطاها را برمیگرداند.
مزیت دیگر مدیریت خطا با استفاده از الگوی Result این است که تست کردن آن آسانتر است.
public sealed class FollowerService
{
private readonly IFollowerRepository _followerRepository;
public FollowerService(IFollowerRepository followerRepository)
{
_followerRepository = followerRepository;
}
public async Task<Result> StartFollowingAsync(
User user,
User followed,
DateTime utcNow,
CancellationToken cancellationToken = default)
{
if (user.Id == followed.Id)
{
return Result.Failure(FollowerErrors.SameUser);
}
if (!followed.HasPublicProfile)
{
return Result.Failure(FollowerErrors.NonPublicProfile);
}
if (await _followerRepository.IsAlreadyFollowingAsync(
user.Id,
followed.Id,
cancellationToken))
{
return Result.Failure(FollowerErrors.AlreadyFollowing);
}
var follower = Follower.Create(user.Id, followed.Id, utcNow);
_followerRepository.Insert(follower);
return Result.Success();
}
}
مستندسازی خطاهای اپلیکیشن 📚
شما میتوانید از کلاس Error برای مستندسازی تمام خطاهای ممکن در اپلیکیشن خود استفاده کنید.
یک رویکرد، ایجاد یک کلاس استاتیک به نام Errors است. این کلاس، کلاسهای تودرتو در داخل خود خواهد داشت که حاوی خطاهای خاص هستند. نحوه استفاده به شکل Errors.Followers.NonPublicProfile خواهد بود.
با این حال، رویکردی که من دوست دارم استفاده کنم، ایجاد یک کلاس خاص است که حاوی خطاها باشد.
در اینجا کلاس FollowerErrors آمده است که خطاهای ممکن برای انتیتی Follower را مستند میکند: 📝
public static class FollowerErrors
{
public static readonly Error SameUser = new Error(
"Followers.SameUser", "Can't follow yourself");
public static readonly Error NonPublicProfile = new Error(
"Followers.NonPublicProfile", "Can't follow non-public profiles");
public static readonly Error AlreadyFollowing = new Error(
"Followers.AlreadyFollowing", "Already following");
}
به جای فیلدهای استاتیک، شما همچنین میتوانید از متدهای استاتیک که یک خطا برمیگردانند، استفاده کنید. شما این متد را با یک آرگومان مشخص فراخوانی میکنید تا یک نمونه Error دریافت کنید.
public static class FollowerErrors
{
public static Error NotFound(Guid id) => new Error(
"Followers.NotFound", $"The follower with Id '{id}' was not found");
}
تبدیل Resultها به پاسخهای API 🚀
آبجکت Result در نهایت به endpoint Minimal API (یا کنترلر) در ASP.NET Core خواهد رسید. Minimal APIها یک پاسخ IResult برمیگردانند و کنترلرها یک پاسخ IActionResult برمیگردانند. صرف نظر از این، شما باید نمونه Result را به یک پاسخ API معتبر تبدیل کنید.
رویکرد سرراست، بررسی وضعیت Result و برگرداندن یک پاسخ HTTP است. در اینجا یک مثال آمده است که در آن ما فلگ Result.IsFailure را بررسی میکنیم:
app.MapPost(
"users/{userId}/follow/{followedId}",
async (Guid userId, Guid followedId, FollowerService followerService) =>
{
var result = await followerService.StartFollowingAsync(
userId,
followedId,
DateTime.UtcNow);
if (result.IsFailure)
{
return Results.BadRequest(result.Error);
}
return Results.NoContent();
});
با این حال، این یک فرصت عالی برای یک رویکرد تابعیتر است. شما میتوانید متد توسعه Match را برای ارائه یک callback برای هر وضعیت Result پیادهسازی کنید. متد Match، callback مربوطه را اجرا کرده و نتیجه را برمیگرداند.
در اینجا پیادهسازی Match آمده است: 👍
public static class ResultExtensions
{
public static T Match<T>(
this Result result,
Func<T> onSuccess,
Func<Error, T> onFailure)
{
return result.IsSuccess ? onSuccess() : onFailure(result.Error);
}
}
و اینگونه از متد Match در یک endpoint Minimal API استفاده میکنید:
app.MapPost(
"users/{userId}/follow/{followedId}",
async (Guid userId, Guid followedId, FollowerService followerService) =>
{
var result = await followerService.StartFollowingAsync(
userId,
followedId,
DateTime.UtcNow);
return result.Match(
onSuccess: () => Results.NoContent(),
onFailure: error => Results.BadRequest(error));
});
خلاصه 📝
اگر قرار است یک چیز را از مقاله امروز با خود ببرید، باید این باشد: استثناها برای شرایط استثنایی هستند. علاوه بر این، شما فقط باید از استثناها برای خطاهایی که نمیدانید چگونه مدیریت کنید، استفاده کنید. در تمام موارد دیگر، بیان واضح خطا با الگوی Result ارزشمندتر است.
استفاده از کلاس Result به شما اجازه میدهد تا:
✅ نیت اینکه یک متد ممکن است شکست بخورد را بیان کنید.
✅ یک خطای اپلیکیشن را در داخل آن کپسوله کنید.
✅ یک راه تابعی برای مدیریت خطاها فراهم کنید.
علاوه بر این، شما میتوانید تمام خطاهای اپلیکیشن را با کلاس Error مستندسازی کنید. این برای توسعهدهندگان مفید است تا بدانند کدام خطاها را باید مدیریت کنند.
شما حتی میتوانید این را به مستندات واقعی تبدیل کنید. 📚 برای مثال، من یک برنامه ساده نوشتم که پروژه را برای تمام فیلدهای Error اسکن میکند. سپس این را به فرمت جدول تبدیل کرده و در یک صفحه Confluence آپلود میکند.
بنابراین من شما را تشویق میکنم که الگوی Result را امتحان کنید و ببینید چگونه میتواند کد شما را بهبود ببخشد.
📖 سری آموزشی کتاب C# 12 in a Nutshell
چطور میتونیم برای کلاسهامون یه "قرارداد" تعریف کنیم و بگیم "هر کلاسی که میخواد با من کار کنه، باید این قابلیتها رو داشته باشه"؟
ابزار اصلی ما برای این کار در دنیای شیءگرایی، اینترفیس (Interface) هست. اینترفیسها ستون فقرات معماریهای تمیز، انعطافپذیر و تستپذیر هستن.
اینترفیس فقط رفتار (Behavior) رو تعریف میکنه، نه وضعیت (State). یعنی فقط امضای متدها و پراپرتیها رو داره، نه فیلدهای داده و نه بدنه پیادهسازی برای اعضاش.
تفاوتهای کلیدی با کلاس:
🔹 فقط توابع (متد، پراپرتی، ایونت، ایندکسر) را تعریف میکند، نه فیلدها.
🔹 اعضایش به صورت پیشفرض public و abstract هستند.
🔹 یک کلاس میتواند چندین اینترفیس را پیادهسازی کند (برخلاف وراثت از کلاس که فقط یکیه).
مثال (اینترفیس IEnumerator):
وقتی یه کلاس، اینترفیسی رو پیادهسازی میکنه، قول میده که برای تمام اعضای اون اینترفیس، یک پیادهسازی public ارائه بده.
حالا میتونید یک نمونه از Countdown رو در متغیری از نوع IEnumerator بریزید:
حالا اگه یه کلاس، دو تا اینترفیس رو پیادهسازی کنه که متدهایی با اسم یکسان ولی امضای متفاوت دارن، چی میشه؟ یا اگه بخوایم یه متد از اینترفیس رو از دید عمومی کلاس مخفی کنیم؟
اینجا پای پیادهسازی صریح (Explicit Implementation) به میدون باز میشه. در این حالت، شما اسم اینترفیس رو قبل از اسم متد میارید.
مثال (حل تداخل):
نکته حیاتی: ⚠️ متدی که به صورت صریح پیادهسازی شده، دیگه به صورت عمومی در دسترس نیست. برای صدا زدنش، باید اول آبجکت رو به اون اینترفیس خاص کست کنید:
اینترفیسها، اساس طراحیهای ماژولار، تستپذیر و مبتنی بر اصول SOLID هستن.
📜 قراردادهای کدنویسی در #C: راهنمای کامل اینترفیسها (Interfaces)
چطور میتونیم برای کلاسهامون یه "قرارداد" تعریف کنیم و بگیم "هر کلاسی که میخواد با من کار کنه، باید این قابلیتها رو داشته باشه"؟
ابزار اصلی ما برای این کار در دنیای شیءگرایی، اینترفیس (Interface) هست. اینترفیسها ستون فقرات معماریهای تمیز، انعطافپذیر و تستپذیر هستن.
1️⃣ اینترفیس چیست؟ یک قرارداد، بدون پیادهسازی
اینترفیس فقط رفتار (Behavior) رو تعریف میکنه، نه وضعیت (State). یعنی فقط امضای متدها و پراپرتیها رو داره، نه فیلدهای داده و نه بدنه پیادهسازی برای اعضاش.
تفاوتهای کلیدی با کلاس:
🔹 فقط توابع (متد، پراپرتی، ایونت، ایندکسر) را تعریف میکند، نه فیلدها.
🔹 اعضایش به صورت پیشفرض public و abstract هستند.
🔹 یک کلاس میتواند چندین اینترفیس را پیادهسازی کند (برخلاف وراثت از کلاس که فقط یکیه).
مثال (اینترفیس IEnumerator):
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
2️⃣ پیادهسازی اینترفیس: امضای قرارداد
وقتی یه کلاس، اینترفیسی رو پیادهسازی میکنه، قول میده که برای تمام اعضای اون اینترفیس، یک پیادهسازی public ارائه بده.
internal class Countdown : IEnumerator
{
int count = 11;
public bool MoveNext() => count-- > 0;
public object Current => count;
public void Reset() { throw new NotSupportedException(); }
}
حالا میتونید یک نمونه از Countdown رو در متغیری از نوع IEnumerator بریزید:
IEnumerator e = new Countdown();
while (e.MoveNext())
{
Console.Write(e.Current + " "); // 10 9 8 7 6 5 4 3 2 1 0
}
3️⃣ جادوی پیادهسازی صریح (Explicit Implementation) 🎭
حالا اگه یه کلاس، دو تا اینترفیس رو پیادهسازی کنه که متدهایی با اسم یکسان ولی امضای متفاوت دارن، چی میشه؟ یا اگه بخوایم یه متد از اینترفیس رو از دید عمومی کلاس مخفی کنیم؟
اینجا پای پیادهسازی صریح (Explicit Implementation) به میدون باز میشه. در این حالت، شما اسم اینترفیس رو قبل از اسم متد میارید.
مثال (حل تداخل):
interface I1 { void Foo(); }
interface I2 { int Foo(); }
public class Widget : I1, I2
{
public void Foo() // پیادهسازی پیشفرض برای I1
{
Console.WriteLine("Widget's implementation of I1.Foo");
}
// پیادهسازی صریح برای حل تداخل با I1.Foo
int I2.Foo()
{
Console.WriteLine("Widget's implementation of I2.Foo");
return 42;
}
}نکته حیاتی: ⚠️ متدی که به صورت صریح پیادهسازی شده، دیگه به صورت عمومی در دسترس نیست. برای صدا زدنش، باید اول آبجکت رو به اون اینترفیس خاص کست کنید:
Widget w = new Widget();
w.Foo(); // I1.Foo صدا زده میشه
((I1)w).Foo(); // I1.Foo صدا زده میشه
((I2)w).Foo(); // I2.Foo صدا زده میشه
🤔 حرف حساب و تجربه شما
اینترفیسها، اساس طراحیهای ماژولار، تستپذیر و مبتنی بر اصول SOLID هستن.
🔖 هشتگها:
#CSharp #DotNet #OOP #Interface #CleanCode
📖 سری آموزشی کتاب C# 12 in a Nutshell
تو پست قبلی، وراثت و اینترفیسها رو جداگونه بررسی کردیم. اما وقتی این دو تا با هم ترکیب میشن، دنیایی از نکات ظریف و الگوهای پیشرفته به وجود میاد.
امروز میخوایم یاد بگیریم چطور یه عضو اینترفیس رو در سلسلهمراتب وراثت به درستی override کنیم.
به صورت پیشفرض، وقتی یه عضو اینترفیس رو پیادهسازی میکنید، اون sealed (مهر و موم شده) هست. برای اینکه به کلاسهای فرزند اجازه override کردنش رو بدید، باید اون رو صراحتاً virtual مشخص کنید. این کار به شما اجازه میده از قدرت کامل چندریختی (Polymorphism) استفاده کنید.
حالا فرض کنید کلاس پدر، متد رو virtual نکرده. یه راه برای "override" کردنش، بازپیادهسازی اینترفیس در کلاس فرزنده. این کار، پیادهسازی رو فقط وقتی که آبجکت از طریق خود اینترفیس صدا زده بشه، "هایجک" میکنه.
تلهی بزرگ: ☠️ اگه پیادهسازی در کلاس پدر به صورت ضمنی (public) باشه، این الگو باعث رفتار متناقض و خطرناک میشه!
بازپیادهسازی معمولاً یه راه حل ضعیف و نشانهی طراحی بده. دو الگوی خیلی بهتر برای طراحی کلاسهای توسعهپذیر وجود داره:
الگوی اول: اگه پیادهسازی ضمنیه، همیشه virtual ـش کنید (همون روش شماره ۱).
الگوی دوم (برای پیادهسازی صریح): پیادهسازی صریح (explicit) اینترفیس رو به یک متد protected virtual وصل کنید. این الگو، قدرت کامل رو به کلاسهای فرزند میده تا رفتار رو به صورت امن override کنن.
این الگوها، تفاوت بین یه کتابخونه قابل اعتماد و یه کتابخونه شکننده رو رقم میزنن. طراحی برای توسعهپذیری، نشانه یک معمار نرمافزار حرفهایه.
🧩 وراثت و اینترفیسها در #C: بازپیادهسازی و الگوهای حرفهای
تو پست قبلی، وراثت و اینترفیسها رو جداگونه بررسی کردیم. اما وقتی این دو تا با هم ترکیب میشن، دنیایی از نکات ظریف و الگوهای پیشرفته به وجود میاد.
امروز میخوایم یاد بگیریم چطور یه عضو اینترفیس رو در سلسلهمراتب وراثت به درستی override کنیم.
1️⃣ روش استاندارد: virtual و override
به صورت پیشفرض، وقتی یه عضو اینترفیس رو پیادهسازی میکنید، اون sealed (مهر و موم شده) هست. برای اینکه به کلاسهای فرزند اجازه override کردنش رو بدید، باید اون رو صراحتاً virtual مشخص کنید. این کار به شما اجازه میده از قدرت کامل چندریختی (Polymorphism) استفاده کنید.
public interface IUndoable { void Undo(); }
public class TextBox : IUndoable
{
public virtual void Undo() => Console.WriteLine("TextBox.Undo");
}
public class RichTextBox : TextBox
{
public override void Undo() => Console.WriteLine("RichTextBox.Undo");
}
// --- نتایج ---
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo
((IUndoable)r).Undo(); // RichTextBox.Undo
((TextBox)r).Undo(); // RichTextBox.Undo (رفتار یکپارچه و درست)2️⃣ تکنیک خطرناک: بازپیادهسازی
(Re-implementation) ⚠️
حالا فرض کنید کلاس پدر، متد رو virtual نکرده. یه راه برای "override" کردنش، بازپیادهسازی اینترفیس در کلاس فرزنده. این کار، پیادهسازی رو فقط وقتی که آبجکت از طریق خود اینترفیس صدا زده بشه، "هایجک" میکنه.
تلهی بزرگ: ☠️ اگه پیادهسازی در کلاس پدر به صورت ضمنی (public) باشه، این الگو باعث رفتار متناقض و خطرناک میشه!
public class TextBox : IUndoable
{
// این متد virtual نیست!
public void Undo() => Console.WriteLine("TextBox.Undo");
}
public class RichTextBox : TextBox, IUndoable // بازپیادهسازی اینترفیس
{
// اینجا new هم میتونستیم بذاریم
public void Undo() => Console.WriteLine("RichTextBox.Undo");
}
// --- نتایج متناقض و خطرناک ---
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo
((IUndoable)r).Undo(); // RichTextBox.Undo (هایجک شد)
((TextBox)r).Undo(); // TextBox.Undo (فاجعه! رفتار چندریختی شکست)
3️⃣ الگوهای حرفهای (جایگزینهای بهتر) ✅
بازپیادهسازی معمولاً یه راه حل ضعیف و نشانهی طراحی بده. دو الگوی خیلی بهتر برای طراحی کلاسهای توسعهپذیر وجود داره:
الگوی اول: اگه پیادهسازی ضمنیه، همیشه virtual ـش کنید (همون روش شماره ۱).
الگوی دوم (برای پیادهسازی صریح): پیادهسازی صریح (explicit) اینترفیس رو به یک متد protected virtual وصل کنید. این الگو، قدرت کامل رو به کلاسهای فرزند میده تا رفتار رو به صورت امن override کنن.
public class TextBox : IUndoable
{
// پیادهسازی صریح، کار را به یک متد مجازی و محافظتشده میسپارد
void IUndoable.Undo() => Undo();
protected virtual void Undo() => Console.WriteLine("TextBox.Undo");
}
public class RichTextBox : TextBox
{
protected override void Undo() => Console.WriteLine("RichTextBox.Undo");
}
🤔 حرف حساب و تجربه شما
این الگوها، تفاوت بین یه کتابخونه قابل اعتماد و یه کتابخونه شکننده رو رقم میزنن. طراحی برای توسعهپذیری، نشانه یک معمار نرمافزار حرفهایه.
🔖 هشتگها:
#CSharp #DotNet #OOP #Interface #Inheritance
8️⃣ نکته برای نوشتن کد تمیز 🧼
کد تمیز، کدی است که خواندن، نگهداری و درک آن آسان است.
نقطه شروع 🏁
من دوست دارم هنگام یادگیری مفاهیم جدید، با یک مشکل شروع کنم.
و هر چه مشکل گویاتر باشد، بهتر است.
بنابراین ما از یک کد با نوشتار ضعیف به عنوان نقطه شروع برای بازآرایی خود استفاده خواهیم کرد.
و در هر مرحله، من مشخص خواهم کرد که مشکل فعلی چیست و چگونه آن را برطرف خواهیم کرد.
این چیزی است که من وقتی به متد Process نگاه میکنم، میبینم:
nesting
تو در توی عمیق کد - دقیقاً ۴ سطح.
چکهای پیششرط (Precondition) یکی پس از دیگری اعمال میشوند.
پرتاب استثنا (Exception) برای نمایش یک شکست.
چگونه میتوانیم این را به کد تمیز تبدیل کنیم؟ 👎
public void Process(Order? order)
{
if (order != null)
{
if (order.IsVerified)
{
if (order.Items.Count > 0)
{
if (order.Items.Count > 15)
{
throw new Exception(
"The order " + order.Id + " has too many items");
}
if (order.Status != "ReadyToProcess")
{
throw new Exception(
"The order " + order.Id + " isn't ready to process");
}
order.IsProcessed = true;
}
}
}
}
1️⃣: اصل بازگشت زودهنگام (Early Return Principle) 🚪
تا الان باید به طرز دردناکی واضح باشد که نسخه اولیه به دلیل دستورات if که چکهای پیششرط را اعمال میکنند، به شدت تودرتو است.
ما این مشکل را با استفاده از اصل بازگشت زودهنگام حل خواهیم کرد، که بیان میکند ما باید به محض برآورده شدن شرایط، از یک متد return کنیم.
در مورد متد Process، این به معنای حرکت از یک ساختار به شدت تودرتو به مجموعهای از guard clauses (شرطهای محافظ) است. 👍
public void Process(Order? order)
{
if (order is null)
{
return;
}
if (!order.IsVerified)
{
return;
}
if (order.Items.Count == 0)
{
return;
}
if (order.Items.Count > 15)
{
throw new Exception(
"The order " + order.Id + " has too many items");
}
if (order.Status != "ReadyToProcess")
{
throw new Exception(
"The order " + order.Id + " isn't ready to process");
}
order.IsProcessed = true;
}
2️⃣: ادغام دستورات If برای بهبود خوانایی 🔄
اصل بازگشت زودهنگام، متد Process را خواناتر میکند.
اما نیازی نیست که یک guard clause پس از دیگری داشته باشیم.
بنابراین میتوانیم همه آنها را در یک دستور if ادغام کنیم.
رفتار متد Process بدون تغییر باقی میماند، اما ما مقدار زیادی کد اضافی را حذف میکنیم.
public void Process(Order? order)
{
if (order is null
!order.IsVerified
order.Items.Count == 0)
{
return;
}
if (order.Items.Count > 15)
{
throw new Exception(
"The order " + order.Id + " has too many items");
}
if (order.Status != "ReadyToProcess")
{
throw new Exception(
"The order " + order.Id + " isn't ready to process");
}
order.IsProcessed = true;
}
3️⃣: استفاده از LINQ برای کد خلاصهتر ✨
یک بهبود سریع میتواند استفاده از LINQ برای خلاصهتر و گویاتر کردن کد باشد.
به جای بررسی Items.Count == 0، من ترجیح میدهم از متد Any در LINQ استفاده کنم.
شما میتوانید استدلال کنید که LINQ عملکرد بدتری دارد، اما من همیشه برای خوانایی بهینهسازی میکنم.
عملیات بسیار پرهزینهتری در یک اپلیکیشن نسبت به یک فراخوانی متد ساده وجود دارد.
public void Process(Order? order)
{
if (order is null
!order.IsVerified
!order.Items.Any()) // استفاده از Any()
{
return;
}
if (order.Items.Count > 15)
{
throw new Exception(
"The order " + order.Id + " has too many items");
}
if (order.Status != "ReadyToProcess")
{
throw new Exception(
"The order " + order.Id + " isn't ready to process");
}
order.IsProcessed = true;
}
4️⃣: عبارت بولین با متد توصیفی 🗣
ادغام چندین شرط در یک دستور if به معنای نوشتن کد کمتر است، اما میتواند خوانایی را در شرایط پیچیده کاهش دهد.
با این حال، شما میتوانید این مشکل را برطرف کرده و خوانایی را با استفاده از یک متغیر یا متد با نام توصیفی بهبود ببخشید.
من استفاده از متدها را ترجیح میدهم، بنابراین متد IsProcessable را برای نمایش چک پیششرط معرفی خواهم کرد.
public void Process(Order? order)
{
if (!IsProcessable(order))
{
return;
}
if (order.Items.Count > 15)
{
throw new Exception(
"The order " + order.Id + " has too many items");
}
if (order.Status != "ReadyToProcess")
{
throw new Exception(
"The order " + order.Id + " isn't ready to process");
}
order.IsProcessed = true;
}
static bool IsProcessable(Order? order)
{
return order is not null &&
order.IsVerified &&
order.Items.Any();
}
5️⃣: ترجیح دادن پرتاب استثناهای سفارشی (Custom Exceptions) 💥
حالا بیایید در مورد پرتاب کردن استثناها صحبت کنیم. من دوست دارم از استثناها فقط برای شرایط "استثنایی" استفاده کنم، و از آنها برای کنترل جریان در کدم استفاده نمیکنم.
با این حال، اگر شما میخواهید از استثناها برای کنترل جریان استفاده کنید، بهتر است از استثناهای سفارشی استفاده کنید.
شما میتوانید اطلاعات زمینهای ارزشمندی را معرفی کرده و دلیل پرتاب استثنا را بهتر توصیف کنید.
و اگر میخواهید این استثناها را به صورت سراسری مدیریت کنید، میتوانید یک کلاس پایه ایجاد کنید تا بتوانید استثناهای خاصی را catch کنید.
public void Process(Order? order)
{
if (!IsProcessable(order))
{
return;
}
if (order.Items.Count > 15)
{
throw new TooManyLineItemsException(order.Id);
}
if (order.Status != "ReadyToProcess")
{
throw new NotReadyForProcessingException(order.Id);
}
order.IsProcessed = true;
}
static bool IsProcessable(Order? order)
{
return order is not null &&
order.IsVerified &&
order.Items.Any();
}
6️⃣: رفع اعداد جادویی (Magic Numbers) با ثابتها (Constants) 🔢
یک code smell رایج که من میبینم، استفاده از اعداد جادویی است.
آنها معمولاً به راحتی قابل تشخیص هستند زیرا برای بررسی اعمال شدن یک شرط عددی استفاده میشوند.
مشکل اعداد جادویی این است که هیچ معنایی ندارند.
استدلال در مورد کد دشوارتر و مستعد خطا میشود.
رفع اعداد جادویی باید سرراست باشد و یک راهحل، معرفی یک ثابت (constant) است.
const int MaxNumberOfLineItems = 15;
public void Process(Order? order)
{
if (!IsProcessable(order))
{
return;
}
if (order.Items.Count > MaxNumberOfLineItems)
{
throw new TooManyLineItemsException(order.Id);
}
if (order.Status != "ReadyToProcess")
{
throw new NotReadyForProcessingException(order.Id);
}
order.IsProcessed = true;
}
static bool IsProcessable(Order? order)
{
return order is not null &&
order.IsVerified &&
order.Items.Any();
}
7️⃣: رفع رشتههای جادویی (Magic Strings) با Enumها 📜
مشابه اعداد جادویی، ما code smell رشتههای جادویی را داریم.
یک مورد استفاده معمول برای رشتههای جادویی، نمایش نوعی از وضعیت (state) است.
شما متوجه خواهید شد که ما مقدار Order.Status را با یک رشته جادویی مقایسه میکنیم تا بررسی کنیم آیا سفارش آماده پردازش است یا نه.
چند مشکل با رشتههای جادویی:
🔹 امکان اشتباه کردن (اشتباه تایپی) آسان است.
🔹 عدم وجود تایپ قوی (strong typing).
🔹 در برابر بازآرایی (refactoring) مقاوم نیستند.
بیایید یک enum به نام OrderStatus برای نمایش وضعیتهای ممکن ایجاد کنیم: 🏷
enum OrderStatus
{
Pending = 0,
ReadyToProcess = 1,
Processed = 2
}
و حالا باید از OrderStatus مناسب در چک استفاده کنیم:
const int MaxNumberOfLineItems = 15;
public void Process(Order? order)
{
if (!IsProcessable(order))
{
return;
}
if (order.Items.Count > MaxNumberOfLineItems)
{
throw new TooManyLineItemsException(order.Id);
}
if (order.Status != OrderStatus.ReadyToProcess)
{
throw new NotReadyForProcessingException(order.Id);
}
order.IsProcessed = true;
order.Status = OrderStatus.Processed;
}
static bool IsProcessable(Order? order)
{
return order is not null &&
order.IsVerified &&
order.Items.Any();
}
8️⃣: از الگوی آبجکت نتیجه (Result Object Pattern) استفاده کنید 🎁
من گفتم که استفاده از استثناها برای کنترل جریان را ترجیح نمیدهم. اما چگونه میتوانیم این مشکل را برطرف کنیم؟
یک راهحل، استفاده از الگوی آبجکت نتیجه است.
شما میتوانید از یک کلاس جنریک Result برای نمایش انواع نتایج یا یک کلاس خاص مانند ProcessOrderResult استفاده کنید.
برای اینکه آبجکتهای نتیجه شما کپسوله شوند، مجموعهای از متدهای factory برای ایجاد نوع نتیجه مشخص، ارائه دهید.
public class ProcessOrderResult
{
private ProcessOrderResult(
ProcessOrderResultType type,
long orderId,
string message)
{
Type = type;
OrderId = orderId;
Message = message;
}
public ProcessOrderResultType Type { get; }
public long OrderId { get; }
public string? Message { get; }
public static ProcessOrderResult NotProcessable() =>
new(ProcessOrderResultType.NotProcessable, default, "Not processable");
public static ProcessOrderResult TooManyLineItems(long oderId) =>
new(ProcessOrderResultType.TooManyLineItems, orderId, "Too many items");
public static ProcessOrderResult NotReadyForProcessing(long oderId) =>
new(ProcessOrderResultType.NotReadyForProcessing, oderId, "Not ready");
public static ProcessOrderResult Success(long oderId) =>
new(ProcessOrderResultType.Success, orderId, "Success");
}
استفاده از یک enum مانند ProcessOrderResultType، مصرف کردن آبجکت نتیجه را با switch expressions آسانتر میکند. در اینجا enum برای نمایش ProcessOrderResult.Type آمده است:
public enum ProcessOrderResultType
{
NotProcessable = 0,
TooManyLineItems = 1,
NotReadyForProcessing = 2,
Success = 3
}
و حالا متد Process به این شکل در میآید:
const int MaxNumberOfLineItems = 15;
public ProcessOrderResult Process(Order? order)
{
if (!IsProcessable(order))
{
return ProcessOrderResult.NotProcessable();
}
if (order.Items.Count > MaxNumberOfLineItems)
{
return ProcessOrderResult.TooManyLineItems(order.Id);
}
if (order.Status != OrderStatus.ReadyToProcess)
{
return ProcessOrderResult.NotReadyForProcessing(order.Id);
}
order.IsProcessed = true;
order.Status = OrderStatus.Processed;
return ProcessOrderResult.Success(order.Id);
}
static bool IsProcessable(Order? order)
{
return order is not null &&
order.IsVerified &&
order.Items.Any();
}
`
در اینجا نحوه استفاده از یک enum برای ProcessOrderResult.Type به شما اجازه میدهد یک switch expression بنویسید:
var result = Process(order);
result.Type switch
{
ProcessOrderResultType.TooManyLineItems =>
Console.WriteLine($"Too many line items: {result.OrderId}"),
ProcessOrderResultType.NotReadyForProcessing =>
Console.WriteLine($"Not ready for processing {result.OrderId}"),
ProcessOrderResultType.Success =>
Console.WriteLine($"Processed successfully {result.OrderId}"),
_ => Console.WriteLine("Failed to process: {OrderId}", result.OrderId),
};
نکات پایانی 📝
تمام شد، ۸ نکته برای نوشتن کد تمیز:
1️⃣ اصل بازگشت زودهنگام
2️⃣ ادغام چندین دستور if
3️⃣ استفاده از LINQ برای اختصار
4️⃣ جایگزینی عبارت بولین با متد
5️⃣ ترجیح دادن پرتاب استثناهای سفارشی
6️⃣ جایگزینی اعداد جادویی با ثابتها
7️⃣ جایگزینی رشته جادویی با enumها
8️⃣ استفاده از الگوی آبجکت نتیجه
نوشتن کد تمیز، موضوعی از تمرین هدفمند و تجربه است.
اکثر مردم در مورد اصول کدنویسی تمیز میخوانند، اما تعداد کمی تلاش میکنند تا آنها را روزانه به کار ببرند.
اینجاست که شما میتوانید خود را متمایز کنید.
امیدوارم این مطلب مفید بوده باشد.
اقدام عملی امروز: 🚀
نگاهی به پروژه خود بیندازید و ببینید آیا برخی از اشتباهاتی که من در اینجا برجسته کردم را مرتکب میشوید یا نه. و سپس آنها را با استفاده از نکات کد تمیزی که با شما به اشتراک گذاشتم، برطرف کنید.