فصل یازدهم:🎯 پرداختن به مسائل فراگیر (Cross-Cutting Concerns)

وقتی که می‌خواهید کد تمیز (Clean Code) بنویسید، دو نوع مسئله (Concern) وجود دارد که باید به آن‌ها توجه کنید:

  1. مسائل اصلی (Core Concerns): دلایل ایجاد نرم‌افزار و علت توسعه آن هستند.
  2. مسائل فراگیر (Cross-Cutting Concerns): مسائلی که بخشی از نیازمندی‌های کسب‌وکار نیستند اما در تمام بخش‌های کد باید به آن‌ها توجه شود و با مسائل اصلی در ارتباط هستند.

این موضوع را می‌توان به شکل زیر در نمودار نشان داد: 📊

Conventions-UsedThis-Book

این فصل بر مسائل فراگیر (Cross-Cutting Concerns) تمرکز دارد و هدف ما ساخت یک کتابخانه کلاس قابل استفاده مجدد (Reusable Class Library) است که می‌توانید آن را به دلخواه خود تغییر دهید یا گسترش دهید.

مسائل فراگیر شامل موارد زیر هستند:

برای ساخت کتابخانه قابل استفاده مجدد، از الگوی دکوراتور (Decorator Pattern) و چارچوب PostSharp Aspect استفاده خواهیم کرد که در زمان کامپایل به پروژه تزریق می‌شود.

همچنین با مطالعه این فصل خواهید دید که برنامه‌نویسی مبتنی بر Attribute چگونه باعث می‌شود:

در نتیجه، تنها کدهای ضروری کسب‌وکار در متدهای شما باقی می‌ماند و کدهای تکراری مدیریت می‌شوند.

بسیاری از این ایده‌ها را قبلاً هم مطرح کرده‌ایم، اما دوباره ذکر می‌کنیم زیرا مسائل فراگیر هستند.

در این فصل، موضوعات زیر را پوشش می‌دهیم:

  1. الگوی دکوراتور (Decorator Pattern) 🏷️
  2. الگوی پراکسی (Proxy Pattern) 🛡️
  3. برنامه‌نویسی مبتنی بر جنبه (Aspect-Oriented Programming – AOP) با PostSharp ⚙️
  4. پروژه – کتابخانه قابل استفاده مجدد برای مسائل فراگیر

در پایان این فصل، شما توانایی‌های زیر را کسب خواهید کرد:

پیش‌نیازهای فنی:
برای بهره‌برداری کامل از این فصل، به Visual Studio 2019 و PostSharp نیاز دارید.
برای دریافت فایل‌های کد این فصل، به لینک زیر مراجعه کنید:
https://github.com/PacktPublishing/Clean-Code-in-C-/tree/master/CH11

بیایید با الگوی دکوراتور شروع کنیم:


الگوی دکوراتور (Decorator Pattern) 🏷️

الگوی طراحی دکوراتور یک الگوی ساختاری (Structural Pattern) است که برای افزودن قابلیت‌های جدید به یک شیء موجود بدون تغییر ساختار آن استفاده می‌شود.
در این الگو، کلاس اصلی در کلاس دکوراتور قرار می‌گیرد و رفتارها و عملیات جدید در زمان اجرا به شیء اضافه می‌شوند.

Conventions-UsedThis-Book

رابطه Component و اعضایی که دارد، توسط کلاس‌های ConcreteComponent و Decorator پیاده‌سازی می‌شود.


حال یک مثال می‌نویسیم که یک عملیات را در یک بلاک try/catch قرار می‌دهد.
در هر دو بخش try و catch، یک رشته به کنسول چاپ خواهد شد.

1️⃣ یک Console Application در .NET 4.8 به نام CH10_AddressingCrossCuttingConcerns بسازید.
2️⃣ یک فولدر به نام DecoratorPattern اضافه کنید.
3️⃣ یک interface جدید به نام IComponent اضافه کنید:

public interface IComponent {
   void Operation();
}

برای ساده نگه‌داشتن مثال، این interface فقط یک عملیات از نوع void دارد.


حالا که interface آماده است، یک کلاس انتزاعی اضافه می‌کنیم که آن را پیاده‌سازی کند:

public abstract class Decorator : IComponent {
    private IComponent _component;

    public Decorator(IComponent component) {
        _component = component;
    }

    public virtual void Operation() {
        _component.Operation();
    }
}

حال کلاس ConcreteComponent را می‌سازیم:

public class ConcreteComponent : IComponent {
    public void Operation() {
        throw new NotImplementedException();
    }
}

