serviceCollection.AddScoped<HttpClient>();
@inject HttpClient Http
serviceCollection.AddScoped<HttpClient>();
@inject HttpClient Http
ابتدا از طریق فرمت protocol buffer، فایلهای خود را که قرار است انتقال داده شوند، مینویسیم.
سپس بصورت خودکار برای زبان برنامه نویسی مطبوع خود آن را generate میکنیم.
کدهای تولید شده بصورت خودکار و کاملا آماده هستند و ضمن اینکه encode/decode شدن بصورت خودکار توسط فریم ورک انجام شده و قابلیت تعامل بین زبانهای مختلف برنامه نویسی یا سرویسهای مختلف برقرار است.
نکته:
نصب Code generator
برای اینکه بتوانیم از طریق فایلهایی که میسازیم کدهای generate شده را تولید کنیم، احتیاج به کامپایلر مربوطه را داریم.
اگر از MacOSX استفاده میکنید، به راحتی با استفاده از دستور زیر میتوانید آن را نصب کنید:
brew install protobuf
اگر هم از ویندوز استفاده میکنید، از این طریق میتوانید نسخهی مورد نظر را به راحتی دانلود و مورد استفاده قرار بدهید:
https://github.com/google/protobuf/releases https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-win32.zip
حالا میخواهیم اولین فایل خود را با این فرمت بسازیم.
اول از همه با هم نگاهی به ساختار فایل مربوطه میاندازیم:
همانطور که در تصویر فوق میبینید، همه چیز به سادگی مشخص است؛ ورژن 3 که آخرین ورژن پروتکل بافر میباشد، آیتمی به نام MyMessage با پراپرتیهایی مشخص شده از Type بخصوص، تعریف شدهاند، تگها هم باید به ترتیب وارد شده باشند.
حالا میخواهیم بصورت واقعی protocol buffer خود را طراحی کرده و سپس از روی آن کدهای مربوطه را generate نماییم؛ به نام sample.proto بصورت زیر:
syntax = "proto3"; package helloworld; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
در فایل فوق علاوه بر تعریفهای اولیه، یک سرویس را هم اضافه کردهایم و همچنین متدی را با ورودی و خروجیهای مشخصی ایجاد کردهایم (امکانات پروتکل بافر خیلی بیشتر از این موارد است؛ از جمله فرمتهای آرایه و غیره را نیز پشتیبانی میکند، همچنین از روشی برای versioning استفاده میکند که obsolete کردن پراپرتیها و نسخه بندی را بسیار راحت میکند و ...). به سادگی قابلیت طراحی و پیاده سازی سرور و کلاینت مربوط به این آیتم ایجاد شده با استفاده از زبانهای برنامه نویسی مختلف فراهم میباشد. حال کافیاست که پروتکل بافر خود را با زبان دلخواه خود generate کنیم. در قسمت زیر برای زبانهای برنامه نویسی Go و #C، کدها را تولید میکنیم.
protoc sample.proto --go_out=plugins=grpc:.
protoc sample.proto --csharp_out=.
بعد از تولید شدن کدها با استفاده از زبان برنامه نویسی دلخواه خود میتوانید مشاهد کنید سرویس ها، تایپها و غیره همگی ساخته شدهاند و کاملا آمادهی استفاده هستند.
در مقالهی بعدی به آشنایی با gRPC میپردازیم و ضمن اینکه یک سرور با #C و یک کلاینت با زبان برنامه نویسی Go را نوشته که از طریق پروتکل بافر با هم به تبادل اطلاعات میپردازند!
میدانیم که غیرمستقیم کردن دسترسی به منابع در طراحی نرمافزار یک اصل است. اما Abstraction تنها راه جداسازی نیست. راه دیگر طراحی زیرساخت میباشد. مانند استفاده از Message Queue (مانند استفاده از MediatR) و یا Load Balancer.
البته منظور این نیست که استفادهی غیر مستقیم از لایههای نمایش داده شده را حذف کنیم. مشخصا MediatR در اینجا جداسازی خوبی را برای ما ایجاد کرده که مزایای آن بر کسی پوشیده نیست. مسئلهی مهمی که معمولا به آن توجهی نمیشود، هزینهی Abstraction میباشد. اگر بخواهیم دربارهی هزینههای Abstraction صحبت کنیم، اشاره به موارد زیر قابل تامل است:
ابتدا، فهم سخت بعد از استفاده از Abstraction میباشد. در واقع ما با ایجاد این Abstractionها، مسیر رسیدن به هدف را در بین یکسری لایهها قرار دادهایم که فهم دقیق آنها، ارتباط مستقیمی با منطق پیادهسازی ما دارد. البته مزیت اینکار عدم درگیری با لایههای مشخص شده میباشد.
دوم کارآیی پایین. البته فقط صحبت در مورد مصرف زیاد حافظه و یا CPU نیست. یکی از این موارد، به دلیل Generic تعریف شدن Repositoryها میباشد که باعث میشود دادههای بیشتر از نیازی را به لایههای دیگر ارسال کنیم. برای بررسی دقیقتر میتوانید این مقاله را مطالعه بفرمایید.
اما Vertical Slices چگونه به ما در این زمینه کمک میکند. در این معماری، ما به جای تمرکز بر روی لایهها، تمرکز خود را روی فیچرها میگذاریم. در واقع نرمافزار را به قسمتهای بسیار کوچکتری تقسیم میکنیم و از Abstraction اضافی جلوگیری میکنیم. در این صورت اگر تغییری لازم به اعمال شدن دارد، در سطح فیچر اعمال میشود و نه در سطح لایهها. در ضمن دیگر Repository و یا Specification ی برای تست وجود ندارد؛ پس میزان تستهای نوشته شده کاهش پیدا میکنند و طبیعتا برای تست Integration میتوانیم کل فیچر را تست کنیم.
در اینجا ما بجای تمرکز بر روی ساختار کل کدبیس، تمرکز خود را بر روی ساختار کدبیس یک فیچر خاص نگه میداریم.
در Vertical Slices Arch ما مشکلی با اشتراکگذاری Domain بین فیچرها نداریم. زیرا Domain ما به هیچ فریموورکی وابسته نیست و بصورت مستقیم مورد استفاده قرار میگیرد. لطفا به تصویر زیر توجه بفرمایید:
زمانیکه این فیچرهای کوچک توسعه داده میشوند، دلیلی برای تعریف Repository و یا Specificationهای بی مورد نداریم. پس میتوانیم به راحتی آنها را حذف کنیم و به صورت مستقیم از EF و یا هر ORM دیگری استفاده کنیم و یا میتوانیم به راحتی از Raw Queryها و مزیت آنها بهرهمند شویم. نگهداری یک فیچر کوچک که Command، Handler و Viewها و سایر نیازمندیهایش(به استثنای Domain) داخل خودش قرار گرفته و شامل یک ساختار ویژهی خودش با توجه به نیازمندیهای تعریف شده میباشد، بسیار کار سادهتری است تا نگهداری یک سری لایه که به صورت گسترده از Abstraction در آنها استفاده شدهاست.
هدف | سرویسهای اضافه شده | متد تنظیم سرویسها |
- مناسب برای توسعهی Web API - اما باید بخاطر داشت که این سرویسها را به صورت پیشفرض اضافه نمیکند:
|
| ()AddControllers |
شبیه به ASP.NET Core 1.X عمل میکند؛ یعنی سرویس Pages را که مرتبط با Razor Pages است، به صورت پیشفرض ثبت نمیکند. |
| ()AddControllersWithViews |
برای کار با Razor pages بوده و این سرویسها را به صورت پیشفرض به همراه ندارد:
|
| ()AddRazorPages |
OData چهار قسمت اصلی دارد:
class Painter { private readonly float daysPerHouse; public Painter(float daysPerHouse) { this.daysPerHouse = daysPerHouse; } public float EstimateDaysToPaint(int houses) { return houses * daysPerHouse; } }
class LandOwner { private readonly Painter painter; private readonly int housesCount; public LandOwner(Painter painter, int housesCount) { this.painter = painter; this.housesCount = housesCount; } public void ManageHouses() { float daysToPaint = this.painter.EstimateDaysToPaint(this.housesCount); Console.WriteLine("Painting houses for {0:0.0} day(s).", daysToPaint); } }
class LandOwner { private readonly IEnumerable<Painter> painters; private readonly int housesCount; public LandOwner(IEnumerable<Painter> painters, int housesCount) { this.painters = new List<Painter>(painters); this.housesCount = housesCount; } ... }
اکنون مالک زمین مسئولیت انجام این محاسبه را برعهده گرفته است؛ ولی این پیاده سازی کمی پیچیدهتر میشود:
class LandOwner { private readonly IEnumerable<Painter> painters; private readonly int housesCount; public LandOwner(IEnumerable<Painter> painters, int housesCount) { this.painters = new List<Painter>(painters); this.housesCount = housesCount; } private float GetVelocity(Painter painter) { return painter.EstimateDaysToPaint(1); } private float GetTotalVelocity() { float sum = 0; foreach (Painter painter in this.painters) sum += 1 this.GetVelocity(painter); return sum; } public void ManageHouses() { float daysToPaint = this.GetTotalVelocity() * this.housesCount; Console.WriteLine("Painting houses for {0:0.0} day(s).", daysToPaint); } }
این پیاده سازی کمی پیچیدهاست؛ اما کار میکند و همچنین دارای مشکلاتی است. فرض کنید یکی از نقاشان صاحب شرکت، نقاشی است که نقاشان دیگر را استخدام میکند. حتی بدتر از آن این است که اگر شرکت نقاشی، شرکت دیگری را نیز همراه با نقاشان خود، استخدام کند. مالک زمین با سلسله مراتبی چندسطحی از نقاشان روبرو میشود. برآورد زمان لازم برای نقاشی خانهها در چنین شرایطی برای مالک زمین دشوار خواهد شد.
پیاده سازی Composite
اگر تنها بتوانیم یک اینترفیس عمومی را از یک نقاش، بیرون بکشیم، سازماندهی نقاشها راحتتر میشود:
interface IPainter { float EstimateDaysToPaint(int houses); }
مالک زمین دیگر کاری با مجموعه نقاشها ندارد و در حال حاضر تنها یک نقاش انتزاعی را کنترل میکند:
class LandOwner { private readonly IPainter painter; private readonly int housesCount; public LandOwner(IPainter painter, int housesCount) { this.painter = painter; this.housesCount = housesCount; } public void ManageHouses() { float daysToPaint = this.painter.EstimateDaysToPaint(this.housesCount); Console.WriteLine("Painting houses for {0:0.0} day(s).", daysToPaint); } }
اینبار مالک زمین فقط ارجاعی را به یک نقاش انتزاعی دارد. از سوی دیگر، کلاس نقاش دست نخورده باقی میماند و تنها رابط IPainter را پیاده سازی میکند:
class Painter: IPainter { ... }
حالا میتوانیم نتیجه آن را ببینیم. ما آماده تعریف یک عنصر Composite هستیم که خود و عناصرش، اینترفیس IPainter را پیاده سازی کردهاند.
class PaintingCompany: IPainter { private readonly IEnumerable<IPainter> painters; public PaintingCompany(IEnumerable<IPainter> painters) { this.painters = new List<IPainter>(painters); } private float GetVelocity(Painter painter) { return painter.EstimateDaysToPaint(1); } private float GetTotalVelocity() { float sum = 0; foreach (Painter painter in this.painters) sum += 1 this.GetVelocity(painter); return sum; } public float EstimateDaysToPaint(int houses) { return this.GetTotalVelocity() * houses; } }
این پیاده سازی شرکت نقاشی است. کد کلاس LandOwner قبلی که وظیفه آن کنترل نقاشها بود، به این کلاس منتقل شدهاست. تفاوت این است که شرکت نقاشی اکنون تعدادی نقاش انتزاعی را مدیریت میکند. از انتزاعات میتوان دو حالت را در نظر گرفت: به صورت تک و یا به صورت گروه. این مورد قدرت نوع انتزاعی است در برنامه نویسی شیء گرا که در اینجا خودش را به صورت یک نقاش و یا گروهی از افراد که با هم کار میکنند، نشان میدهد.
نتیجه گیری
در این مقاله ما به یک نمونه از الگوی طراحی Composite پرداختیم. با استفاده از الگوی Composite، شیوهای که کلاسها با مجموعهها برخورد میکنند، بسیار سادهتر شدهاست. کار با مجموعهها، کد را پیچیدهتر کرده و باعث میشود کلاس، کاری بیشتر از مسئولیتهای خود را انجام دهد که ربطی به آن ندارد.