دو الگوی Iterator (پیمایشگر) و Observer (ناظر) از دسته الگوهای رفتاری (Behavioral Patterns) GoF، بنیان متفاوتی برای دسترسی و انتقال داده ارائه میدهند. در الگوی Iterator، مصرفکننده کنترلی Pull-based بر جریان دریافت داده دارد؛ در حالی که Observer مبتنی بر مکانیزم Push-based بوده و با وقوع رویدادها، تغییرات را به اطلاع ناظران میرساند.
در این مقاله تخصصی، معماری، پیادهسازی متون فنی، نحوه ترکیب این دو الگو در قالب Reactive Extensions (Rx) و رفتارهای حافظهای آنها را در سناریوهای واقعی پردازش داده بررسی میکنیم.
الگوی Iterator امکان پیمایش ترتیبی عناصر یک شیء مرکب (Collection/Stream) را بدون افشای ساختار داخلی آن فراهم میسازد. در مدل پردازش داده، این الگو شبیه به یک شیر خروجی عمل میکند: مصرفکننده هر زمان که آمادگی داشت، عنصر بعدی را درخواست میکند (()MoveNext و Current).
معماری و ساختار داخلی
+------------------+ +-----------------+
| IEnumerable |------------------->| IEnumerator |
+------------------+ +-----------------+
| + GetEnumerator()| | + MoveNext() |
+------------------+ | + Current |
| + Reset() |
+-----------------+
پیادهسازی Lazy Evaluation و Pipeline Processing
یکی از بزرگترین مزایای Iterator در پردازش داده، ارزیابی تنبل (Lazy Evaluation) است. برخلاف مجموعههای مقیم در حافظه (In-Memory Collections) مانند List یا Array که تمام عناصر را یکجا بارگذاری میکنند، Iterator میتواند دادهها را به صورت قطعهقطعه (Chunked) یا دانهدانه (Element-by-Element) از دیسک، پایگاه داده یا شبکه بخواند.
کد زیر پیادهسازی یک Pipeline پردازش فایل لوگ بزرگ را با استفاده از C# و قابلیت yield return نشان میدهد:
public class LogProcessor
{
// ۱. خواندن تنبل خطوط فایل بدون بارگذاری کامل در RAM
public IEnumerable<string> ReadLogFileLazy(string filePath)
{
using var reader = new StreamReader(filePath);
string? line;
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
// ۲. فیلتر کردن دادهها در مسیر پیمایش
public IEnumerable<string> FilterErrors(IEnumerable<string> logLines)
{
foreach (var line in logLines)
{
if (line.Contains("[ERROR]", StringComparison.OrdinalIgnoreCase))
{
yield return line;
}
}
}
// ۳. تبدیل (Transformation) داده
public IEnumerable<ParsedLog> ParseLogs(IEnumerable<string> errorLines)
{
foreach (var line in errorLines)
{
var parts = line.Split(' ');
yield return new ParsedLog
{
Timestamp = DateTime.Parse(parts[0]),
Message = string.Join(' ', parts.Skip(2))
};
}
}
}
public class ParsedLog
{
public DateTime Timestamp { get; set; }
public string Message { get; set; } = string.Empty;
}
هنگامی که حلقه foreach روی ParseLogs اجرا میشود، اجرای برنامه قطعهبهقطعه بین متدها جابهجا میشود. هیچگاه بیش از یک خط از فایل لوگ در حافظه قرار نمیگیرد. مدیریت مصرف حافظه در این رویکرد O(1) است، در حالی که بدون Iterator مصرف حافظه O(N) خواهد بود.
الگوی Observer وابستگی یک-به-چند (One-to-Many) بین اشياء تعریف میکند تا وقتی یک شیء (Subject/Publisher) تغییر حالت داد، تمام وابستگان آن (Observers/Subscribers) به صورت خودکار مطلع و بهروزرسانی شوند.
معماری و ساختار
در سناریوهای دادهمحور، Publisher منابع تولید رویداد (مانند سنسورهای IoT، پیامهای RabbitMQ/Kafka، یا تغییرات قیمت در بازار بورس) است و Subscriberها موتورهای پردازش یا ذخیرهسازی داده هستند.
+--------------------+ +-------------------+
| ISubject | | IObserver |
+--------------------+ +-------------------+
| + Attach(Observer) |<--------------------| + Update(data) |
| + Detach(Observer) | +-------------------+
| + Notify() | ^
+--------------------+ |
^ |
| |
+--------------------+ +-------------------+
| ConcreteSubject | | ConcreteObserver |
+--------------------+ +-------------------+
پیادهسازی پردازش جریانی رویدادمحور (Event-Driven Processing)
کد زیر پیادهسازی کلاسیک الگوی Observer برای پردازش جریانی دادههای حسگرهای صنعتی را نشان میدهد:
public interface IObserver<T>
{
void OnNext(T value);
void OnError(Exception error);
void OnCompleted();
}
public interface IObservable<T>
{
IDisposable Subscribe(IObserver<T> observer);
}
public class TelemetryData
{
public string SensorId { get; set; } = string.Empty;
public double Temperature { get; set; }
public DateTime Timestamp { get; set; }
}
// Publisher (Subject)
public class SensorTelemetryStream : IObservable<TelemetryData>
{
private readonly List<IObserver<TelemetryData>> _observers = new();
public IDisposable Subscribe(IObserver<TelemetryData> observer)
{
if (!_observers.Contains(observer))
_observers.Add(observer);
return new Unsubscriber(_observers, observer);
}
public void PublishData(TelemetryData data)
{
foreach (var observer in _observers.ToList())
{
try
{
observer.OnNext(data);
}
catch (Exception ex)
{
observer.OnError(ex);
}
}
}
private class Unsubscriber : IDisposable
{
private readonly List<IObserver<TelemetryData>> _observers;
private readonly IObserver<TelemetryData> _observer;
public Unsubscriber(List<IObserver<TelemetryData>> observers, IObserver<TelemetryData> observer)
{
_observers = observers;
_observer = observer;
}
public void Dispose()
{
if (_observer != null && _observers.Contains(_observer))
_observers.Remove(_observer);
}
}
}
برای انتخاب صحیح بین Iterator و Observer در سیستمهای توزیعشده یا برنامههای با کارایی بالا (High-Performance)، تحلیل جدول زیر ضروری است:
| معیار | Iterator (Pull-Based) | Observer (Push-Based) |
| جهت جریان داده | Consumer \leftarrow Producer | Producer \rightarrow Consumer |
| کنترل سرعت (Control Flow) | در اختیار Consumer | در اختیار Producer |
| مدیریت حافظه (Backpressure) | عالی (اعمال فشار معکوس طبیعی) | نیازمند مکانیزمهای صریح کنترل خفگی/بافر |
| مناسب برای | دادههای ایستا، فایلها، DB Readers، Batch | دادههای زنده، سنسورها، سیستمهای real-time |
| تاخیر (Latency) | بالاتر (باید درخواست داده ارسال شود) | بسیار پایین (بلافاصله پس از تولید داده) |
| تعدد مصرفکنندگان | معمولاً تکمصرفکننده (Single Consumer) | چندمصرفکننده (Multicast) |
پارادایم برنامهنویسی واکنشی (Reactive Programming) و مشخصاً Reactive Extensions (Rx) بر پایه همزادپنداری ریاضیاتی (Duality) بین این دو الگو شکل گرفته است.
از نظر ریاضی:
Iterator: IEnumerable<T> \rightarrow \text{Pull Data}
Observer: IObservable<T> \rightarrow \text{Push Data}
الگوی IObservable<T> در واقع همان IEnumerable<T> است، اما در بعد زمان وارون (Dual) شده است.
مشکل Backpressure و حل آن
در الگوی Push (Observer)، اگر سرعت تولید داده توسط Producer بسیار بیشتر از سرعت پردازش Consumer باشد، سیستم دچار سرریز حافظه (Out of Memory) میشود. به این چالش Backpressure میگویند. برای حل آن، سیستمهای مدرن پردازش داده (مانند Reactive Streams / RxPy / System.Reactive) الگوی Observer را با ویژگیهای کنترلی Iterator ترکیب میکنند.
کد زیر با استفاده از Reactive Extensions (Rx) نشان میدهد چگونه میتوان یک جریانی از دادههای زنده را مانند یک مجموعه Iterator نگاشت، فیلتر و windowing نمود:
using System.Reactive.Linq;
using System.Reactive.Subjects;
public class ReactiveDataStream
{
public static void ProcessStream()
{
var sensorStream = new Subject<TelemetryData>();
// ترکیب الگوی Observer با اپراتورهای فلئونت شبیه به LINQ (Iterator-style)
IDisposable subscription = sensorStream
.Where(data => data.Temperature > 75.0) // فیلتر دادهها
.Buffer(TimeSpan.FromSeconds(5)) // بافر کردن ۵ ثانیهای (کنترل Backpressure)
.Select(batch => new
{
Count = batch.Count,
AvgTemp = batch.Count > 0 ? batch.Average(b => b.Temperature) : 0
})
.Subscribe(
summary => Console.WriteLine($"[BATCH SUMMARY] Count: {summary.Count}, Avg Temp: {summary.AvgTemp:F2}"),
ex => Console.WriteLine($"Error: {ex.Message}"),
() => Console.WriteLine("Stream Completed")
);
// شبیهسازی ورود داده
sensorStream.OnNext(new TelemetryData { Temperature = 80, Timestamp = DateTime.Now });
sensorStream.OnNext(new TelemetryData { Temperature = 90, Timestamp = DateTime.Now });
// لغو اشتراک
subscription.Dispose();
}
}
در مقیاسهای صنعتی و پردازشهای سنگین، پیادهسازی این الگوها نیازمند دقت در نکات ریز معماری است:
۱. Thread Safety در الگوی Observer
در سیستمهای Multi-threaded، متد PublishData ممکن است همزمان با متدهای Subscribe یا Unsubscribe فراخوانی شود. برای جلوگیری از خطای Race Condition یا InvalidOperationException هنگام پیمایش لیست Observerها:
۲. نشت حافظه (Memory Leaks) در الگوی Observer (مسئله Lapsed Listener)
اگر یک Subscriber دارای طول عمر (Lifetime) کوتاه باشد اما به یک Publisher با طول عمر طولانی (مثلاً یک Singleton Service) بپیوندد، Subscriber تا زمانی که Publisher زنده است از Garbage Collector (GC) پاک نخواهد شد.
۳. State Management در Iterator
پیادهسازی سفارشی Iterator با استفاده از State Machine در کامپایلر انجام میشود. در صورتی که ساختار داده زیرین در حین پیمایش تغییر کند (Concurrent Modification)، Iterator باید خطای سریع (Fail-Fast) صادر کند تا از ناهماهنگی داده جلوگیری شود.
الگوهای Iterator و Observer دو رکن مکمل در مهندسی نرمافزار برای ساخت Pipelineهای پردازش داده هستند:
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.