همان‌طور که می‌بینید، این کلاس فقط یک عملیات دارد که NotImplementedException پرتاب می‌کند.


در ادامه کلاس ConcreteDecorator را می‌نویسیم:

public class ConcreteDecorator : Decorator {
    public ConcreteDecorator(IComponent component) : base(component) { }

    public override void Operation() {
        try {
            Console.WriteLine("Operation: try block.");
            base.Operation();
        } catch(Exception ex)  {
            Console.WriteLine("Operation: catch block.");
            Console.WriteLine(ex.Message);
        }
    }
}

قبل از استفاده از کد، کلاس Program را به‌روزرسانی می‌کنیم. متد DecoratorPatternExample() را اضافه کنید:

private static void DecoratorPatternExample() {
    var concreteComponent = new ConcreteComponent();
    var concreteDecorator = new ConcreteDecorator(concreteComponent);
    concreteDecorator.Operation();
}

دو خط زیر را به Main() اضافه کنید:

DecoratorPatternExample();
Console.ReadKey();

Conventions-UsedThis-Book

با این، مرور ما بر الگوی دکوراتور (Decorator Pattern) به پایان رسید ✅.
حالا زمان آن است که به الگوی پراکسی (Proxy Pattern) بپردازیم.


الگوی پراکسی (Proxy Pattern) 🛡️

الگوی پراکسی یک الگوی طراحی ساختاری (Structural Design Pattern) است که اشیایی را فراهم می‌کند که به‌عنوان جایگزین برای اشیاء واقعی سرویس (Service Objects) مورد استفاده توسط کلاینت‌ها عمل می‌کنند.

Conventions-UsedThis-Book

یک نمونه‌ای از زمانی که می‌خواهید الگوی پراکسی (Proxy Pattern) را استفاده کنید، زمانی است که:

پروکسی‌ها کارها را به اشیاء دیگر واگذار می‌کنند. مگر اینکه پروکسی از سرویس مشتق شده باشد، متدهای پروکسی در نهایت باید به یک شیء Service ارجاع دهند.


ما یک پیاده‌سازی ساده از الگوی پراکسی را بررسی می‌کنیم:

1️⃣ یک فولدر به نام ProxyPattern در ریشه پروژه فصل ۱۱ اضافه کنید.
2️⃣ یک interface به نام IService با یک متد برای مدیریت درخواست بسازید:

public interface IService {
    void Request();
}

3️⃣ کلاس Service را اضافه کرده و رابط IService را پیاده‌سازی کنید:

public class Service : IService {
    public void Request() {
        Console.WriteLine("Service: Request();");
    }
}

4️⃣ حال، کلاس Proxy را می‌نویسیم:

public class Proxy : IService {
    private IService _service;

    public Proxy(IService service) {
        _service = service;
    }

    public void Request() {
        Console.WriteLine("Proxy: Request();");
        _service.Request();
    }
}

5️⃣ برای دیدن عملکرد، کلاس Program را به‌روزرسانی می‌کنیم:

private static void ProxyPatternExample() {
    Console.WriteLine("### Calling the Service directly. ###");
    var service = new Service();
    service.Request();

    Console.WriteLine("## Calling the Service via a Proxy. ###");
    new Proxy(service).Request();
}

با اجرای پروژه، خروجی مشابه نمونه زیر مشاهده خواهد شد: 📺

Conventions-UsedThis-Book

حال که با الگوی دکوراتور و الگوی پراکسی آشنا شدید، بیایید نگاهی به برنامه‌نویسی مبتنی بر جنبه (AOP) با PostSharp بیندازیم. ⚙️


برنامه‌نویسی مبتنی بر جنبه (AOP) با PostSharp 🧩


می‌توانید PostSharp را از لینک زیر دانلود کنید:
https://www.postsharp.net/download

نحوه کار AOP با PostSharp:
1️⃣ بسته PostSharp را به پروژه اضافه کنید.
2️⃣ کد خود را با Attributes نشانه‌گذاری کنید.
3️⃣ کامپایلر C# کد را به باینری تبدیل می‌کند و PostSharp باینری را تحلیل کرده و پیاده‌سازی Aspectها را تزریق می‌کند.


PostSharp الگوهای آماده خوبی برای شما ارائه می‌دهد:

اگر هیچکدام نیاز شما را برآورده نکرد، می‌توانید الگوهای خود را با توسعه Framework جنبه‌ها یا معماری خودکار کنید.


توسعه جنبه‌ها و چارچوب معماری

نکته: هنگام نوشتن Aspect و Attributes باید بسته PostSharp.Redist NuGet را اضافه کنید.


