paint-brush
نحوه برخورد با پیچیدگی هنگام طراحی سیستم های نرم افزاریتوسط@fairday
64,464 قرائت
64,464 قرائت

نحوه برخورد با پیچیدگی هنگام طراحی سیستم های نرم افزاری

توسط Aleksei23m2024/02/05
Read on Terminal Reader
Read this story w/o Javascript

خیلی طولانی؛ خواندن

پیچیدگی دشمن است! بیایید یاد بگیریم که چگونه با آن مقابله کنیم!
featured image - نحوه برخورد با پیچیدگی هنگام طراحی سیستم های نرم افزاری
Aleksei HackerNoon profile picture

این همه در مورد چیست؟

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


فهرست کردن همه مشکلات چالش برانگیز و شاید حتی غیرممکن است. تنها در صورتی با برخی از این مسائل مواجه خواهید شد که در یک جایگاه خاص کار کنید. از سوی دیگر، موارد بسیاری وجود دارد که همه ما باید نحوه حل آنها را بدانیم، زیرا برای ساختن سیستم های IT بسیار مهم هستند. با احتمال زیاد در تمامی پروژه ها با آنها مواجه خواهید شد.


در این مقاله تجربیات خود را در مورد برخی از مشکلاتی که در هنگام ایجاد برنامه های نرم افزاری با آن مواجه شده ام به اشتراک می گذارم.

نگرانی متقاطع چیست؟

اگر به ویکی پدیا نگاهی بیندازیم، تعریف زیر را خواهیم یافت


در توسعه نرم افزار جنبه گرا، نگرانی های مقطعی جنبه هایی از یک برنامه هستند که بر چندین ماژول تأثیر می گذارند، بدون اینکه امکان محصور شدن در هیچ یک از آنها وجود نداشته باشد. این نگرانی‌ها اغلب نمی‌توانند در طراحی و پیاده‌سازی از بقیه سیستم جدا شوند و می‌توانند منجر به پراکندگی (تکثیر کد)، درهم‌تنیدگی (وابستگی‌های قابل توجه بین سیستم‌ها) یا هر دو شوند.


آن را تا حد زیادی توصیف می کند که چیست، اما من می خواهم آن را کمی بسط و ساده کنم:

یک نگرانی متقابل مفهوم یا جزء سیستم/سازمان است که بر بسیاری از بخش‌های دیگر تأثیر می‌گذارد (یا «برش می‌دهد»).


بهترین نمونه‌های این نگرانی‌ها عبارتند از معماری سیستم، ورود به سیستم، امنیت، مدیریت تراکنش، تله متری، طراحی پایگاه داده و بسیاری موارد دیگر. در ادامه این مقاله در مورد بسیاری از آنها توضیح خواهیم داد.


در سطح کد، نگرانی‌های مقطعی اغلب با استفاده از تکنیک‌هایی مانند برنامه‌نویسی جنبه‌گرا (AOP) پیاده‌سازی می‌شوند، جایی که این نگرانی‌ها به اجزای جداگانه‌ای تبدیل می‌شوند که می‌توانند در سراسر برنامه اعمال شوند. این امر منطق تجاری را از این نگرانی‌ها جدا نگه می‌دارد و کد را خواناتر و قابل نگهداری‌تر می‌کند.

طبقه بندی جنبه ها

روش‌های زیادی برای طبقه‌بندی جنبه‌ها با تقسیم‌بندی آنها با ویژگی‌های مختلف مانند محدوده، اندازه، عملکرد، اهمیت، هدف و موارد دیگر وجود دارد، اما در این مقاله، من قصد دارم از یک طبقه‌بندی دامنه ساده استفاده کنم. منظور من از این است که این جنبه خاص به کجا هدایت می شود، خواه کل سازمان باشد، یک سیستم خاص، یا یک عنصر خاص از آن سیستم.


بنابراین، من می خواهم جنبه ها را به ماکرو و میکرو تقسیم کنم.


منظور من از جنبه کلان عمدتاً ملاحظاتی است که ما برای کل سیستم دنبال می کنیم مانند معماری سیستم انتخابی و طراحی آن (یکپارچه، میکروسرویس، معماری سرویس گرا)، پشته فناوری، ساختار سازمانی و غیره. جنبه های کلان عمدتاً به استراتژیک و سطح بالا مربوط می شود. تصمیمات


