Architecture « افزونه مستقل + ریه مصرفکننده »: چرا ساختهای Gradle من تکرار میشوند؟
منتشر شده در 14 May 2026
- مشکل: سه معماری که زمانم را از بین بردند
- حل : دو ساخت مستقل، ریشه مصرفکننده
- چرا این معماری من را به خود جلب کرد
- ساختار زیرپوشه `{name}-plugin/
- چیزی که ما انجام نمیدهیم
- مقایسه : سه معماری مواجه با Dogfooding
- قرارداد DAG : یک ساخت ریشه هرگز از یک زیرپوشه
- نتیجه : تکرار ظاهری که زمان را صرفهجویی میکند
- منابع
شما یک مخزن پلاگین Gradle را کلون میکنید. شما یک ترمینال را باز میکنید. شما چه چیزی را تایپ میکنید؟ بعد؟./gradlew tasks, حتماً. ولی در کدام پوشه؟ ریشه؟ زیر-ماژول؟ آیا باید اول README را بخوانیم تا بدانیم چگونه ساخت کنیم؟ اگر تردید میکنید، آیا در یک ثانیه، معماری پروژه خراب شده است.
این است که چطور این مشکل را برای همیشه حل کردم — و چرا این pattern امروز امضای تمام پلاگینهای Gradle من در`foundry/public/`.
مشکل: سه معماری که زمانم را از بین بردند
قبل از همگرایی به الگو فعلی، من بین سه روش حیران شدم تقلیدی برای سازماندهی یک پلاگین Gradle. هر کدام یک عیب فاحش داشت.
Option 1 : افزونه جداگانه (بدون dogfooding)
پروژه فقط پلاگین را دارد. هیچ مثالی از استفاده. هیچ پروژهای که آن را به کار میگیرد. برای تست آن، باید یک پروژه خارجی ایجاد کنیم، y ارجاع دادن به پلاگین از طریق`mavenLocal`یا یک ساخت ترکیبی و فقط بررسی کنیم که کار میکند.
$ git clone mon-plugin
$ cd mon-plugin
$ ./gradlew build # le plugin compile
$ # ... et maintenant ? comment je l'essaie ?
|
افزونه Gradle بدون مثال استفاده، یک کتابخانه بدون تستهای یکپارچگی. هرگز نمیدانید آیا آخرین تغییر داشته باشد تجربه کاربری را شکسته |
گزینهٔ ۲: Monorepo کلاسیک (include(":plugin"))
گریدل`init`میسازد`settings.gradle.kts`با`include("plugin")`. ریشه و ماژول فرعی یک دیمون یکسان را به اشتراک میگذارند، تنظیمات یکسان، همان کتالوغها. کاربردی، اما پیوسته.
.
├── settings.gradle.kts → include("plugin")
├── build.gradle.kts → plugins { id("mon-plugin") }
├── plugin/
│ └── build.gradle.kts → java-gradle-plugin
└── gradle/
└── libs.versions.toml
مشکل چیست:
-
ریشهبایدداشتن همان نسخه Gradle که زیر ماژول دارد.
-
`libs.versions.toml`به اشتراک گذاشته شده — نسخههای کاتلگهای مشترک، وابستگیها
که از یک ماژول به ماژول دیگر فرار میکنند.
-
امکان ساخت پلاگین بهصورت مستقل از ریشه وجود ندارد.
-
CI باید ماژولهای دو را بسازد، حتی اگر تنها پلاگین تغییر کرده باشد.
گزینهٔ ۳: ساخت ترکیبی (includeBuild())
ما دو را در ساختهای Gradle جداگانه جدا میکنیم و آنها را از طریق includeBuild("mon-plugin")`در`settings.gradle.kts.
این بهتر است. ساخت افزونه جداگانه است. اما مصرفکننده باید به صراحت به بیلد خارجی ارجاع دهد — و خروجی آن `./gradlew tasks`به ریشه بستگی به تنظیم مناسب از دارد کامپوزیتی. کپیبرداری zero-config نیست: باید بدانید که افزونه در یک پوشه جداگانه است که ریشه آن را ارجاع میدهد و غیره.
.
├── settings.gradle.kts → includeBuild("plugin-build/")
├── build.gradle.kts → plugins { id("mon-plugin") }
└── plugin-build/
├── settings.gradle.kts
└── build.gradle.kts
من به دنبال بهتر بودم. بسیار بهتر.
حل : دو ساخت مستقل، ریشه مصرفکننده
این الگو است که در نهایت به آن دست یافتم :
.
├── settings.gradle.kts ← racine consommateur
├── build.gradle.kts ← 3 lignes : apply plugin + dogfood
├── gradle/
│ └── libs.versions.toml ← catalogue du consommateur
├── {name}-plugin/ ← BUILD INDÉPENDANT
│ ├── gradlew ← son propre wrapper
│ ├── settings.gradle.kts ← rootProject.name = "{name}-plugin"
│ ├── build.gradle.kts ← java-gradle-plugin, signing, publish
│ ├── gradle/
│ │ ├── libs.versions.toml ← catalogue du plugin
│ │ └── wrapper/
│ ├── src/ ← sources du plugin
│ ├── .agents/ ← gouvernance agent
│ └── *.adoc ← AGENT, PROMPT_REPRISE, snapshot, etc.
└── site.yml / slides-context.yml / ... ← configs dogfood
|
کلید :`{name}-plugin/`یک پروژه Gradle کامل و مستقل است. او یک wrapper اختصاصی دارد، یک settings اختصاصی دارد، یک catalogue اختصاصی از نسخهها. خود را کلون میسازد، ساخته میشود، تست میشود و منتشر میشود بدون ریشه آگاه ازexistsش باشید. |
رِوت، او فقط یک کار انجام میدهد: افزونه را اعمال میکند.
plugins {
alias(libs.plugins.bakery)
}
repositories {
mavenLocal()
mavenCentral()
}
bakery { configPath = file("site.yml").absolutePath }
۳ خط در صورت`bakery-gradle`. هیچ چیز بیشتر. صفر`include(), صفر`includeBuild(), صفر زیرپروژه. یک ساخت Gradle کلاسیک که یک پلاگین را همانند هر پلاگین دیگری اعمال میکند کدام مصرفکننده این کار را میکرد؟
روند کاری
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12
left to right direction
package "ریشه (مصرفکننده)" #CCFFCC {
usecase "مخزن را کلون کنید" as Clone
usecase "./gradlew tasks" as Tasks
usecase "./gradlew bake" as Dogfood
note bottom of Dogfood
Exerce le plugin
Feedback immédiat
Zéro config
end note
}
package "{name}-plugin/ (ساخت مستقل)" #CCE5FF {
usecase "./gradlew build" as BuildPlugin
usecase "./gradlew publishToMavenLocal" as MavenLocal
usecase "./gradlew test" as TestPlugin
note bottom of BuildPlugin
Cycle de vie isolé
Tests unitaires + Cucumber
CI dédiée possible
end note
}
Clone -down-> Tasks
Tasks -down-> Dogfood
Dogfood ..> MavenLocal : "به پلاگین وابسته است
منتشر شده به صورت محلی"
BuildPlugin -up-> MavenLocal
TestPlugin -up-> BuildPlugin
@enduml
ورکفلو ملموس
# 1. Builder le plugin
$ cd codebase-plugin
$ ./gradlew publishToMavenLocal
# 2. L'exercer depuis la racine
$ cd ..
$ ./gradlew indexCodebase queryCodebase snapshot
# La boucle est fermée. Le plugin est testé dans des conditions
# réelles de consommation, par le projet même qui l'héberge.
چرا این معماری من را به خود جلب کرد
سه مزیت که وقتی ترکیب میشوند، هزینهٔ «تکرار» را جبران میکنند:
Dogfooding بومی، بازخورد فوری
بهترین تست یک پلاگین Gradle، استفاده از آن است. یک تست واحدی شبیهسازیشده نیست. یک`GradleRunner`با یک پروژه تست. یک vrai build که پلاگین را روی فایلهای واقعی اعمال میکند.
$ git clone bakery-gradle
$ cd bakery-gradle
$ ./gradlew bake # ← le plugin est exercé immédiatement
اگر پلاگین خراب باشد، ساخت ریشه آن را میگوید. لازم نیست بروید. جستجوی یک پروژه تست خارجی. Dogfooding اولین است تسک که توسط یک مشارکتکننده جدید راهاندازی میشود. این تست دود (smoke test) نهایی است.
|
قانون ساده است: اگر ریشه کامپایل شود و`./gradlew tasks` وظائف پلاگین شما را نمایش میدهد، پلاگین کارآمد است. هیچ تعجبی نیست. در تولید. |
2. کلونسازی بدون پیکربندی
Un `git clone && ./gradlew tasks`و تازهوارد همه چیز را میبیند پیاده رفتن بدون پیکربندی چیزی. Le`build.gradle.kts`ریشه است مستندات استفاده پلاگین زنده.`site.yml`به کنار یک پیکربندی مورد انتظار را نشان میدهد.
با جایگزین آن مقایسه کنید: یک README از سه پاراگراف که توضیح دهد چگونه plugin را بسازیم و چگونه پروژه را بسازیم آزمون. یک مشارکتکننده جدید فایل README را به صورت قطری میخواند، غلط میکند، یک issue باز میکند — در حالی که اطلاعات میتوانست باشد قابل اجرا.
|
اسنادی که قویترین است، همان چیزی نیست که آن را بخوانیم. این همان است که مااجرا کن. ساخت ریشه مستندات است اجراپذیر پلاگین. |
3. ساختهای منحصربهفرد, CI مستقل
افزونه اورنور Gradle مخصوص خود را دارد، دور زندگی مخصوص خود را دارد، تستهای خود. شما میتوانید :
-
Gradle را در پلاگین ارتقا دهید بدون لمس کردن ریشه
-
افزودن یک وابستگی در پلاگین بدون اینکه در ریشه (root) لو شود
-
پلاگین را بدون تأثیر زدن بر بیلد اصلی (تا زمانی که شما
نسخه خراب را منتشر نکنید)
-
داشتن یک CI که پلاگین را build/test کند، و دیگری که آن را تمرین دهد
root — بهصورت مستقل
.github/workflows/
├── test-plugin.yml → codebase-plugin/.gradlew build
├── test-root.yml → .gradlew tasks (vérifie que le plugin est consommable)
└── publish.yml → codebase-plugin/.gradlew publish
ساختار زیرپوشه `{name}-plugin/
بیایید بهتر نگاهی به چیزی که در پوشهی پلاگین زندگی میکند بیندازیم:
codebase-plugin/
├── gradlew ← wrapper indépendant
├── settings.gradle.kts ← @Suppress("UnstableApiUsage")
│ + foobar-resolver-convention
├── build.gradle.kts ← java-gradle-plugin + signing + publish
├── gradle/
│ ├── libs.versions.toml ← catalogue complet (langchain4j, pgvector...)
│ └── wrapper/
├── buildSrc/ ← classes utilitaires buildSrc
│ ├── build.gradle.kts
│ └── src/main/kotlin/
│ ├── codebase/ ← CodebaseYmlAnonymizer, CodebaseConfiguration
│ ├── benchmark/ ← BenchmarkConfig, BenchmarkProtocol
│ ├── readme/ ← ReadmeYmlAnonymizer
│ ├── site/ ← SiteYmlAnonymizer
│ ├── slider/ ← SliderYmlAnonymizer
│ └── snapshot/ ← SnapshotManager
├── src/
│ ├── main/kotlin/codebase/
│ │ ├── CodebasePlugin.kt ← class Plugin<Project>
│ │ ├── rag/ ← pgvector, embedding, anonymization...
│ │ ├── benchmark/ ← BenchmarkRunner, export, comparison...
│ │ └── walker/ ← WorkspaceWalker
│ └── test/
│ ├── kotlin/codebase/scenarios/ ← steps Cucumber
│ ├── features/ ← .feature files
│ └── resources/datasets/ ← fixtures .adoc, .yml, .json
├── .agents/ ← gouvernance agent (INDEX, SESSIONS, etc.)
├── AGENT.adoc ← règles agent
├── PROMPT_REPRISE.adoc ← mission session
├── BACKLOG.adoc ← backlog produit
├── snapshot.adoc ← snapshot auto-généré du projet
└── embeds.yml ← config RAG embeds
همه اینجا است. بین ریشه و زیرپوشه پراکندگیای نیست. توسعهدهنده که کار میکند روی پلاگین هرگز نیاز ندارد به ترک کردن`codebase-plugin/`. توسعه دهندهای که از این پلاگین استفاده میکند فقط به ریشه نگاه میکند — و`build.gradle.kts`az 3 lignes? Wait, we need Persian: "az 3 khatoun"? Actually "lignes" → "خطوط". So "از 3 خطوط". We’ll output that.
</think>
از 3 خطوط به او بگو که او همهٔ چیزی که به او نیاز دارد بداند.
فهرست نسخهها : دو فایل متمایز
|
|
|
تعداد وابستگیها |
۲-۳ (پلاگین + فایل readme به صورت اختیاری) |
30+ (langchain4j, pgvector, cucumber…) |
نقش |
افزونه را مصرف کنید |
ساخت پلاگین |
کی آن را میخواند |
کاربر پلاگین |
توسعهدهنده پلاگین |
root دارای یک فهرست بهصورت عمدی حداقلی است. plugin دارای یک فهرست است. الالتباس ممکن نیست: هر build دارای scope خود است. به وابستگیها.
|
اگر شما پیش از این یک ساعت را برای رفع یک تداخل وابستگی صرف کردهاید بین پلاگین شما و پروژه تست شما، ارزش آن را میفهمید این جداسازی. کاتالوگهای مستقل این مشکل را بر طرف میکنند به صورت ساخت. |
چیزی که ما انجام نمیدهیم
این الگو جادویی نیست. یک محدودیت را تحمیل میکند که من به سادگی بر من وارد میکند :
ریشه پلاگین را نمیسازد. شما باید`publishToMavenLocal` ou استقرار دادن بر روی یک مخزن قبل از اینکه ریشه بتواند آن را مصرف کند
این یک هزینه کم است، و این محدودیت خوب است. ریش پلاگین را به عنوان یک مشتری خارجی — از طریق Maven — مصرف میکند. دقیقاً. همانطور که یک پروژه شخص ثالث انجام میدهد. اگر پلاگین قابل انتشار نباشد, ریشه به شما بلافاصله این را میگوید.
# La seule "friction" du pattern
$ cd codebase-plugin && ./gradlew publishToMavenLocal && cd ..
$ ./gradlew tasks --group=codebase
مقایسه : سه معماری مواجه با Dogfooding
پلاگین جداگانه |
مونورپو`include()` |
مرکب`includeBuild()` |
ریشه + پلاگین مستقل |
|
`git clone && gradlew tasks`وژھات پلاگین را میدهد |
❌ |
(Empty output) |
✅ |
تمام-spanهای کد داخل علامت بیکتك ( |
ساخت پلاگین مستقل از ریشه |
Could you please provide the French text you’d like translated? |
❌ |
✅ |
✅ |
کاتالگ نسخههای جداگانه |
✅ |
❌ |
(Empty) |
✅ |
هیچ`include()` ni |
✅ |
❌ |
❌ |
(Note: Since no French text was provided to translate, the output is empty.) |
Dogfood بومی بدون تنظیمات |
❌ |
✅ |
⚠️ |
(empty) |
CI پلاگین مستقل |
✅ |
❌ |
✅ |
✅ |
ریشه یک مثال از مصرف واقعی است |
❌ |
⚠️ |
(empty) |
✅ |
ستون سمت راست تمام جعبهها را علامت میزند. این دلیل است که من من بیشتر به عقب بر خواهم گشت.
قرارداد DAG : یک ساخت ریشه هرگز از یک زیرپوشه
این معماری بهطور طبیعی در DAG N0→N3 ادغام میشود. از فضای کاری من. الگو این است: افزونه N2 مرکز است از وابستگیهای خود، ریشه N3 یک ترمینال است که hubs را اعمال میکند.
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 11
title قرارداد DAG — ریشه مصرفکننده vs افزونه مستقل
package "N3 — ریشه (انبار)" #FFCCCC {
[build.gradle.kts (3 lignes)] as ROOT
[libs.versions.toml (minimal)] as ROOT_TOML
note right of ROOT
plugins { bakery }
bakery { configPath = ... }
Aucune logique métier
end note
}
package "N2 — {name}-افزونه/" #CCFFCC {
[build.gradle.kts (complet)] as PLUGIN
[src/main/kotlin/] as SRC
[gradle/libs.versions.toml] as PLUGIN_TOML
[buildSrc/] as BUILDSRC
note bottom of PLUGIN
java-gradle-plugin
signing + publish
Tests unitaires + Cucumber
CI dédiée
end note
}
ROOT_TOML --> ROOT : "نسخه پلاگین"
ROOT -down-> PLUGIN : "انتشار به مخزن محلی Maven"
@enduml
Le codebase-gradle/build.gradle.kts`شش خط است. هیچ`src/, هیچ`buildSrc/, هیچ`gradle/rag-bench.gradle.kts. عادل plugins { alias(libs.plugins.codebase) }`و مخازن تمام پیچیدگی در`codebase-plugin/.
نتیجه : تکرار ظاهری که زمان را صرفهجویی میکند
وقتی این ساختار را به کسی نشان میدهم، اولین واکنش غالباً : « اما تو دو داری`gradlew`, دو`settings.gradle.kts`, دو`libs.versions.toml`— این تکرار است !
بله. و نه.
duplication » به این معناست که یکسانی اطلاعات را در دو مکان بازتولید میشود. اینجا، دو فایل منفصل هستند که دو کاربرد منفصل را انجام میدهند: كتالگ پلاگین (30+ وابستگی برای builder) و کتالگ از ریشه (۲-۳ وابسته برای مصرف). لایهپوش پلاگین (نسخه قفلشده برای توسعه) و بستهبندی ریشه (نسخهای که احتمالاً متفاوت است، برای تمرین افزونه).
این تفکیک است مسئولیتها اعمال شده به سیستم ساخت. هر ساخت یک کاری انجام میدهد و فقط یک کاری. ریشه مصرف میکند. پلاگین ساخته میشود.
قیمت؟ یک سفارش`publishToMavenLocal`بين بیلد پلاگین و ساخت ریشه. مزیت؟ یک شفافیت معماری که ساعات دیبگ در مسیر بعدی را حذف میکند.
از زمانی که من این الگو را پیادهسازی کردم روی`bakery-gradle`, plantuml-gradle, codebase-gradle، و سایر پلاگینهای`foundry/public/, من ندارم هرگز هنگام باز کردن یک ترمینال در یکی از مخازن من تردید نکردم. ال اولین انعکاس —./gradlew tasks`— همیشه میرود، همیشه میدهد کارهای خوب، و بلافاصله به من میگوید که همه چیز سالم است.
این همان معماری خوب است. این در یک فایل README خوانده نمیشود. او در یک ترمینال، در کمتر از ده ثانیه، خود را امتحان میکند.
منابع
-
مقاله 0105 — ادغام Graphify در یک گردش کار Gradle