توسعه یک جنبه ساده


تزریق رفتار قبل و بعد از اجرای متد

جنبه OnMethodBoundaryAspect الگوی دکوراتور را پیاده‌سازی می‌کند.

جدول متدهای Advice در OnMethodBoundaryAspect:

Advice متد توضیح
OnEntry(MethodExecutionArgs) قبل از اجرای متد و قبل از هر کد کاربر اجرا می‌شود
OnSuccess(MethodExecutionArgs) بعد از اجرای موفق متد، بدون استثنا، اجرا می‌شود
OnException(MethodExecutionArgs) در صورت بروز Exception بعد از کد کاربر اجرا می‌شود، معادل catch است
OnExit(MethodExecutionArgs) هنگام خروج متد، چه موفق و چه با Exception، اجرا می‌شود، معادل finally است

نمونه جنبه Logging

1️⃣ ابتدا PostSharp را به پروژه اضافه کنید.
2️⃣ یک فولدر به نام Aspects بسازید و یک کلاس جدید به نام LoggingAspect اضافه کنید:

[PSerializable]
public class LoggingAspect : OnMethodBoundaryAspect { }

بازنویسی متدهای Advice

public override void OnEntry(MethodExecutionArgs args) {
    Console.WriteLine("The {0} method has been entered.", args.Method.Name);
}

public override void OnSuccess(MethodExecutionArgs args) {
    Console.WriteLine("The {0} method executed successfully.", args.Method.Name);
}

public override void OnExit(MethodExecutionArgs args) {
    Console.WriteLine("The {0} method has exited.", args.Method.Name);
}

public override void OnException(MethodExecutionArgs args) {
    Console.WriteLine("An exception was thrown in {0}.", args.Method.Name);
}

استفاده از LoggingAspect

1️⃣ متد موفق:

[LoggingAspect]
private static void SuccessfulMethod() {
    Console.WriteLine("Hello World, I am a success!");
}

2️⃣ متد ناموفق:

[LoggingAspect]
private static void FailedMethod() {
    Console.WriteLine("Hello World, I am a failure!");
    var x = 1;
    var y = 0;
    var z = x / y;
}

هر دو متد را از Main() فراخوانی کنید و پروژه را اجرا کنید. خروجی مشابه نمونه زیر خواهد بود: 📺

Conventions-UsedThis-Book

در این مرحله، Debugger باعث خروج برنامه خواهد شد. ✅
همان‌طور که می‌بینید، ایجاد جنبه‌های PostSharp خودتان برای برآورده کردن نیازهایتان، فرآیندی ساده است.

حالا به سراغ افزودن محدودیت‌های معماری (Architectural Constraints) می‌رویم.


توسعه چارچوب معماری 🏗️


ابتدا کلاس BusinessRulePatternValidation را اضافه کنید:

[MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)]
public class BusinessRulePatternValidation : ScalarConstraint { }

متد ValidateCode() را بازنویسی می‌کنیم:

public override void CodeValidation(object target)  {
    var targetType = (Type)target;
    if (targetType.GetNestedType("Factory") == null) {
        Message.Write(
            targetType, SeverityType.Warning,
            "10",
            "You must include a 'Factory' as a nested type for {0}.",
            targetType.DeclaringType,
            targetType.Name);
    }
}

کلاس BusinessRule را اضافه کنید:

[BusinessRulePatternValidation]
public class BusinessRule  { }

پروژه – کتابخانه قابل استفاده مجدد برای مسائل فراگیر

در این بخش، یک کتابخانه قابل استفاده مجدد برای مدیریت مسائل فراگیر می‌سازیم.


شروع پروژه

1️⃣ یک .NET Standard Class Library به نام CrossCuttingConcerns بسازید.
2️⃣ یک .NET Framework Console Application به نام TestHarness به پروژه اضافه کنید.


افزودن مسأله کشینگ (Caching) 🗄️


1️⃣ فولدری به نام Caching در پروژه CrossCuttingConcerns بسازید.
2️⃣ یک کلاس به نام MemoryCache اضافه کنید.
3️⃣ پکیج‌های NuGet زیر را اضافه کنید:


کلاس MemoryCache را به صورت زیر به‌روزرسانی کنید:

public static class MemoryCache {
    public static T GetItem<T>(string itemName, TimeSpan timeInCache, Func<T> itemCacheFunction) {
        var cache = System.Runtime.Caching.MemoryCache.Default;
        var cachedItem = (T) cache[itemName];
        if (cachedItem != null) return cachedItem;

        var policy = new CacheItemPolicy { AbsoluteExpiration = DateTimeOffset.Now.Add(timeInCache) };
        cachedItem = itemCacheFunction();
        cache.Set(itemName, cachedItem, policy);
        return cachedItem;
    }
}