در این بین، جنبه Micro به سطح کد و توسعه بسیار نزدیکتر است. به عنوان مثال، از کدام چارچوب برای تعامل با پایگاه داده، ساختار پروژه پوشه ها و کلاس ها، یا حتی الگوهای طراحی شی خاص استفاده می شود.


در حالی که این طبقه‌بندی ایده‌آل نیست، به ساختار درک مشکلات احتمالی و اهمیت و تأثیر راه‌حل‌هایی که برای آنها اعمال می‌کنیم کمک می‌کند.


در این مقاله، تمرکز اصلی من بر روی جنبه های کلان خواهد بود.

جنبه های کلان

ساختار سازمانی

وقتی تازه شروع به یادگیری معماری نرم افزار کردم، مقالات جالب زیادی در مورد قانون کانوی و تاثیر آن بر ساختار سازمانی خواندم. مخصوصا این یکی بنابراین، این قانون بیان می کند که


هر سازمانی که سیستمی را طراحی می کند (به طور کلی تعریف می شود) طرحی تولید می کند که ساختار آن کپی ساختار ارتباطی سازمان باشد.


من همیشه معتقد بودم که این مفهوم در واقع بسیار جهانی است و قانون طلایی را نشان می دهد.


سپس شروع به یادگیری رویکرد طراحی دامنه محور (DDD) اریک ایوانز برای سیستم های مدل سازی کردم. اریک ایوانز بر اهمیت شناسایی زمینه محدود تاکید می کند. این مفهوم شامل تقسیم یک مدل دامنه پیچیده به بخش های کوچکتر و قابل مدیریت تر است که هر کدام مجموعه محدودی از دانش خود را دارند. این رویکرد به ارتباطات تیمی مؤثر کمک می کند، زیرا نیاز به دانش گسترده از کل دامنه را کاهش می دهد و تغییر زمینه را به حداقل می رساند، بنابراین مکالمات را کارآمدتر می کند. تغییر متن بدترین و پرمصرف‌ترین چیزی است که تاکنون وجود داشته است. حتی کامپیوترها هم با آن دست و پنجه نرم می کنند. اگرچه بعید است که به غیاب کامل تغییر زمینه دست یابیم، به نظر من این چیزی است که باید برای آن تلاش کنیم.


Fantasy about keeping in mind a lot of bounded contexts

با بازگشت به قانون کانوی، چندین مشکل در آن پیدا کردم.


اولین مسئله ای که من با قانون کانوی مواجه شدم، که نشان می دهد طراحی سیستم ساختار سازمانی را آینه می کند، پتانسیل شکل گیری زمینه های محدود پیچیده و جامع است. این پیچیدگی زمانی به وجود می آید که ساختار سازمانی با مرزهای دامنه همسو نباشد و منجر به ایجاد زمینه های محدودی شود که به شدت به یکدیگر وابسته و مملو از اطلاعات هستند. این منجر به تغییر متن مکرر برای تیم توسعه می شود.


مسئله دیگر این است که اصطلاحات سازمانی به سطح کد نشت می کند. هنگامی که ساختارهای سازمانی تغییر می کند، نیاز به اصلاحات پایه کد و مصرف منابع ارزشمند دارد.


بنابراین، پیروی از مانور Inverse Conway به ساختن سیستم و سازمانی کمک می‌کند که معماری نرم‌افزار مورد نظر را تشویق کند. با این حال، قابل ذکر است که این رویکرد در معماری و سازه‌های از قبل شکل‌گرفته به‌خوبی کار نمی‌کند، زیرا تغییرات در این مرحله طولانی‌تر است، اما در استارت‌آپ‌ها به‌طور استثنایی عمل می‌کند، زیرا آن‌ها به سرعت تغییراتی را ارائه می‌کنند.

توپ بزرگ گلی

