حالت تاریک
فصل ۱۶ — ساخت پشتهها از کامپوننتها
فصل ۱۵ توضیح داد چگونه کامپوننتهای خوب طراحیشده میتوانند تغییر سیستم زیرساخت را آسانتر و ایمنتر کنند. این پیام از تم کتاب پشتیبانی میکند: استفاده از سرعت تغییر برای بهبود مداوم کیفیت سیستم، و استفاده از کیفیت بالا برای امکانپذیر کردن تغییر سریعتر.
این فصل روی ماژولار کردن پشتههای زیرساخت تمرکز دارد؛ یعنی شکستن پشتهها به قطعات کوچکتر کد. چند دلیل برای در نظر گرفتن ماژولار کردن پشته وجود دارد:
بازاستفاده (Reuse) دانش نحوه پیادهسازی یک سازه خاص را در یک کامپوننت قرار دهید تا بتوانید آن را در پشتههای مختلف بازاستفاده کنید.
ترکیبپذیری (Composition) توانایی جابهجایی پیادهسازیهای مختلف یک مفهوم را ایجاد کنید تا در ساخت پشتهها انعطاف داشته باشید.
قابلیت تست (Testability) با شکستن پشته به قطعاتی که میتوانند جداگانه قبل از یکپارچهسازی تست شوند، سرعت و تمرکز تست را بهبود دهید. اگر کامپوننت ترکیبپذیر باشد، میتوانید آنها را با test doubleها («استفاده از Test Fixtureها برای مدیریت وابستگیها» در صفحه ۱۳۷) جایگزین کنید تا ایزولاسیون و سرعت تست بیشتر شود.
اشتراکگذاری (Sharing) کامپوننتهای ترکیبپذیر، قابل بازاستفاده و تستشده را بین تیمها به اشتراک بگذارید تا مردم بتوانند سیستمهای بهتری سریعتر بسازند.
همانطور که در «کامپوننتهای پشته در برابر پشتهها بهعنوان کامپوننت» در صفحه ۲۵۷ ذکر شد، شکستن پشته به ماژولها و کتابخانهها کد را ساده میکند، اما نمونههای پشته را کوچکتر یا سادهتر نمیکند. کامپوننتهای پشته میتوانند با پنهان کردن تعداد و پیچیدگی منابع زیرساختی که به نمونه پشته شما اضافه میکنند، اوضاع را بدتر کنند.
پس باید مطمئن شوید آنچه زیر abstractionها، کتابخانهها و پلتفرمهایی که استفاده میکنید قرار دارد را درک میکنید. اینها راحتی هستند که میتوانند به شما اجازه دهند روی وظایف سطح بالاتر تمرکز کنید. اما نباید جایگزین درک کامل نحوه پیادهسازی سیستم شما باشند.
زبانهای زیرساخت برای کامپوننتهای پشته
فصل ۴ انواع مختلف زبانهای کد زیرساخت را توصیف میکند. دو سبک اصلی زبان برای تعریف پشتهها declarative (ببینید «زبانهای declarative زیرساخت» در صفحه ۴۱) و imperative (ببینید «زبانهای imperative زیرساخت قابل برنامهنویسی» در صفحه ۴۳) هستند. آن فصل اشاره میکند که این انواع مختلف زبان برای انواع مختلف کد مناسباند (ببینید «زبانهای declarative در برابر imperative برای زیرساخت» در صفحه ۴۴).
این تفاوتها اغلب با کامپوننتهای پشته برخورد میکنند، وقتی مردم آنها را با نوع اشتباه زبان مینویسند. استفاده از نوع اشتباه زبان معمولاً به مخلوطی از هر دو زبان declarative و imperative منجر میشود که، همانطور که قبلاً توضیح داده شد، چیز بدی است (ببینید «جداسازی کد declarative و imperative» در صفحه ۴۶).
تصمیم اینکه کدام زبان استفاده شود معمولاً توسط ابزار مدیریت پشته زیرساختی که استفاده میکنید و زبانهایی که پشتیبانی میکند هدایت میشود.¹
الگوهایی که بعداً در این فصل تعریف میشوند باید شما را تشویق کنند به آنچه میخواهید با پشته و کامپوننتهایش به دست آورید فکر کنید. برای استفاده از این برای در نظر گرفتن نوع زبان، و احتمالاً نوع ابزار پشته، که باید استفاده کنید، دو کلاس کامپوننت پشته بر اساس نوع زبان را در نظر بگیرید.
بازاستفاده از کد declarative با ماژولها
بیشتر ابزارهای مدیریت پشته با زبانهای declarative به شما اجازه میدهند کامپوننتهای مشترک را با همان زبان بنویسید. CloudFormation پشتههای تو در تو (nested stacks) دارد و Terraform ماژول دارد. میتوانید پارامترها را به این ماژولها پاس دهید، و زبانها حداقل مقداری قابلیت برنامهنویسی دارند (مثل زیرزبان expressionهای HCL برای Terraform).
اما زبانها اساساً declarative هستند، پس تقریباً همه منطق پیچیده نوشتهشده در آنها وحشیانه است.
پس ماژولهای کد declarative بهترین کار را برای تعریف کامپوننتهای زیرساختی که خیلی تغییر نمیکنند انجام میدهند. یک ماژول declarative برای ماژول facade (ببینید «الگو: ماژول Facade» در صفحه ۲۷۴) که یک منبع ارائهشده توسط پلتفرم زیرساخت را wrap و ساده میکند، خوب کار میکند. این ماژولها وقتی برای موارد پیچیدهتر استفاده میکنید ناخوشایند میشوند و ماژولهای spaghetti ایجاد میکنند (ببینید «ضدالگو: ماژول Spaghetti» در صفحه ۲۸۰).
همانطور که در «چالش: تستهای کد declarative اغلب ارزش کم دارند» در صفحه ۱۱۰ ذکر شد، تست یک ماژول declarative باید نسبتاً ساده باشد. نتایج اعمال یک ماژول declarative خیلی تغییر نمیکند، پس پوشش تست جامع لازم نیست. این به معنای آن نیست که نباید برای این ماژولها تست بنویسید. وقتی یک ماژول چند declaration را ترکیب میکند تا موجودیت پیچیدهتری بسازد، باید تست کنید که نیازمندی خود را برآورده میکند.
ایجاد پویای عناصر پشته با کتابخانهها
برخی ابزارهای مدیریت پشته، مثل Pulumi و AWS CDK، از زبانهای imperative عمومیمنظوره استفاده میکنند. میتوانید از این زبانها برای نوشتن کتابخانههای قابل بازاستفاده استفاده کنید که از کد پروژه پشته خود فراخوانی میکنید. یک کتابخانه میتواند منطق پیچیدهتری شامل شود که منابع زیرساختی را بر اساس نحوه استفاده بهصورت پویا provision میکند.
مثلاً زیرساخت تیم ShopSpinner شامل پشتههای زیرساخت application server مختلف است. هر کدام از این پشتهها یک application server و ساختارهای شبکه برای آن برنامه provision میکنند. بعضی برنامهها public-facing هستند و بقیه internally facing.
در هر دو حالت، پشته زیرساخت باید یک آدرس IP و نام DNS به سرور اختصاص دهد و مسیر شبکه از gateway مربوطه ایجاد کند. آدرس IP و نام DNS برای برنامه public-facing در برابر internally facing متفاوت خواهد بود. و برنامه public-facing به قانون firewall برای اجازه اتصال نیاز دارد.
پشته checkout_service یک برنامه public-facing میزبانی میکند:
application_networking = new ApplicationServerNetwork(PUBLIC_FACING, "checkout")
virtual_machine:
name: appserver-checkout
vlan: $(application_networking.address_block)
ip_address: $(application_networking.private_ip_address)کد پشته یک شی ApplicationServerNetwork از کتابخانه application_networking ایجاد میکند که عناصر زیرساختی لازم را provision یا reference میکند:
class ApplicationServerNetwork {
def vlan;
def public_ip_address;
def private_ip_address;
def gateway;
def dns_hostname;
public ApplicationServerNetwork(application_access_type, hostname) {
if (application_access_type == PUBLIC_FACING) {
vlan = get_public_vlan()
public_ip_address = allocate_public_ip()
dns_hostname = PublicDNS.set_host_record(
"${hostname}.shopspinners.xyz",
this.public_ip_address
)
} else {
// Similar stuff but for a private VLAN
}
private_ip_address = allocate_ip_from(this.vlan)
gateway = get_gateway(this.vlan)
create_route(gateway, this.private_ip_address)
if (application_access_type == PUBLIC_FACING) {
create_firewall_rule(ALLOW, '0.0.0.0', this.private_ip_address, 443)
}
}
}این pseudocode سرور را به VLAN عمومی موجود اختصاص میدهد و آدرس IP خصوصی آن را از محدوده آدرس VLAN تنظیم میکند. همچنین یک ورودی DNS عمومی برای سرور تنظیم میکند که در مثال ما checkout.shopspinners.xyz خواهد بود. کتابخانه gateway را بر اساس VLAN استفادهشده پیدا میکند، پس این برای برنامه internally facing متفاوت خواهد بود.
الگوها برای کامپوننتهای پشته
مجموعه الگوها و ضدالگوهای زیر ایدههایی برای طراحی کامپوننتهای پشته و ارزیابی کامپوننتهای موجود میدهد. لیست کاملی از راههایی که باید یا نباید ماژولها و کتابخانهها بسازید نیست؛ بلکه نقطه شروعی برای فکر کردن درباره موضوع است.
الگو: ماژول Facade
همچنین شناختهشده بهعنوان: wrapper module.
یک ماژول facade یک رابط سادهشده به یک منبع از زبان ابزار پشته یا پلتفرم زیرساخت ایجاد میکند. ماژول چند پارامتر به کد فراخواننده expose میکند (مثال ۱۶-۱ را ببینید).
مثال ۱۶-۱. کد نمونه با استفاده از ماژول facade
use module: shopspinner-server
name: checkout-appserver
memory: 8GBماژول از این پارامترها برای فراخوانی منبعی که wrap میکند استفاده میکند، و مقادیر پارامترهای دیگر لازم برای منبع را hardcode میکند (مثال ۱۶-۲).
مثال ۱۶-۲. کد ماژول facade نمونه
declare module: shopspinner-server
virtual_machine:
name: ${name}
source_image: hardened-linux-base
memory: ${memory}
provision:
tool: servermaker
maker_server: maker.shopspinner.xyz
role: application_server
network:
vlan: application_zone_vlanاین ماژول نمونه به فراخواننده اجازه میدهد یک سرور مجازی ایجاد کند و نام و مقدار حافظه سرور را مشخص کند. هر سروری که با ماژول ایجاد میشود از source image، role و شبکهای که ماژول تعریف میکند استفاده میکند.
انگیزه (Motivation)
یک ماژول facade یک use case رایج برای یک منبع زیرساختی را ساده و استاندارد میکند. کد پشتهای که از ماژول facade استفاده میکند باید سادهتر و خواناتر باشد. بهبودهای کیفیت کد ماژول بهسرعت برای همه پشتههایی که از آن استفاده میکنند در دسترس است.
کاربرد (Applicability)
ماژولهای facade برای use caseهای ساده بهترین کار را میکنند، معمولاً شامل یک منبع زیرساختی پایه.
پیامدها (Consequences)
یک ماژول facade محدود میکند چگونه میتوانید از منبع زیرساختی زیرین استفاده کنید. انجام این کار میتواند مفید باشد، گزینهها را ساده کند و حول پیادهسازیهای بهتر و امنتر استاندارد شود. اما انعطاف را محدود میکند، پس برای هر use case اعمال نمیشود.
یک ماژول لایه اضافی کد بین کد پشته و کدی است که مستقیماً منابع زیرساختی را مشخص میکند. این لایه اضافی حداقل مقداری overhead به نگهداری، دیباگ و بهبود کد اضافه میکند. همچنین میتواند درک کد پشته را سختتر کند.
پیادهسازی (Implementation)
پیادهسازی یک ماژول facade معمولاً شامل مشخص کردن یک منبع زیرساختی با تعدادی مقدار hardcode شده، و تعداد کمی مقدار است که از کد استفادهکننده از ماژول عبور میکند. یک زبان زیرساخت declarative برای ماژول facade مناسب است.
الگوهای مرتبط (Related patterns)
یک ماژول obfuscation ماژول facadeای است که خیلی چیز پنهان نمیکند و پیچیدگی بدون افزودن ارزش زیاد اضافه میکند. یک ماژول bundle («الگو: ماژول Bundle» در صفحه ۲۷۸) چند منبع زیرساختی مرتبط را declare میکند، پس مثل ماژول facade با قطعات بیشتر است.
ضدالگو: ماژول Obfuscation
یک ماژول obfuscation کد یک عنصر زیرساختی تعریفشده توسط زبان پشته یا پلتفرم زیرساخت را wrap میکند، اما آن را ساده نمیکند یا ارزش خاصی اضافه نمیکند. در بدترین موارد، ماژول کد را پیچیده میکند. مثال ۱۶-۳ را ببینید.
مثال ۱۶-۳. کد نمونه با استفاده از ماژول obfuscation
use module: any_server
server_name: checkout-appserver
ram: 8GB
source_image: base_linux_image
provisioning_tool: servermaker
server_role: application_server
vlan: application_zone_vlanخود ماژول پارامترها را مستقیماً به کد ابزار مدیریت پشته پاس میدهد، همانطور که در مثال ۱۶-۴ نشان داده شده.
مثال ۱۶-۴. کد ماژول obfuscation نمونه
declare module: any_server
virtual_machine:
name: ${server_name}
source_image: ${origin_server_image}
memory: ${ram}
provision:
tool: ${provisioning_tool}
role: ${server_role}
network:
vlan: ${server_vlan}انگیزه (Motivation)
یک ماژول obfuscation ممکن است یک ماژول facade («الگو: ماژول Facade» در صفحه ۲۷۴) باشد که اشتباه پیش رفته. گاهی مردم این نوع ماژول را با هدف پیروی از اصل DRY مینویسند (ببینید «اجتناب از تکرار» در صفحه ۲۵۳). میبینند کدی که یک عنصر زیرساختی رایج، مثل سرور مجازی، load balancer یا security group، در چند جای codebase استفاده میشود. پس ماژولی میسازند که آن نوع عنصر را یکبار declare میکند و همهجا از آن استفاده میکنند. اما چون عناصر در قسمتهای مختلف کد متفاوت استفاده میشوند، باید تعداد زیادی پارامتر در ماژول expose کنند.
بعضی مردم ماژولهای obfuscation را در جستجوی طراحی زبان خودشان برای اشاره به عناصر زیرساختی میسازند و زبان ارائهشده توسط ابزار پشته خود را «بهبود» میدهند.
کاربرد (Applicability)
هیچکس عمداً ماژول obfuscation نمینویسد. ممکن است بحث کنید آیا ماژول دادهشده obfuscate میکند یا facade است، و آن بحث مفید است. باید در نظر بگیرید آیا ماژول ارزش واقعی اضافه میکند و اگر نه، آن را به کدی refactor کنید که مستقیماً از زبان پشته استفاده میکند.
پیامدها (Consequences)
نوشتن، استفاده و نگهداری کد ماژول بهجای استفاده مستقیم از constructهای ارائهشده توسط ابزار پشته overhead اضافه میکند. کد بیشتری برای نگهداری، overhead شناختی برای یادگیری، و قطعات متحرک اضافی در فرایند build و delivery اضافه میکند. یک کامپوننت باید ارزش کافی اضافه کند تا overhead ارزشش را داشته باشد.
پیادهسازی (Implementation)
اگر ماژول نه منابعی که تعریف میکند را ساده میکند و نه نسبت به کد زبان پشته زیرین ارزش اضافه میکند، استفادهها را با استفاده مستقیم از زبان پشته جایگزین کنید.
الگوهای مرتبط (Related patterns)
یک ماژول obfuscation شبیه ماژول facade («الگو: ماژول Facade» در صفحه ۲۷۴) است، اما کد زیرین را بهطور محسوس ساده نمیکند.
ضدالگو: ماژول Unshared
یک ماژول unshared فقط یکبار در یک codebase استفاده میشود، نه اینکه توسط چند پشته بازاستفاده شود.
انگیزه (Motivation)
مردم معمولاً ماژولهای unshared را بهعنوان راهی برای سازماندهی کد درون یک پروژه پشته ایجاد میکنند.
کاربرد (Applicability)
با رشد کد یک پروژه پشته، ممکن است وسوسه شوید کد را به ماژولها تقسیم کنید. اگر کد را طوری تقسیم کنید که بتوانید برای هر ماژول تست بنویسید، این میتواند کار با کد را آسانتر کند. در غیر این صورت، ممکن است راههای بهتری برای بهبود codebase وجود داشته باشد.
پیامدها (Consequences)
سازماندهی کد یک پشته تکی به ماژولها overhead به codebase اضافه میکند، احتمالاً شامل versioning و قطعات متحرک دیگر. ساخت ماژول قابل بازاستفاده وقتی نیازی به بازاستفاده ندارید نمونهای از YAGNI («You Aren't Gonna Need It») است، سرمایهگذاری تلاش الان برای منفعتی که ممکن است در آینده نیاز داشته باشید یا نداشته باشید.
پیادهسازی (Implementation)
وقتی یک پروژه پشته خیلی بزرگ میشود، چند جایگزین برای انتقال کد آن به ماژولها وجود دارد. اغلب بهتر است پشته را به چند پشته تقسیم کنید، با استفاده از الگوی ساختاری پشته مناسب (فصل ۱۷ را ببینید). اگر پشته نسبتاً cohesive است (ببینید «ویژگیهای کامپوننتهای خوب طراحیشده» در صفحه ۲۵۲)، میتوانید بهجای آن ساده کد را در فایلهای مختلف و در صورت لازم پوشههای مختلف سازماندهی کنید. انجام این کار میتواند کد را بدون overhead گزینههای دیگر آسانتر برای پیمایش و درک کند.
قانون سهگانه برای بازاستفاده نرمافزار پیشنهاد میکند وقتی سه جایی پیدا کردید که به استفاده از آن نیاز دارید، چیزی را به کامپوننت قابل بازاستفاده تبدیل کنید.²
الگوهای مرتبط (Related patterns)
یک ماژول unshared ممکن است نزدیک به عناصر زیرساختی سطح پایین map شود، مثل ماژول facade («الگو: ماژول Facade» در صفحه ۲۷۴)، یا به موجودیت سطح بالاتر، مثل موجودیت دامنه زیرساخت («الگو: موجودیت دامنه زیرساخت» در صفحه ۲۸۳).
الگو: ماژول Bundle
یک ماژول bundle مجموعهای از منابع زیرساختی مرتبط را با رابط سادهشده declare میکند. کد پشته از ماژول برای تعریف آنچه باید provision کند استفاده میکند:
use module: application_server
service_name: checkout_service
application_name: checkout_application
application_version: 1.23
min_cluster: 1
max_cluster: 3
ram_required: 4GBکد ماژول چند منبع زیرساختی declare میکند، معمولاً حول یک منبع هسته. در مثال ۱۶-۵، منبع یک server cluster است، اما همچنین شامل load balancer و ورودی DNS است.
مثال ۱۶-۵. کد ماژول برای application server
declare module: application_server
server_cluster:
id: "${service_name}-cluster"
min_size: ${min_cluster}
max_size: ${max_cluster}
each_server_node:
source_image: base_linux
memory: ${ram_required}
provision:
tool: servermaker
role: appserver
parameters:
app_package: "${checkout_application}-${application_version}.war"
app_repository: "repository.shopspinner.xyz"
load_balancer:
protocol: https
target:
type: server_cluster
target_id: "${service_name}-cluster"
dns_entry:
id: "${service_name}-hostname"
record_type: "A"
hostname: "${service_name}.shopspinner.xyz"
ip_address: {$load_balancer.ip_address}انگیزه (Motivation)
یک ماژول bundle برای تعریف مجموعه cohesive از منابع زیرساختی مفید است. از کد verbose و تکراری اجتناب میکند. این ماژولها برای capture کردن دانش درباره عناصر مختلف لازم و نحوه wire کردن آنها برای یک هدف رایج مفیدند.
کاربرد (Applicability)
یک ماژول bundle وقتی با زبان پشته declarative کار میکنید و منابع درگیر در use caseهای مختلف تغییر نمیکنند مناسب است. اگر ببینید ماژول باید منابع مختلف ایجاد کند یا آنها را متفاوت پیکربندی کند بسته به استفاده، باید یا ماژولهای جداگانه ایجاد کنید، یا به زبان imperative بروید و یک موجودیت دامنه زیرساخت بسازید (ببینید «الگو: موجودیت دامنه زیرساخت» در صفحه ۲۸۳).
پیامدها (Consequences)
یک ماژول bundle ممکن است در بعضی موقعیتها منابع بیشتری از آنچه نیاز دارید provision کند. کاربران ماژول باید درک کنند چه چیزی provision میکند، و اگر برای use case آنها overkill است از ماژول اجتناب کنند.
پیادهسازی (Implementation)
ماژول را بهصورت declarative تعریف کنید، شامل عناصر زیرساختی که نزدیک به هدف declare شده مرتبطاند.
الگوهای مرتبط (Related patterns)
یک ماژول facade («الگو: ماژول Facade» در صفحه ۲۷۴) یک منبع زیرساختی تکی را wrap میکند، در حالی که ماژول bundle چند منبع شامل میشود، اگرچه هر دو ماهیت declarative دارند. یک موجودیت دامنه زیرساخت («الگو: موجودیت دامنه زیرساخت» در صفحه ۲۸۳) شبیه ماژول bundle است، اما منابع زیرساختی را بهصورت پویا تولید میکند. یک ماژول spaghetti ماژول bundleای است که آرزو دارد موجودیت دامنه باشد اما بهخاطر محدودیتهای زبان declarative خود به جنون فرو میرود.
ضدالگو: ماژول Spaghetti
یک ماژول spaghetti تا جایی قابل پیکربندی است که نتایج بهطور قابل توجهی متفاوت بسته به پارامترهای دادهشده به آن ایجاد میکند. پیادهسازی ماژول messy و سخت برای درک است، چون قطعات متحرک زیادی دارد (مثال ۱۶-۶ را ببینید).
مثال ۱۶-۶. نمونه ماژول spaghetti
declare module: application-server-infrastructure
variable: network_segment = {
if ${parameter.network_access} = "public"
id: public_subnet
else if ${parameter.network_access} = "customer"
id: customer_subnet
else
id: internal_subnet
end
}
switch ${parameter.application_type}:
"java":
virtual_machine:
origin_image: base_tomcat
network_segment: ${variable.network_segment}
server_configuration:
if ${parameter.database} != "none"
database_connection: ${database_instance.my_database.connection_string}
end
...
"NET":
virtual_machine:
origin_image: windows_server
network_segment: ${variable.network_segment}
server_configuration:
if ${parameter.database} != "none"
database_connection: ${database_instance.my_database.connection_string}
end
...
"php":
container_group:
cluster_id: ${parameter.container_cluster}
container_image: nginx_php_image
network_segment: ${variable.network_segment}
server_configuration:
if ${parameter.database} != "none"
database_connection: ${database_instance.my_database.connection_string}
end
...
end
switch ${parameter.database}:
"mysql":
database_instance: my_database
type: mysql
...
...این کد نمونه سروری که ایجاد میکند را به یکی از سه network segment مختلف اختصاص میدهد، و اختیاری یک database cluster ایجاد میکند و connection string را به پیکربندی سرور پاس میدهد. در بعضی موارد، گروهی از نمونه container بهجای سرور مجازی ایجاد میکند. این ماژول کمی وحشی است.
انگیزه (Motivation)
مثل سایر ضدالگوها، مردم ماژول spaghetti را تصادفی ایجاد میکنند، اغلب در طول زمان. ممکن است یک ماژول facade («الگو: ماژول Facade» در صفحه ۲۷۴) یا ماژول bundle («الگو: ماژول Bundle» در صفحه ۲۷۸) ایجاد کنید که در پیچیدگی رشد میکند تا use caseهای متفاوتی را handle کند که در ظاهر مشابه به نظر میرسند.
ماژولهای spaghetti اغلب از تلاش برای پیادهسازی موجودیت دامنه زیرساخت («الگو: موجودیت دامنه زیرساخت» در صفحه ۲۸۳) با زبان declarative نتیجه میدهند.
پیامدها (Consequences)
ماژولی که کارهای زیادی انجام میدهد کمتر قابل نگهداری از یکی با scope محدودتر است. هر چه ماژول کارهای بیشتری انجام دهد و تغییرات بیشتری در زیرساختی که میتواند ایجاد کند وجود داشته باشد، تغییر آن بدون شکستن چیزی سختتر است. این ماژولها تست سختتری دارند. همانطور که در فصل ۸ توضیح میدهم، کد بهتر طراحیشده تست آسانتری دارد، پس اگر برای نوشتن تستهای خودکار و pipelineهای build برای تست ماژول بهصورت ایزوله مشکل دارید، نشانهای است که ماژول spaghetti دارید.
پیادهسازی (Implementation)
کد یک ماژول spaghetti اغلب شامل شرطهایی است که specificationهای مختلف را در موقعیتهای مختلف اعمال میکند. مثلاً، یک ماژول database cluster ممکن است پارامتری برای انتخاب اینکه کدام database provision شود بگیرد.
وقتی متوجه میشوید ماژول spaghetti در دست دارید، باید آن را refactor کنید. اغلب میتوانید آن را به ماژولهای مختلف تقسیم کنید، هر کدام با وظیفه متمرکزتر. مثلاً، ممکن است ماژول زیرساخت application تکی خود را به ماژولهای مختلف برای قسمتهای مختلف زیرساخت برنامه decompose کنید. نمونهای از پشتهای که از ماژولهای decompose شده به این شکل استفاده میکند، بهجای استفاده از ماژول spaghetti از مثال ۱۶-۶، ممکن است شبیه مثال ۱۶-۷ باشد.
مثال ۱۶-۷. نمونه استفاده از ماژولهای decompose شده بهجای یک ماژول spaghetti تکی
use module: java-application-servers
name: checkout_appserver
application: "shopping_app"
application_version: "4.20"
network_segment: customer_subnet
server_configuration:
database_connection: ${module.mysql-database.outputs.connection_string}
use module: mysql-database
cluster_minimum: 1
cluster_maximum: 3
allow_connections_from: customer_subnetهر کدام از ماژولها کوچکتر، سادهتر و بنابراین آسانتر برای نگهداری و تست از ماژول spaghetti اصلی هستند.
الگوهای مرتبط (Related patterns)
یک ماژول spaghetti اغلب تلاشی برای ساخت موجودیت دامنه زیرساخت با کد declarative است. همچنین میتواند ماژول facade («الگو: ماژول Facade» در صفحه ۲۷۴) یا ماژول bundle («الگو: ماژول Bundle» در صفحه ۲۷۸) باشد که مردم سعی کردهاند برای handle کردن use caseهای مختلف گسترش دهند.
الگو: موجودیت دامنه زیرساخت
یک موجودیت دامنه زیرساخت یک کامپوننت پشته سطح بالا را با ترکیب چند منبع زیرساختی سطح پایینتر پیادهسازی میکند. نمونهای از مفهوم سطح بالاتر، زیرساخت لازم برای اجرای یک برنامه است.
این مثال نشان میدهد چگونه کتابخانهای که یک نمونه زیرساخت برنامه Java را پیادهسازی میکند ممکن است از کد پروژه پشته استفاده شود:
use module: application_server
service_name: checkout_service
application_name: checkout_application
application_version: 1.23
traffic_level: mediumکد برنامه و نسخه برای deploy و همچنین سطح ترافیک را تعریف میکند. کد کتابخانه موجودیت دامنه ممکن است شبیه مثال ماژول bundle (مثال ۱۶-۵) باشد، اما شامل کد پویا برای provision منابع بر اساس پارامتر traffic_level است:
...
switch (${traffic_level}) {
case ("high") {
$appserver_cluster.min_size = 3
$appserver_cluster.max_size = 9
} case ("medium") {
$appserver_cluster.min_size = 2
$appserver_cluster.max_size = 5
} case ("low") {
$appserver_cluster.min_size = 1
$appserver_cluster.max_size = 2
}
}
...انگیزه (Motivation)
یک موجودیت دامنه اغلب بخشی از یک لایه abstraction (ببینید «ساخت یک لایه Abstraction» در صفحه ۲۸۴) است که مردم میتوانند برای تعریف و ساخت زیرساخت بر اساس نیازمندیهای سطح بالاتر استفاده کنند. یک تیم پلتفرم زیرساخت کامپوننتهایی میسازد که تیمهای دیگر میتوانند برای ensamble کردن پشتهها استفاده کنند.
کاربرد (Applicability)
چون یک موجودیت دامنه زیرساخت منابع زیرساختی را بهصورت پویا provision میکند، باید به زبان imperative نوشته شود نه declarative. ببینید «زبانهای declarative در برابر imperative برای زیرساخت» در صفحه ۴۴ برای بیشتر.
پیادهسازی (Implementation)
در سطح concrete، پیادهسازی یک موجودیت دامنه زیرساخت مسئله نوشتن کد است. اما بهترین راه برای ایجاد codebase با کیفیت بالا که برای مردم یادگیری و نگهداری آسان است، رویکرد design-led است.
توصیه میکنم از درسهای آموختهشده در معماری و طراحی نرمافزار بهره بگیرید. الگوی موجودیت دامنه زیرساخت از Domain Driven Design (DDD) مشتق میشود، که یک مدل مفهومی برای دامنه کسبوکار یک سیستم نرمافزاری ایجاد میکند و از آن برای هدایت طراحی خود سیستم استفاده میکند.³ زیرساخت، بهویژه وقتی بهعنوان نرمافزار طراحی و ساخته میشود، باید بهعنوان دامنهای به خودی خود دیده شود. دامنه، ساخت، تحویل و اجرای نرمافزار است.
یک رویکرد بهویژه قدرتمند این است که یک سازمان از DDD برای طراحی معماری نرمافزار کسبوکار استفاده کند، و سپس دامنه را گسترش دهد تا سیستمها و سرویسهای استفادهشده برای ساخت و اجرای آن نرمافزار را شامل شود.
الگوهای مرتبط (Related patterns)
یک ماژول bundle (ببینید «الگو: ماژول Bundle» در صفحه ۲۷۸) شبیه موجودیت دامنه است در اینکه مجموعه cohesive از منابع زیرساختی ایجاد میکند. اما ماژول bundle معمولاً مجموعه نسبتاً static از منابع ایجاد میکند، بدون تغییر زیاد. mindset یک ماژول bundle معمولاً bottom-up است، شروع با منابع زیرساختی برای ایجاد. یک موجودیت دامنه رویکرد top-down است، شروع با آنچه برای use case لازم است.
بیشتر ماژولهای spaghetti (ببینید «ضدالگو: ماژول Spaghetti» در صفحه ۲۸۰) برای پشتههای زیرساخت نتیجه فشار دادن کد declarative برای پیادهسازی منطق پویا هستند. اما گاهی یک موجودیت دامنه زیرساخت بیش از حد پیچیده میشود. یک موجودیت دامنه با cohesion ضعیف به ماژول spaghetti تبدیل میشود.
ساخت یک لایه Abstraction
یک لایه abstraction یک رابط سادهشده به منابع سطح پایینتر ارائه میدهد. مجموعهای از کامپوننتهای پشته قابل بازاستفاده و ترکیبپذیر میتواند بهعنوان لایه abstraction برای منابع زیرساختی عمل کند. کامپوننتها میتوانند دانش نحوه ensamble کردن منابع سطح پایین expose شده توسط پلتفرم زیرساخت به موجودیتهایی که برای مردم متمرکز بر وظایف سطح بالاتر مفیدند را پیادهسازی کنند.
مثلاً، یک تیم برنامه ممکن است نیاز داشته باشد محیطی تعریف کند که شامل application server، نمونه database و دسترسی به message queueها باشد. تیم میتواند از کامپوننتهایی استفاده کند که جزئیات ensamble کردن قوانین routing و مجوزهای منابع زیرساختی را abstract میکنند.
کامپوننتها حتی برای تیمی که مهارت و تجربه پیادهسازی منابع سطح پایین را دارد مفیدند. یک abstraction کمک میکند نگرانیهای مختلف را جدا کند، تا مردم روی مسئله در سطح جزئیات خاص تمرکز کنند. همچنین باید بتوانند drill down کنند تا کامپوننتهای زیرین را درک و در صورت نیاز بهبود یا گسترش دهند.
ممکن است بتوانید لایه abstraction را برای بعضی سیستمها با کامپوننتهای staticتر مثل ماژولهای facade (ببینید «الگو: ماژول Facade» در صفحه ۲۷۴) یا ماژولهای bundle (ببینید «الگو: ماژول Bundle» در صفحه ۲۷۸) پیادهسازی کنید. اما اغلب نیاز دارید کامپوننتهای لایه انعطافپذیرتر باشند، پس کامپوننتهای پویا مثل موجودیتهای دامنه زیرساخت (ببینید «الگو: موجودیت دامنه زیرساخت» در صفحه ۲۸۳) مفیدترند.
یک لایه abstraction ممکن است بهصورت organic با ساخت کتابخانهها و کامپوننتهای دیگر توسط مردم ظهور کند. اما داشتن طراحی و استانداردهای سطح بالاتر مفید است تا کامپوننتهای لایه خوب با هم کار کنند و در نمای cohesive سیستم جا بگیرند.
کامپوننتهای یک لایه abstraction معمولاً با زبان زیرساخت سطح پایین («زبانهای زیرساخت سطح پایین» در صفحه ۵۴) ساخته میشوند. بسیاری تیمها ساخت یک زبان سطح بالاتر («زبانهای زیرساخت سطح بالا» در صفحه ۵۵) برای تعریف پشتهها با لایه abstraction خود مفید مییابند. نتیجه اغلب یک زبان declarative سطح بالاتر است که نیازمندیهای بخشی از محیط runtime برنامه را مشخص میکند، که به کامپوننتهای پویا نوشتهشده به زبان imperative سطح پایین فراخوانی میکند.
مدلهای Abstraction برنامه
Open Application Model نمونهای از تلاش برای تعریف معماری استانداردی است که برنامه، runtime و زیرساخت را از هم جدا میکند.
نتیجهگیری
ساخت پشتهها از کامپوننتها وقتی چند نفر و تیم روی زیرساخت کار میکنند و از آن استفاده میکنند مفید است. اما مراقب پیچیدگیای باشید که با لایههای abstraction و کتابخانههای کامپوننت همراه است، و مطمئن شوید استفاده از این constructها را با اندازه و پیچیدگی سیستم خود تطبیق دهید.
¹ همانطور که این را مینویسم، در اواسط ۲۰۲۰، فروشندگان ابزار بهسرعت استراتژیهای خود را حول انواع زبانها تکامل میدهند. امیدوارم چند سال آینده بلوغ componentization پشته را ببینیم.
² قانون سهگانه در کتاب Robert Glass، Facts and Fallacies of Software Engineering (Addison-Wesley) تعریف شد. Jeff Atwood نیز در پست خود درباره توهم بازاستفاده درباره قانون سهگانه نظر داد.
³ ببینید Domain-Driven Design: Tackling Complexity in the Heart of Software، اثر Eric Evans (Addison-Wesley).