افزودن کلاس تست

1️⃣ در پروژه TestHarness یک کلاس جدید به نام TestClass اضافه کنید.
2️⃣ متدهای GetCachedItem() و GetMessage() را اضافه کنید:

public string GetCachedItem() {
    return MemoryCache.GetItem<string>("Message", TimeSpan.FromSeconds(30), GetMessage);
}

private string GetMessage() {
    return "Hello, world of cache!";
}

به‌روزرسانی متد Main()

var harness = new TestClass();
Console.WriteLine(harness.GetCachedItem());
Console.WriteLine(harness.GetCachedItem());
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine(harness.GetCachedItem());

افزودن قابلیت لاگ‌گیری در فایل 📄

در پروژه ما، فرآیندهای Logging، Auditing و Instrumentation خروجی خود را به یک فایل متنی می‌فرستند.
برای این کار نیاز به یک کلاس داریم که:

1️⃣ بررسی کند فایل مورد نظر وجود دارد یا خیر و در صورت عدم وجود، آن را ایجاد کند.
2️⃣ خروجی را به فایل اضافه کرده و ذخیره کند.


1️⃣ یک فولدر به نام FileSystem در کتابخانه کلاس ایجاد کنید.
2️⃣ یک کلاس به نام LogFile بسازید و آن را public static تعریف کنید.
3️⃣ متغیرهای عضو زیر را اضافه کنید:

private static string _location = string.Empty;
private static string _filename = string.Empty;
private static string _file = string.Empty;

افزودن فولدر Logs در زمان اجرا

private static void AddDirectory() {
    if (!Directory.Exists(_location))
        Directory.CreateDirectory("Logs");
}

افزودن فایل در صورت عدم وجود

private static void AddFile() {
    _file = Path.Combine(_location, _filename);
    if (File.Exists(_file)) return;
    using (File.Create($"Logs\\{_filename}")) { }
}

متد ذخیره داده در فایل

public static void AppendTextToFile(string filename, string text) {
    _location = $"{Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location)}\\Logs";
    _filename = filename;
    AddDirectory();
    AddFile();
    File.AppendAllText(_file, text);
}

افزودن مسأله Logging 🖥️

1️⃣ فولدری به نام Logging در کتابخانه کلاس ایجاد کنید.
2️⃣ یک کلاس به نام ConsoleLoggingAspect اضافه کنید و کد زیر را وارد کنید:

[PSerializable]
public class ConsoleLoggingAspect : OnMethodBoundaryAspect { }

بازنویسی متدهای Advice برای لاگ‌گیری در کنسول

public override void OnEntry(MethodExecutionArgs args) {
    Console.WriteLine($"Method: {args.Method.Name}, OnEntry().");
}

public override void OnExit(MethodExecutionArgs args) {
    Console.WriteLine($"Method: {args.Method.Name}, OnExit().");
}

public override void OnSuccess(MethodExecutionArgs args) {
    Console.WriteLine($"Method: {args.Method.Name}, OnSuccess().");
}

public override void OnException(MethodExecutionArgs args) {
    Console.WriteLine($"An exception was thrown in {args.Method.Name}. {args}");
}

لاگ‌گیری در فایل متنی 📝

مثال OnEntry() در TextFileLoggingAspect:

public override void OnEntry(MethodExecutionArgs args) {
    LogFile.AppendTextToFile("Log.txt", $"\nMethod: {args.Method.Name}, OnEntry().");
}

با این کار قابلیت‌های لاگ‌گیری در فایل و کنسول آماده است.
در مرحله بعد، به مسأله Exceptions می‌پردازیم. ⚡

افزودن مسأله Exception-Handling ⚠️

در نرم‌افزار، تجربه Exceptions توسط کاربران اجتناب‌ناپذیر است.
پس باید روشی برای لاگ‌گیری آن‌ها داشته باشیم.


1️⃣ یک فولدر به نام Exceptions در کتابخانه کلاس ایجاد کنید.
2️⃣ یک فایل به نام ExceptionAspect بسازید و کد زیر را وارد کنید:

[PSerializable]
public class ExceptionAspect : OnExceptionAspect {
    public string Message { get; set; }
    public Type ExceptionType { get; set; }
    public FlowBehavior Behavior { get; set; }