این الگو یا «ضد الگو» باعث ایجاد یک سیستم بدون هیچ معماری می شود. هیچ قانون، هیچ مرزی، و هیچ استراتژی برای کنترل پیچیدگی رو به رشد اجتناب ناپذیر وجود ندارد. پیچیدگی سرسخت ترین دشمن در سفر ساختن سیستم های نرم افزاری است.


Entertaining illustration made by ChatGPT

برای جلوگیری از ساخت چنین سیستمی، باید قوانین و محدودیت های خاصی را دنبال کنیم.

معماری سیستم

تعاریف بی شماری برای معماری نرم افزار وجود دارد. من بسیاری از آنها را دوست دارم زیرا جنبه های مختلفی از آن را پوشش می دهند. با این حال، برای اینکه بتوانیم در مورد معماری استدلال کنیم، طبیعتاً باید برخی از آنها را در ذهن خود شکل دهیم. و قابل ذکر است که این تعریف ممکن است متحول شود. بنابراین، حداقل در حال حاضر، شرح زیر را برای خودم دارم.


معماری نرم افزار درباره تصمیمات و انتخاب هایی است که هر روز می گیرید و بر سیستم ساخته شده تأثیر می گذارد.


برای تصمیم گیری که باید اصول و الگوهای "کیف" خود را برای حل مشکلات پیش آمده داشته باشید، همچنین ضروری است که بیان کنید که درک الزامات کلیدی برای ساختن آنچه یک کسب و کار به آن نیاز دارد است. با این حال، گاهی اوقات الزامات شفاف نیستند یا حتی تعریف نشده اند، در این مورد، بهتر است منتظر بمانید تا شفاف سازی بیشتری دریافت کنید یا به تجربه خود تکیه کنید و به شهود خود اعتماد کنید. اما به هر حال، اگر اصول و الگوهایی برای تکیه نداشته باشید، نمی توانید به درستی تصمیم بگیرید. اینجاست که من به تعریف سبک معماری نرم افزار می رسم.


سبک معماری نرم افزار مجموعه ای از اصول و الگوهایی است که نحوه ساخت نرم افزار را مشخص می کند.


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


به عنوان مثال، مانند:

  1. معماری یکپارچه

  2. طراحی دامنه محور

  3. مبتنی بر مولفه

  4. میکروسرویس ها

  5. لوله و فیلتر

  6. رویداد محور

  7. میکروکرنل

  8. خدمات محور


و غیره…


البته، آنها مزایا و معایب خود را دارند، اما مهمترین چیزی که من یاد گرفتم این است که معماری به تدریج در حالی که به مشکلات واقعی بستگی دارد، تکامل می یابد. شروع با معماری یکپارچه یک انتخاب عالی برای کاهش پیچیدگی های عملیاتی است، به احتمال زیاد این معماری حتی پس از رسیدن به مرحله تناسب محصول-بازار (PMI) ساخت محصول، با نیازهای شما مطابقت خواهد داشت. در مقیاس، ممکن است حرکت به سمت رویکرد رویداد محور و ریزخدمات برای دستیابی به استقرار مستقل، محیط پشته فناوری ناهمگن، و معماری کمتر جفت شده را در نظر بگیرید (و در عین حال به دلیل ماهیت رویکردهای رویداد محور و pub-sub اگر اینها پذیرفته می شوند). سادگی و کارایی به هم نزدیک هستند و تاثیر زیادی روی یکدیگر دارند. معمولاً معماری‌های پیچیده بر سرعت توسعه ویژگی‌های جدید تأثیر می‌گذارند، از ویژگی‌های موجود پشتیبانی و حفظ می‌کنند و تکامل طبیعی سیستم را به چالش می‌کشند.


با این حال، سیستم های پیچیده اغلب به معماری پیچیده و جامع نیاز دارند که اجتناب ناپذیر است.


