این پست اولین بخش از سری پستهای «طریقت یکFPزامبی در طراحی نرمافزار» است.در این سری از پستها من تلاش میکنم که به سادهترین شکل ممکن اصولدرست طراحی نرمافزار را با استفاده از قدرتfunctional programmingوtype-driven designبه شما آموزش بدم.
- تایپهای ناشایسته
- کوری boolean و شواهد عمومی
- نگهبان در خروجی
- تایپهایی که روح دارند
- شواهد اختصاصی
چند نکته من باب این سری از پستها:
- تنها ابزار مورد نیاز جهت پیادهسازی این اصول یک زبانstatic typeاست.(ترجیحا از خانواده ML)
- اصول عنوان شدهframework-agnosticهستند.
- علیرغم این حقیقت که این اصول ازfunctional programmingنشات میگیرنددرobject-oriented programmingهم قابلیت اجرا دارند.(تا حدودی!)
تایپ واقعا چیه؟!کاری به تعاریف رسمی ندارم.من معتقدم که مفهوم تایپ از بدو وجود با ذهن ما آمیخته شدهو اساس و پایه تفکر ما را تشکیل میده.من تایپ را یکدستهاز موجودیتهایی تعریف میکنم که بینشان حداقل یک صفت مشترک آنها را به هم مرتبط میکنه.
مِن باب مثال،تایپSquareرا در نظر بگیرید که صفت مشترک بین اعضای این تایپ مربع بودن است.
1dataSquare=ShortSquare|MediumSquare|LongSquare
همانطور که میبینید من از کلمهدستهجهت تعریف تایپ استفاده کردم.هر دسته(set)میتونه از 0 الی∞عضو(inhabitant)تشکیل شده باشه.تایپSquareهمانطور که میبینید سهinhabitantداره.تایپ اعداد طبیعی(ℕ)هم میتونه یک مثال از تایپی با بینهایتinhabitantباشه:
به نظرتان صفت مشترک بینinhabitantهای تایپℕچه چیزی میتونه باشه؟
شایستگی تایپ!
حالا که با مفهومtypeوinhabitantآشنا شدیم، میتونم موضوع اصلی این پست را عنوان کنم.
زمانی که یک تایپinhabitantهای بیشتری نسبت به منطق تحت توصیف شما را همراهخودش حمل کند، آن تایپ برای آن جایگاه ناشایسته است.
فرض کنید برنامهای دارید کهمتشکل از تعدادی کاربر و پروژه است و رابطه بینکاربر و پروژه به صورتn-nاست.هر کاربر ممکنه منتسب به 0 تا∞پروژه باشه و بالعکس.من از شما میخوامfunctionای تعریف کنید که یکUserرا به عنوان ورودی میگیره و تعداد پروژههای منتسب به آن کاربر را برمیگردونه.چه تایپی را برای خروجی اینfunctionانتخاب میکنید؟
1countProjectsRelatedToUser::User->?
فرض کنید تایپIntرا انتخاب کنیم که متشکل از تمام اعداد صحیح میشه.
آیا میتونیم از این تایپ استفاده کنیم؟البته که میتونیم!ما به تایپی نیاز داریم که از اعداد0تا∞+بخشی از دامنه آن تایپ باشه و تایپIntتمام این اعداد را در دامنه خودش داره.اما آیا هیچوقت فانکشنcountProjectsRelatedToUserیک عدد منفی را برمیگردونه؟فرض کنید که بر اساس خروجی اینfunctionما قصد داریم که یک عملیاتی انجام بدیم:
1countProjectsRelatedToUser::User->Int
2
3f::Int->String
4f0="You havn't any project."
5f1="You have one project."
6fn|n>0="You have multiple projects."
7|n<0=error"UNREACHABLE"
ما میدونیم که شاخهیn < 0با توجه به منطقی که تعریف کردیم ممکن نیست که رخ بده.وجودUnreachable branch exceptionنشانه خوبی نیست!
اگر بهجای تایپIntاز تایپWholeاستفاده کنیم چطور؟
1dataWhole=0|1|...
2
3countProjectsRelatedToUser::User->Whole
4
5f::Whole->String
6f0="You havn't any project."
7f1="You have one project."
8f_="You have multiple projects."
میبینید که تایپWholeکاملا مطابق تعریف ما از خروجیcountProjectsRelatedToUserتعریف شده.نه یکinhabitantبیشتر و نه یکinhabitantکمتر!همینطور میبینید که دیگه نیازی بهUnreachable branch exceptionنیست.
لذا با توجه به منطقی که تعریف کردیم تایپIntبه عنوان خروجیcountProjectsRelatedToUserیک تایپ ناشایسته به حساب میاد و استفاده از یک تایپ ناشایستههمانطور که دیدید، میتونه مشکلات جدیای در طراحی نرمافزار ما ایجاد کنه!
چطور تایپهای شایسته تعریف کنیم؟
حالا که احاطه خوبی نسبت به شایستگی یک تایپ دارید، باید این سوال برای شما مطرح بشهکه چطور تایپهای شایسته با توجه بهbusiness logicخودمان تعریف کنیم؟!
بعضی از تایپها ممکنه که توسط زبانی که با آن کار میکنید تعریف شده باشند،اما قطعا خیلی از تایپهایی که متناسب با نیازهای شما باشند را بایدخودتان تعریف کنید.برای درک این موضوع با مثال پیش خواهیم رفت.هر مثال با دو زبانHaskellوTypeScriptپیادهسازی خواهند شد.
سناریو ۱ - Non-empty List
در نظر بگیرید در برنامهای قصد دارید تایپUserرا به شکلی مدل کنید که همواره هر کاربر حداقل باید یکE-Mailدر سیستم داشته باشه.
1dataUser=User{emails::?Email}
برای پیادهسازی منطقnon-emptyکافیه مطمئن بشید که همواره اولین مقدار داخلListوجود داره.
1namespaceArray{
2exportclassNonEmpty<a>{
3constructor(private _first: a,private _rest:Array<a>){}
4}
5}
1typeUser={
2 emails:Array.NonEmpty<Email>
3}
4
5declareconst email1:Email
6declareconst email2:Email
7
8const user:User={
9 emails:newArray.NonEmpty(email1,[email2]),
10}
درHaskellهم به شکل مشابهای میتونید این کار را بکنید.کافیه که یکproduct typeاز اولین و مابقی مقادیر تشکیل بدید:
1moduleData.List(NonEmpty(..))where
2
3dataNonEmptya=a `NonEmpty`[a]
1moduleUserwhere
2
3importqualified Data.Listas List
4
5dataUser=User{emails::List.NonEmptyEmail}
6
7email1::Email
8email2::Email
9
10user=User{emails=email1 `NonEmpty`[email2]}
سناریو ۲ - E-Mail
اگر بخواهید یک تایپ برایE-Mailانتخاب کنید، یحتمل تا قبل از مطالعه این پستStringرا انتخاب میکردید.اما همانطور که خودتان دیگه میتونید حدس بزنید تایپStringشایستگی جالبی برای بیانE-Mailندارد.
1dataString="a"|"b"|"the_dr_lazt@pm.me"|...
در واقع تایپStringبینهایتinhabitantدارد که اصلا در دامنهE-Mailنیستند.خب قطعا باید تایپ جدیدی وارد سیستم کنید.اما دیگه مثل سناریو اول انقدر این موضوع ساده نیست که با ساختن یکproduct typeبتونید این تایپ را بیان کنید.
برای بیان چنین تایپی باید یکwrapperبرای تایپStringدرست کنیم و تنها راه ساخته شدن تایپ جدید را محدود به مسیرvalidateشده کنید.
1declareconstisValidEmail:(value:string)=>boolean
2
3classEmail{
4privateconstructor(private _value:string){}
5
6// smart constructor
7publicstaticmk(value:string):Maybe<Email>{
8if(!isValidEmail(value))returnMaybe.nothing
9
10returnMaybe.just(newEmail(value))
11}
12}
همانطور که میبینید باprivateکردنconstructorجلوی ساخته شدن تایپEmailاز خارج کلاسEmailگرفته شده.تنها راه ساخت تایپEmailبا استفاده از فانکشنmkاست که همواره از نظرvalidبودن، ورودی را بررسی میکنه و صرفا زمانی تایپEmailرا برمیگردونه که حتما ورودیvalidباشه.همانطور که میبینید تایپEmailصرفا یکwrapperبرای تایپStringبه حساب میاد.
اما پیادهسازی این تایپ درHaskellبسیار جالبتر میشه.
1moduleEmail(Email,mk)where
2
3dataEmail=EmailString
4
5mk::String->Email
6mk=undefined
در اینجا هم در خط اول ما جلویexportشدنdata constructorبرایEmailرا گرفتیم.این پیادهسازی با مثالOOPتوفیقی نداره.اما ما درHaskellمجبور نیستیم برای معرفی کردن تایپ جدید به کامپایلر یکobjectبسازیم!
1newtypeEmail=EmailString
با استفاده ازnewtypeدرHaskellما تایپ جدیدی به نامEmailمیسازیم ولی در زمانruntimeمقدار تایپEmailلیترالی هیچ توفیقی باStringنداره.به عبارتیmemory representationدو مقدار زیر با هم کاملا یکسان هستند:
1x=Email"the_dr_lazy@pm.me"
2y="the_dr_lazy@pm.me"
ختم کلام
مثالهای متعددی وجود دارند که شما میتونید با توجه بهbusiness logicتان از این اصل استفاده کنید.امیدوارم که لذت برده باشید و این پست دانش کاربردی به شما منتقل کرده باشه.اگر سوالی دارید، مثل همیشه میتونید با من از طریقTwitterدر ارتباط باشید.
همچنین امیدوارم که هرچه زودتر قسمتهای بعدی این سری از پستها رابتونم بنویسم.با حمایتتان هم میتونید من را از کاربردی بودنه این سری از پستها آگاه کنید.