    public override void OnException(MethodExecutionArgs args) {
        var message = args.Exception != null ? args.Exception.Message : "Unknown error occured.";
        LogFile.AppendTextToFile(
            "Exceptions.log", $"\n{DateTime.Now}: Method: {args.Method}, Exception: {message}"
        );
        args.FlowBehavior = FlowBehavior.Continue;
    }

    public override Type GetExceptionType(System.Reflection.MethodBase targetMethod) {
        return ExceptionType;
    }
}

افزودن مسأله Security 🔒

امنیت موضوع بزرگی است و فراتر از این کتاب می‌باشد.
منابع پیشنهادی:


ایجاد Component امن

1️⃣ فولدری به نام Security بسازید.
2️⃣ یک Interface به نام ISecureComponent اضافه کنید:

public interface ISecureComponent {
    void AddData(dynamic data);
    int EditData(dynamic data);
    int DeleteData(dynamic data);
    dynamic GetData(dynamic data);
}

ایجاد کلاس پایه DecoratorBase

public abstract class DecoratorBase : ISecureComponent {
    private readonly ISecureComponent _secureComponent;

    public DecoratorBase(ISecureComponent secureComponent) {
        _secureComponent = secureComponent;
    }

    public virtual void AddData(dynamic data) {
        _secureComponent.AddData(data);
    }

    public virtual int EditData(dynamic data) {
        return _secureComponent.EditData(data);
    }

    public virtual int DeleteData(dynamic data) {
        return _secureComponent.DeleteData(data);
    }

    public virtual dynamic GetData(dynamic data) {
        return _secureComponent.GetData(data);
    }
}

کلاس ConcreteSecureComponent


مدیریت دسترسی کاربران

public readonly struct Credentials {
    public static string Role { get; private set; }

    public Credentials(string username, string password) {
        switch (username) {
            case "System" when password == "Administrator":
                Role = "Administrator";
                break;
            case "End" when password == "User":
                Role = "Restricted";
                break;
            default:
                Role = "Imposter";
                break;
        }
    }
}

کلاس ConcreteDecorator برای امنیت

public class ConcreteDecorator : DecoratorBase {
    public ConcreteDecorator(ISecureComponent secureComponent) : base(secureComponent) { }

    public override void AddData(dynamic data) {
        if (Credentials.Role.Contains("Administrator") || Credentials.Role.Contains("Restricted")) {
            base.AddData((object)data);
        } else {
            throw new UnauthorizedAccessException("Unauthorized");
        }
    }

    public override int EditData(dynamic data) {
        if (Credentials.Role.Contains("Administrator")) {
            return base.EditData(data);
        }
        throw new UnauthorizedAccessException("Unauthorized");
    }

    public override int DeleteData(dynamic data) {
        if (Credentials.Role.Contains("Administrator")) {
            return base.DeleteData(data);
        }
        throw new UnauthorizedAccessException("Unauthorized");
    }

    public override dynamic GetData(dynamic data) {
        if (Credentials.Role.Contains("Administrator") || Credentials.Role.Contains("Restricted")) {
            return base.GetData(data);
        }
        throw new UnauthorizedAccessException("Unauthorized");
    }
}

آماده‌سازی برای اجرای امنیت

private static readonly ConcreteDecorator ConcreteDecorator = new ConcreteDecorator(
    new ConcreteSecureComponent()
);

private static void Main(string[] _) {
    new Credentials("End", "User");
    DoSecureWork();
    Console.WriteLine("Press any key to exit.");
    Console.ReadKey();
}

تعریف متد DoSecureWork()

private static void DoSecureWork() {
    AddData();
    EditData();
    DeleteData();
    GetData();
}

تعریف متد AddData() با ExceptionAspect ⚠️

[ExceptionAspect(consoleOutput: true)]
private static void AddData() {
    ConcreteDecorator.AddData("Hello, world!");
}

سایر متدهای داده‌ای


پس از اجرای برنامه، باید خروجی مشابه تصویر زیر را مشاهده کنید:

✅ متدها اجرا می‌شوند
✅ پیام‌ها در کنسول چاپ می‌شوند
✅ هر خطایی در Exceptions.log ثبت می‌شود

Conventions-UsedThis-Book

اکنون ما یک شیء مبتنی بر نقش داریم که همراه با مدیریت استثناها کار می‌کند. گام بعدی ما، پیاده‌سازی Validation Concern یا بررسی اعتبار داده‌ها است. ✅


افزودن Validation Concern 🔍

تمام داده‌های وارد شده توسط کاربر باید اعتبارسنجی شوند، چرا که ممکن است خطرناک، ناقص یا با فرمت اشتباه باشند. هدف این است که اطمینان حاصل کنیم داده‌ها پاک و ایمن هستند.