انصافاً، این یک موضوع بسیار گسترده است، و ایده های بسیار خوبی در مورد چگونگی ساختار و ساختن سیستم هایی برای تکامل طبیعی وجود دارد. بر اساس تجربیاتم، روش زیر را به کار بردم:

  1. تقریباً همیشه با سبک معماری یکپارچه شروع می شود زیرا بسیاری از مشکلات ناشی از ماهیت سیستم های توزیع شده را از بین می برد. همچنین منطقی است که از یکپارچگی مدولار برای تمرکز بر اجزای ساختمان با مرزهای واضح پیروی کنیم. استفاده از یک رویکرد مبتنی بر مؤلفه می تواند به آنها کمک کند تا با استفاده از رویدادها با یکدیگر ارتباط برقرار کنند، اما داشتن تماس های مستقیم (معروف به RPC) در ابتدا کار را ساده می کند. با این حال، ردیابی وابستگی‌های بین مؤلفه‌ها مهم است، زیرا اگر مؤلفه A اطلاعات زیادی در مورد مؤلفه B داشته باشد، شاید منطقی باشد که آنها را در یک ادغام کنیم.
  2. هنگامی که به موقعیتی نزدیک می شوید که نیاز به مقیاس سازی توسعه و سیستم خود دارید، می توانید از الگوی Stangler پیروی کنید تا به تدریج اجزایی را استخراج کنید که نیاز به استقرار مستقل یا حتی مقیاس بندی با الزامات خاص دارند.
  3. حال، اگر چشم انداز روشنی از آینده دارید، که کمی شانس باورنکردنی است، می توانید در مورد معماری مورد نظر تصمیم بگیرید. در این لحظه، می‌توانید با استفاده از رویکردهای ارکستراسیون و رقص، ترکیب الگوی CQRS برای عملیات نوشتن و خواندن در مقیاس مستقل، یا حتی تصمیم بگیرید که معماری یکپارچه را در صورت مطابقت با نیازهای شما، به سمت معماری میکروسرویس حرکت کنید.


همچنین درک اعداد و معیارهایی مانند DAU (کاربران فعال روزانه)، MAU (کاربران فعال ماهانه)، RPC (درخواست در هر ثانیه) و TPC (تراکنش در هر ثانیه) حیاتی است، زیرا می تواند به شما در انتخاب کمک کند زیرا معماری برای 100 کاربر فعال و 100 میلیون کاربر فعال متفاوت هستند.


به عنوان نکته پایانی می گویم که معماری تاثیر بسزایی در موفقیت محصول دارد. معماری ضعیف طراحی شده برای محصولات در مقیاس‌بندی مورد نیاز است، که به احتمال زیاد منجر به شکست می‌شود، زیرا مشتریان منتظر نمی‌مانند تا زمانی که سیستم را مقیاس‌بندی می‌کنید، آنها یک رقیب را انتخاب می‌کنند، بنابراین ما باید از مقیاس‌بندی بالقوه جلوتر باشیم. اگرچه من اعتراف می کنم که گاهی اوقات نمی تواند یک رویکرد ناب باشد، ایده این است که یک سیستم مقیاس پذیر داشته باشیم اما قبلاً مقیاس نشده باشد. از سوی دیگر، داشتن یک سیستم بسیار پیچیده و در حال حاضر مقیاس‌پذیر بدون مشتری یا برنامه‌ای برای به دست آوردن بسیاری از آنها، هزینه‌ای برای کسب‌وکارتان ندارد.

انتخاب پشته فناوری

انتخاب یک پشته فناوری نیز یک تصمیم در سطح کلان است زیرا بر استخدام، دیدگاه‌های تکامل طبیعی سیستم، مقیاس‌پذیری و عملکرد سیستم تأثیر می‌گذارد.