برای نمونه‌ی ما، اعتبارسنجی null را پیاده‌سازی می‌کنیم.

1️⃣ افزودن کلاس AllowNullAttribute

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue |
 AttributeTargets.Property)]
public class AllowNullAttribute : Attribute { }

2️⃣ افزودن enum ValidationFlags

[Flags]
public enum ValidationFlags {
    Properties = 1,
    Methods = 2,
    Arguments = 4,
    OutValues = 8,
    ReturnValues = 16,
    NonPublic = 32,
    AllPublicArguments = Properties | Methods | Arguments,
    AllPublic = AllPublicArguments | OutValues | ReturnValues,
    All = AllPublic | NonPublic
}

3️⃣ افزودن کلاس ReflectionExtensions

public static class ReflectionExtensions {
    private static bool IsCustomAttributeDefined<T>(this ICustomAttributeProvider value) where T : Attribute {
        return value.IsDefined(typeof(T), false);
    }

    public static bool AllowsNull(this ICustomAttributeProvider value) {
        return value.IsCustomAttributeDefined<AllowNullAttribute>();
    }

    public static bool MayNotBeNull(this ParameterInfo arg) {
        return !arg.AllowsNull() && !arg.IsOptional && !arg.ParameterType.IsValueType;
    }
}

4️⃣ افزودن کلاس DisallowNonNullAspect

[PSerializable]
public class DisallowNonNullAspect : OnMethodBoundaryAspect {
    private int[] _inputArgumentsToValidate;
    private int[] _outputArgumentsToValidate;
    private string[] _parameterNames;
    private bool _validateReturnValue;
    private string _memberName;
    private bool _isProperty;

    public DisallowNonNullAspect() : this(ValidationFlags.AllPublic) { }

    public DisallowNonNullAspect(ValidationFlags validationFlags) {
        ValidationFlags = validationFlags;
    }

    public ValidationFlags ValidationFlags { get; set; }
}

5️⃣ Override متد CompileTimeValidate

public override bool CompileTimeValidate(MethodBase method) {
    var methodInformation = MethodInformation.GetMethodInformation(method);
    var parameters = method.GetParameters();

    if (!ValidationFlags.HasFlag(ValidationFlags.NonPublic) && !methodInformation.IsPublic) return false;
    if (!ValidationFlags.HasFlag(ValidationFlags.Properties) && methodInformation.IsProperty) return false;
    if (!ValidationFlags.HasFlag(ValidationFlags.Methods) && !methodInformation.IsProperty) return false;

    _parameterNames = parameters.Select(p => p.Name).ToArray();
    _memberName = methodInformation.Name;
    _isProperty = methodInformation.IsProperty;

    var argumentsToValidate = parameters.Where(p => p.MayNotBeNull()).ToArray();
    _inputArgumentsToValidate = ValidationFlags.HasFlag(ValidationFlags.Arguments) ?
                                argumentsToValidate.Where(p => !p.IsOut).Select(p => p.Position).ToArray() :
                                new int[0];

    _outputArgumentsToValidate = ValidationFlags.HasFlag(ValidationFlags.OutValues) ?
                                 argumentsToValidate.Where(p => p.ParameterType.IsByRef).Select(p => p.Position).ToArray() :
                                 new int[0];

    if (!methodInformation.IsConstructor) {
        _validateReturnValue = ValidationFlags.HasFlag(ValidationFlags.ReturnValues) &&
                               methodInformation.ReturnParameter.MayNotBeNull();
    }

    var validationRequired = _validateReturnValue || _inputArgumentsToValidate.Length > 0 || _outputArgumentsToValidate.Length > 0;
    return validationRequired;
}

6️⃣ Override متد OnEntry

public override void OnEntry(MethodExecutionArgs args) {
    foreach (var argumentPosition in _inputArgumentsToValidate) {
        if (args.Arguments[argumentPosition] != null) continue;
        var parameterName = _parameterNames[argumentPosition];
        if (_isProperty) {
            throw new ArgumentNullException(parameterName, $"Cannot set the value of property '{_memberName}' to null.");
        } else {
            throw new ArgumentNullException(parameterName);
        }
    }
}

7️⃣ Override متد OnSuccess

public override void OnSuccess(MethodExecutionArgs args) {
    foreach (var argumentPosition in _outputArgumentsToValidate) {
        if (args.Arguments[argumentPosition] != null) continue;
        var parameterName = _parameterNames[argumentPosition];
        throw new InvalidOperationException($"Out parameter '{parameterName}' is null.");
    }

    if (!_validateReturnValue || args.ReturnValue != null) return;

    if (_isProperty) {
        throw new InvalidOperationException($"Return value of property '{_memberName}' is null.");
    }
    throw new InvalidOperationException($"Return value of method '{_memberName}' is null.");
}

8️⃣ افزودن کلاس MethodInformation

private class MethodInformation {
    public string Name { get; private set; }
    public bool IsProperty { get; private set; }
    public bool IsPublic { get; private set; }
    public bool IsConstructor { get; private set; }
    public ParameterInfo ReturnParameter { get; private set; }

    private MethodInformation(ConstructorInfo constructor) : this((MethodBase)constructor) {
        IsConstructor = true;
        Name = constructor.Name;
    }

    private MethodInformation(MethodInfo method) : this((MethodBase)method) {
        IsConstructor = false;
        Name = method.Name;
        if (method.IsSpecialName && (Name.StartsWith("set_") || Name.StartsWith("get_"))) {
            Name = Name.Substring(4);
            IsProperty = true;
        }
        ReturnParameter = method.ReturnParameter;
    }

    private MethodInformation(MethodBase method) {
        IsPublic = method.IsPublic;
    }

    private static MethodInformation CreateInstance(MethodInfo method) {
        return new MethodInformation(method);
    }

    public static MethodInformation GetMethodInformation(MethodBase methodBase) {
        var ctor = methodBase as ConstructorInfo;
        if (ctor != null) return new MethodInformation(ctor);
        var method = methodBase as MethodInfo;
        return method == null ? null : CreateInstance(method);
    }
}

📌 اکنون Validation Aspect آماده است و می‌توانید:

گام بعدی ما اضافه کردن Transaction Concern خواهد بود.

اکنون کتابخانه ما شامل چند Cross-Cutting Concern آماده و قابل استفاده است. بیایید بخش‌هایی که اضافه کرده‌ایم را به طور خلاصه مرور کنیم: ✅


1️⃣ Transaction Concern 💳

[PSerializable]
[AttributeUsage(AttributeTargets.Method)]
public sealed class RequiresTransactionAspect : OnMethodBoundaryAspect {
    public override void OnEntry(MethodExecutionArgs args) {
        var transactionScope = new TransactionScope(TransactionScopeOption.Required);
        args.MethodExecutionTag = transactionScope;
    }
    public override void OnSuccess(MethodExecutionArgs args) {
        var transactionScope = (TransactionScope)args.MethodExecutionTag;
        transactionScope.Complete();
    }
    public override void OnExit(MethodExecutionArgs args) {
        var transactionScope = (TransactionScope)args.MethodExecutionTag;
        transactionScope.Dispose();
    }
}

2️⃣ Resource Pool Concern 🏊‍♂️

public class ResourcePool<T> {
    private readonly ConcurrentBag<T> _resources;
    private readonly Func<T> _resourceGenerator;

    public ResourcePool(Func<T> resourceGenerator) {
        _resourceGenerator = resourceGenerator ?? throw new ArgumentNullException(nameof(resourceGenerator));
        _resources = new ConcurrentBag<T>();
    }

    public T Get() => _resources.TryTake(out T item) ? item : _resourceGenerator();
    public void Return(T item) => _resources.Add(item);
}
var pool = new ResourcePool<Course>(() => new Course());
var course = pool.Get();
pool.Return(course);

3️⃣ Configuration Settings Concern ⚙️

public static class Settings {
    public static string GetAppSetting(string key) {
        return System.Configuration.ConfigurationManager.AppSettings[key];
    }
    public static void SetAppSettings(this string key, string value) {
        System.Configuration.ConfigurationManager.AppSettings[key] = value;
    }
}
Console.WriteLine(GetAppSetting("Greeting"));
"Greeting".SetAppSettings("Goodbye!");
Console.WriteLine(GetAppSetting("Greeting"));

4️⃣ Instrumentation Concern ⏱️

[PSerializable]
[AttributeUsage(AttributeTargets.Method)]
public class InstrumentationAspect : OnMethodBoundaryAspect {
    public override void OnEntry(MethodExecutionArgs args) {
        LogFile.AppendTextToFile("Profile.log", $"\nMethod: {args.Method.Name}, Start Time: {DateTime.Now}");
        args.MethodExecutionTag = Stopwatch.StartNew();
    }
    public override void OnException(MethodExecutionArgs args) {
        LogFile.AppendTextToFile("Exception.log", $"\n{DateTime.Now}: {args.Exception.Source} {args.Exception.Message}");
    }
    public override void OnExit(MethodExecutionArgs args) {
        var stopwatch = (Stopwatch)args.MethodExecutionTag;
        stopwatch.Stop();
        LogFile.AppendTextToFile("Profile.log", $"\nMethod: {args.Method.Name}, Stop Time: {DateTime.Now}, Duration: {stopwatch.Elapsed}");
    }
}