این لیستی از ملاحظات اساسی برای انتخاب یک پشته فناوری است:

  • الزامات و پیچیدگی پروژه به عنوان مثال، اگر توسعه دهندگان شما تجربه ای با آن داشته باشند، می توان یک برنامه وب ساده با فریم ورک Blazor ساخت، اما به دلیل عدم بلوغ WebAssembly، انتخاب React و Typescript برای موفقیت طولانی مدت می تواند تصمیم بهتری باشد.
  • مقیاس پذیری و نیازهای عملکردی اگر پیش‌بینی می‌کنید حجم زیادی از ترافیک دریافت کنید، انتخاب ASP.NET Core به جای جنگو می‌تواند انتخاب عاقلانه‌ای به دلیل عملکرد برتر آن در رسیدگی به درخواست‌های همزمان باشد. با این حال، این تصمیم به مقیاس ترافیکی که انتظار دارید بستگی دارد. اگر نیاز به مدیریت بالقوه میلیاردها درخواست با تأخیر کم دارید، حضور Garbage Collection می‌تواند یک چالش باشد.
  • استخدام، زمان توسعه و هزینه. در بیشتر موارد، اینها عواملی هستند که باید به آنها اهمیت دهیم. زمان برای بازار، هزینه تعمیر و نگهداری و ثبات استخدام نیازهای کسب و کار شما را بدون هیچ مانعی هدایت می کند.
  • تخصص و منابع تیم مجموعه مهارت های تیم توسعه شما یک عامل حیاتی است. به طور کلی استفاده از فن آوری هایی که تیم شما قبلاً با آنها آشنا هستند مؤثرتر است، مگر اینکه دلیل محکمی برای سرمایه گذاری در یادگیری یک پشته جدید وجود داشته باشد.
  • بلوغ. یک جامعه قوی و یک اکوسیستم غنی از کتابخانه ها و ابزارها می تواند روند توسعه را تا حد زیادی تسهیل کند. فناوری‌های رایج اغلب از حمایت جامعه بهتری برخوردارند، که می‌تواند برای حل مشکلات و یافتن منابع ارزشمند باشد. بنابراین، می توانید منابع را ذخیره کنید و عمدتاً روی محصول تمرکز کنید.
  • نگهداری و پشتیبانی طولانی مدت. دوام طولانی مدت این فناوری را در نظر بگیرید. فناوری‌هایی که به‌طور گسترده مورد پذیرش و پشتیبانی قرار می‌گیرند، کمتر منسوخ می‌شوند و معمولاً به‌روزرسانی‌ها و بهبودها را دریافت می‌کنند.


چگونه داشتن چندین پشته فناوری می تواند بر رشد کسب و کار تأثیر بگذارد؟

از یک منظر، معرفی یک پشته بیشتر می‌تواند استخدام شما را افزایش دهد، اما از سوی دیگر، هزینه‌های نگهداری اضافی را به همراه دارد زیرا باید از هر دو پشته پشتیبانی کنید. بنابراین، همانطور که قبلاً گفتم، از دیدگاه من، تنها نیاز اضافی باید استدلالی برای ترکیب پشته های فناوری بیشتر باشد.


اما اصل انتخاب بهترین ابزار برای یک مشکل خاص چیست؟

گاهی اوقات چاره ای جز آوردن ابزارهای جدید برای حل یک مشکل خاص بر اساس همان ملاحظات ذکر شده ندارید، در چنین مواقعی انتخاب بهترین راه حل منطقی است.


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


ملاحظات مهم دیگر مربوط به وابستگی های نرم افزاری منبع باز و اختصاصی است. نرم افزار اختصاصی به شما انعطاف کمتری می دهد و امکان سفارشی شدن را به شما می دهد. با این حال، خطرناک ترین عامل قفل شدن فروشنده است، جایی که شما به محصولات، قیمت ها، شرایط و نقشه راه یک فروشنده وابسته می شوید. اگر فروشنده جهت خود را تغییر دهد، قیمت ها را افزایش دهد یا محصول را متوقف کند، می تواند خطرناک باشد. نرم افزار منبع باز این خطر را کاهش می دهد، زیرا یک نهاد واحد آن را کنترل نمی کند. حذف یک نقطه شکست در همه سطوح، کلید ایجاد سیستم های قابل اعتماد برای رشد است.

نقطه شکست (SPOF)

یک نقطه شکست واحد (SPOF) به هر بخشی از یک سیستم اطلاق می‌شود که در صورت از کار افتادن، باعث توقف عملکرد کل سیستم می‌شود. حذف SPOF ها در همه سطوح برای هر سیستمی که نیاز به دسترسی بالا دارد، بسیار مهم است. همه چیز، از جمله دانش، پرسنل، اجزای سیستم، ارائه دهندگان ابر و کابل های اینترنت، ممکن است با شکست مواجه شوند.