✅ نتیجه

حالا شما یک کتابخانه کامل از Cross-Cutting Concerns دارید که شامل موارد زیر است:

  1. Caching – ذخیره موقت داده‌ها
  2. File Logging & Console Logging – لاگ‌گیری
  3. Exception Handling – مدیریت استثناء‌ها
  4. Security – امنیت مبتنی بر نقش
  5. Validation – اعتبارسنجی داده‌ها
  6. Transactions – تراکنش‌ها
  7. Resource Pooling – استفاده مجدد از منابع
  8. Configuration – مدیریت تنظیمات
  9. Instrumentation – پروفایلینگ متدها

تمام این موارد با AOP و Decorator Pattern پیاده‌سازی شده‌اند و شما می‌توانید به راحتی در پروژه‌های خود از آن‌ها استفاده کنید.

خلاصه فصل

در این فصل، ما موارد زیر را یاد گرفتیم:

  1. الگوی دکوراتور (Decorator Pattern)

    • امکان اضافه کردن رفتار جدید به اشیاء بدون تغییر کلاس اصلی.
  2. الگوی پراکسی (Proxy Pattern)

    • ایجاد شیء جایگزین برای سرویس واقعی.
    • پراکسی درخواست‌های مشتری را دریافت و پردازش کرده و به سرویس اصلی منتقل می‌کند.
    • پراکسی و سرویس یک رابط (interface) مشترک دارند، بنابراین قابل جایگزینی هستند.
  3. برنامه‌نویسی مبتنی بر جنبه (AOP) با PostSharp

    • Aspect و Attribute برای تزریق خودکار کد در زمان کامپایل استفاده می‌شوند.

    • امکان مدیریت Cross-Cutting Concerns مانند:

      • Logging (لاگ‌گیری)
      • Auditing (ممیزی)
      • Security (امنیت)
      • Validation (اعتبارسنجی)
      • Exception Handling (مدیریت استثناء‌ها)
      • Instrumentation (پروفایلینگ متدها)
      • Transactions (تراکنش‌ها)
      • Resource Pooling (استفاده مجدد از منابع)
      • Caching (ذخیره موقت داده‌ها)
      • Threading & Concurrency (چندنخی و همزمانی)
  4. گسترش فریمورک Aspect

    • ساخت Aspect سفارشی و اعمال آن روی متدها یا کلاس‌ها.
    • استفاده از PostSharp و الگوی دکوراتور برای مدیریت Concerns به‌صورت تمیز و قابل نگهداری.

سوالات مرور

  1. Cross-Cutting Concern چیست و AOP مخفف چیست؟

    • Cross-Cutting Concern: مسائلی که بر بخش‌های مختلف برنامه اثر می‌گذارند و نمی‌توان آن‌ها را در یک ماژول خاص محدود کرد (مثل لاگ، امنیت، تراکنش).
    • AOP: Aspect-Oriented Programming یا برنامه‌نویسی مبتنی بر جنبه.
  2. Aspect چیست و چگونه آن را اعمال می‌کنید؟

    • Aspect: واحدی از رفتار که می‌تواند به روش‌های مختلف برنامه اضافه شود (مثلاً Logging).
    • اعمال از طریق افزودن Attribute روی کلاس، متد، پارامتر یا property انجام می‌شود.
  3. Attribute چیست و چگونه آن را اعمال می‌کنید؟

    • Attribute: Metadata یا داده‌های توصیفی برای کد.
    • با قرار دادن [AttributeName] روی کلاس یا متد اعمال می‌شود.
  4. Aspects و Attributes چگونه با هم کار می‌کنند؟

    • Attribute جنبه (Aspect) را مشخص می‌کند.
    • PostSharp در زمان کامپایل کد مربوط به Aspect را در محل مورد نظر تزریق می‌کند.
  5. فرآیند ساخت (Build Process) با Aspects چگونه کار می‌کند؟

    • کامپایلر کد را به باینری تبدیل می‌کند.
    • PostSharp باینری را تحلیل کرده و کد Aspect را تزریق می‌کند.
    • نتیجه: کد اصلی دست‌نخورده باقی می‌ماند، اما رفتارهای اضافی در زمان اجرا اعمال می‌شوند.

مطالعه بیشتر