چندین تکنیک اساسی وجود دارد که می‌توانیم برای حذف نقاط شکست به کار ببریم:

  1. افزونگی. پیاده سازی افزونگی برای اجزای حیاتی. این به معنای داشتن مولفه‌های پشتیبان است که می‌توانند در صورت خرابی مؤلفه اصلی، مسئولیت را بر عهده بگیرند. افزونگی می‌تواند در لایه‌های مختلف سیستم، از جمله سخت‌افزار (سرورها، دیسک‌ها)، شبکه (پیوندها، سوئیچ‌ها) و نرم‌افزار (پایگاه‌های داده، سرورهای برنامه) اعمال شود. اگر همه چیز را در یک Cloud Provider میزبانی می‌کنید و حتی پشتیبان‌گیری در آنجا دارید، به ایجاد یک نسخه پشتیبان اضافی معمولی در دیگری فکر کنید تا در صورت بروز فاجعه هزینه از دست رفته خود را کاهش دهید.
  2. مراکز داده سیستم خود را در چندین مکان فیزیکی مانند مراکز داده یا مناطق ابری توزیع کنید. این رویکرد سیستم شما را در برابر خرابی های خاص مکان مانند قطع برق یا بلایای طبیعی محافظت می کند.
  3. Failover. برای همه اجزای خود (DNS، CDN، Load balancers، Kubernetes، API Gateways، و Database) یک رویکرد Failover اعمال کنید. از آنجایی که مشکلات ممکن است به طور غیرمنتظره ایجاد شوند، داشتن یک برنامه پشتیبان برای جایگزینی سریع هر جزء با کلون آن در صورت لزوم بسیار مهم است.
  4. خدمات با در دسترس بودن بالا با رعایت اصول زیر اطمینان حاصل کنید که خدمات شما از همان ابتدا به صورت افقی مقیاس پذیر و در دسترس هستند:
    • عدم وضعیت سرویس را تمرین کنید و از ذخیره جلسات کاربر در کش های حافظه خودداری کنید. در عوض، از یک سیستم کش توزیع شده مانند Redis استفاده کنید.
    • هنگام توسعه منطق از اتکا به ترتیب زمانی مصرف پیام خودداری کنید.
    • برای جلوگیری از ایجاد اختلال در مصرف کنندگان API، تغییرات شکسته را به حداقل برسانید. در صورت امکان، تغییرات سازگار با عقب را انتخاب کنید. همچنین هزینه را در نظر بگیرید زیرا گاهی اوقات، اجرای یک تغییر قطعی ممکن است مقرون به صرفه تر باشد.
    • اجرای مهاجرت را در خط لوله استقرار ادغام کنید.
    • یک استراتژی برای رسیدگی به درخواست های همزمان ایجاد کنید.
    • برای افزایش قابلیت اطمینان و مشاهده پذیری، کشف، نظارت و ثبت خدمات را پیاده سازی کنید.
    • منطق کسب و کار را توسعه دهید تا ناتوان باشید، و تصدیق کنید که شکست شبکه اجتناب ناپذیر است.
  5. بررسی وابستگی به طور منظم وابستگی های خارجی را بررسی و به حداقل برسانید. هر وابستگی خارجی می تواند SPOF های بالقوه را معرفی کند، بنابراین درک و کاهش این خطرات ضروری است.
  6. اشتراک دانش منظم هرگز اهمیت گسترش دانش در سازمان خود را فراموش نکنید. افراد می توانند غیرقابل پیش بینی باشند و تکیه بر یک فرد مخاطره آمیز است. اعضای تیم را تشویق کنید تا دانش خود را از طریق مستندسازی دیجیتالی کنند. با این حال، مراقب اسناد بیش از حد باشید. از ابزارهای مختلف هوش مصنوعی برای ساده کردن این فرآیند استفاده کنید.

نتیجه گیری

در این مقاله به چندین جنبه کلیدی ماکرو و نحوه برخورد با پیچیدگی آنها پرداختیم.


با تشکر از شما برای خواندن! دفعه بعد می بینمت!

L O A D I N G
. . . comments & more!

About Author

Aleksei HackerNoon profile picture
Aleksei@fairday
Hey, I am Alex, a dedicated Software Development Engineer with experience in the .NET environment and architecture

برچسب ها را آویزان کنید

این مقاله در ارائه شده است...