| @@ -0,0 +1,29 | |||
|
|
1 | package org.implab.gradle.common.core.lang; | |
|
|
2 | ||
|
|
3 | import java.util.LinkedList; | |
|
|
4 | import java.util.List; | |
|
|
5 | import java.util.function.Consumer; | |
|
|
6 | ||
|
|
7 | public final class Deferred<T> { | |
|
|
8 | private final List<Consumer<T>> listeners = new LinkedList<>(); | |
|
|
9 | private T value; | |
|
|
10 | private boolean resolved = false; | |
|
|
11 | ||
|
|
12 | public void resolve(T value) { | |
|
|
13 | if (resolved) { | |
|
|
14 | throw new IllegalStateException("Already resolved"); | |
|
|
15 | } | |
|
|
16 | this.value = value; | |
|
|
17 | this.resolved = true; | |
|
|
18 | listeners.forEach(listener -> listener.accept(value)); | |
|
|
19 | listeners.clear(); | |
|
|
20 | } | |
|
|
21 | ||
|
|
22 | public void whenResolved(Consumer<T> listener) { | |
|
|
23 | if (resolved) { | |
|
|
24 | listener.accept(value); | |
|
|
25 | } else { | |
|
|
26 | listeners.add(listener); | |
|
|
27 | } | |
|
|
28 | } | |
|
|
29 | } | |
| @@ -0,0 +1,346 | |||
|
|
1 | # Identity-First Model: Preserving Lazy Semantics Without Custom Events | |
|
|
2 | ||
|
|
3 | In build and configuration systems, the same tension appears again and again: | |
|
|
4 | ||
|
|
5 | * you want an **observable model** that consumers can subscribe to using something like `all(...)` | |
|
|
6 | * you do **not** want that subscription to destroy laziness | |
|
|
7 | * and you would prefer to avoid introducing a custom event bus, event ordering, and a separate lifecycle model | |
|
|
8 | ||
|
|
9 | A practical way to solve this is to **separate identity from computed state**. | |
|
|
10 | ||
|
|
11 | ## Core idea | |
|
|
12 | ||
|
|
13 | The idea is simple: | |
|
|
14 | ||
|
|
15 | * one object is responsible only for **identity** and minimal selection metadata | |
|
|
16 | * the actual aggregate content is obtained through **separate API calls** | |
|
|
17 | * those API calls may be lazy, expensive, cached, provider-based, or computed on demand | |
|
|
18 | ||
|
|
19 | So instead of one “fat” object, we get two layers. | |
|
|
20 | ||
|
|
21 | ### 1. Identity layer | |
|
|
22 | ||
|
|
23 | Lightweight objects that: | |
|
|
24 | ||
|
|
25 | * are cheap to create | |
|
|
26 | * are effectively immutable | |
|
|
27 | * are safe to observe eagerly | |
|
|
28 | * work well as keys and subscription points | |
|
|
29 | ||
|
|
30 | ### 2. State / aggregate access layer | |
|
|
31 | ||
|
|
32 | Separate APIs that: | |
|
|
33 | ||
|
|
34 | * resolve or compute content from identity | |
|
|
35 | * do heavy work only when needed | |
|
|
36 | * preserve lazy semantics where that actually matters | |
|
|
37 | ||
|
|
38 | This is especially useful when a collection must be replayable, but should not drag expensive materialization along with it. | |
|
|
39 | ||
|
|
40 | --- | |
|
|
41 | ||
|
|
42 | ## The problem this solves | |
|
|
43 | ||
|
|
44 | Consider a typical Gradle-like scenario. | |
|
|
45 | ||
|
|
46 | An adapter wants to subscribe to a collection: | |
|
|
47 | ||
|
|
48 | ```java | |
|
|
49 | projections.getProjections().all(projection -> { | |
|
|
50 | ... | |
|
|
51 | }); | |
|
|
52 | ``` | |
|
|
53 | ||
|
|
54 | If `projection` is a heavy object that already contains: | |
|
|
55 | ||
|
|
56 | * computed bindings | |
|
|
57 | * providers of source sets | |
|
|
58 | * derived state | |
|
|
59 | * partially materialized objects | |
|
|
60 | ||
|
|
61 | then `all(...)` starts forcing things that were supposed to stay lazy. | |
|
|
62 | ||
|
|
63 | Typical symptoms: | |
|
|
64 | ||
|
|
65 | * lazy semantics are weakened or broken | |
|
|
66 | * coupling increases | |
|
|
67 | * plugin application order starts to matter | |
|
|
68 | * custom events begin to look tempting: `onCreated`, `onResolved`, `onMaterialized` | |
|
|
69 | ||
|
|
70 | The problem is not that `all(...)` is bad. | |
|
|
71 | The problem is that **too much meaning has been packed into the observable object**. | |
|
|
72 | ||
|
|
73 | --- | |
|
|
74 | ||
|
|
75 | ## The solution: observe identity, not state | |
|
|
76 | ||
|
|
77 | If the observable collection contains only identity objects, the picture changes. | |
|
|
78 | ||
|
|
79 | For example: | |
|
|
80 | ||
|
|
81 | ```java | |
|
|
82 | public interface SourceSetProjection extends Named { | |
|
|
83 | } | |
|
|
84 | ``` | |
|
|
85 | ||
|
|
86 | This object contains only identity: | |
|
|
87 | ||
|
|
88 | * `name` | |
|
|
89 | * perhaps a small amount of selection metadata | |
|
|
90 | * but not heavy aggregate state | |
|
|
91 | ||
|
|
92 | Now this subscription: | |
|
|
93 | ||
|
|
94 | ```java | |
|
|
95 | projections.getProjections().all(projection -> { | |
|
|
96 | ... | |
|
|
97 | }); | |
|
|
98 | ``` | |
|
|
99 | ||
|
|
100 | is no longer dangerous. | |
|
|
101 | It eagerly materializes only cheap keys, not the full aggregate graph. | |
|
|
102 | ||
|
|
103 | The actual content is requested separately: | |
|
|
104 | ||
|
|
105 | ```java | |
|
|
106 | Set<VariantLayerBinding> bindings = projections.getBindings(projection); | |
|
|
107 | NamedDomainObjectProvider<GenericSourceSet> sourceSet = | |
|
|
108 | materializer.getSourceSet(projection.getName()); | |
|
|
109 | ``` | |
|
|
110 | ||
|
|
111 | Those calls may remain: | |
|
|
112 | ||
|
|
113 | * lazy | |
|
|
114 | * computed | |
|
|
115 | * cached | |
|
|
116 | * tied to runtime lifecycle | |
|
|
117 | * delegated to a dedicated materializer | |
|
|
118 | ||
|
|
119 | --- | |
|
|
120 | ||
|
|
121 | ## Example | |
|
|
122 | ||
|
|
123 | ### A problematic design | |
|
|
124 | ||
|
|
125 | Suppose we define projection like this: | |
|
|
126 | ||
|
|
127 | ```java | |
|
|
128 | public interface SourceSetProjection extends Named { | |
|
|
129 | Set<VariantLayerBinding> getBindings(); | |
|
|
130 | NamedDomainObjectProvider<GenericSourceSet> getSourceSet(); | |
|
|
131 | } | |
|
|
132 | ``` | |
|
|
133 | ||
|
|
134 | This is problematic because `SourceSetProjection` is no longer just identity. It is already close to an aggregate. | |
|
|
135 | ||
|
|
136 | It mixes: | |
|
|
137 | ||
|
|
138 | * symbolic identity | |
|
|
139 | * relation data | |
|
|
140 | * runtime references into a foreign domain | |
|
|
141 | ||
|
|
142 | Subscribing via `all(...)` now risks pulling in much more than intended. | |
|
|
143 | ||
|
|
144 | The type says “projection”, but internally it already carries half the system. | |
|
|
145 | ||
|
|
146 | --- | |
|
|
147 | ||
|
|
148 | ### A cleaner design | |
|
|
149 | ||
|
|
150 | Split responsibilities instead: | |
|
|
151 | ||
|
|
152 | ```java | |
|
|
153 | public interface SourceSetProjection extends Named { | |
|
|
154 | } | |
|
|
155 | ``` | |
|
|
156 | ||
|
|
157 | ```java | |
|
|
158 | public interface SourceSetProjections { | |
|
|
159 | NamedDomainObjectCollection<SourceSetProjection> getProjections(); | |
|
|
160 | Set<VariantLayerBinding> getBindings(String sourceSetName); | |
|
|
161 | } | |
|
|
162 | ``` | |
|
|
163 | ||
|
|
164 | ```java | |
|
|
165 | public interface SourceSetMaterializer { | |
|
|
166 | NamedDomainObjectProvider<GenericSourceSet> getSourceSet(String sourceSetName); | |
|
|
167 | } | |
|
|
168 | ``` | |
|
|
169 | ||
|
|
170 | Now the adapter flow looks like this: | |
|
|
171 | ||
|
|
172 | ```java | |
|
|
173 | projections.getProjections().all(projection -> { | |
|
|
174 | Set<VariantLayerBinding> bindings = | |
|
|
175 | projections.getBindings(projection.getName()); | |
|
|
176 | ||
|
|
177 | NamedDomainObjectProvider<GenericSourceSet> sourceSet = | |
|
|
178 | materializer.getSourceSet(projection.getName()); | |
|
|
179 | ||
|
|
180 | // apply adapter-specific policy | |
|
|
181 | }); | |
|
|
182 | ``` | |
|
|
183 | ||
|
|
184 | What changed: | |
|
|
185 | ||
|
|
186 | * replayable subscription is preserved | |
|
|
187 | * eager observation is acceptable because `SourceSetProjection` is cheap | |
|
|
188 | * expensive and computed state has moved to separate APIs | |
|
|
189 | * materialization remains under the control of a single owner | |
|
|
190 | ||
|
|
191 | --- | |
|
|
192 | ||
|
|
193 | ## Why this is often better than events | |
|
|
194 | ||
|
|
195 | When identity and state are mixed together, people quickly start inventing events: | |
|
|
196 | ||
|
|
197 | * `projectionCreated` | |
|
|
198 | * `projectionResolved` | |
|
|
199 | * `sourceSetAvailable` | |
|
|
200 | * `sourceSetMaterialized` | |
|
|
201 | ||
|
|
202 | That usually happens because it becomes important to know **when exactly** an object is “ready enough”. | |
|
|
203 | ||
|
|
204 | If the observable object contains only identity, and heavy state is obtained separately, then many of those events become unnecessary. | |
|
|
205 | ||
|
|
206 | The architecture becomes calmer: | |
|
|
207 | ||
|
|
208 | * an **identity registry** | |
|
|
209 | * a **lookup API** for relations | |
|
|
210 | * a **lazy materialization API** for heavy objects | |
|
|
211 | ||
|
|
212 | Instead of saying: | |
|
|
213 | ||
|
|
214 | > “When this object becomes sufficiently ready, I will react.” | |
|
|
215 | ||
|
|
216 | you can say: | |
|
|
217 | ||
|
|
218 | > “I can observe identity immediately, and ask for the expensive state only when I actually need it.” | |
|
|
219 | ||
|
|
220 | This is easier to reason about, easier to test, and usually easier to evolve. | |
|
|
221 | ||
|
|
222 | --- | |
|
|
223 | ||
|
|
224 | ## What should live inside an identity object | |
|
|
225 | ||
|
|
226 | An identity object does not have to be completely empty. | |
|
|
227 | It may carry **selection metadata**, as long as that metadata is: | |
|
|
228 | ||
|
|
229 | * cheap | |
|
|
230 | * stable | |
|
|
231 | * not expensive to initialize | |
|
|
232 | * not turning the object into an aggregate | |
|
|
233 | ||
|
|
234 | Typical examples: | |
|
|
235 | ||
|
|
236 | * `id` | |
|
|
237 | * `name` | |
|
|
238 | * `kind` | |
|
|
239 | * `type` | |
|
|
240 | * domain key | |
|
|
241 | ||
|
|
242 | What should usually stay out: | |
|
|
243 | ||
|
|
244 | * computed aggregate content | |
|
|
245 | * runtime references to foreign domains | |
|
|
246 | * lazy providers of heavy objects | |
|
|
247 | * derived state that can trigger premature materialization | |
|
|
248 | ||
|
|
249 | A useful rule of thumb: | |
|
|
250 | ||
|
|
251 | **An identity object contains selection metadata; aggregate content is obtained separately.** | |
|
|
252 | ||
|
|
253 | --- | |
|
|
254 | ||
|
|
255 | ## Why this works well with `all(...)` | |
|
|
256 | ||
|
|
257 | `all(...)` weakens laziness only if the observed objects are themselves heavy or stateful. | |
|
|
258 | ||
|
|
259 | If the observed objects are: | |
|
|
260 | ||
|
|
261 | * cheap | |
|
|
262 | * identity-only | |
|
|
263 | * effectively immutable | |
|
|
264 | ||
|
|
265 | then eager observation is usually acceptable. | |
|
|
266 | ||
|
|
267 | So the real principle is: | |
|
|
268 | ||
|
|
269 | **Eager observation of identity is often harmless. | |
|
|
270 | Eager observation of computed state is not.** | |
|
|
271 | ||
|
|
272 | That is why `all(...)` can be perfectly fine for collections of: | |
|
|
273 | ||
|
|
274 | * `Variant` | |
|
|
275 | * `Layer` | |
|
|
276 | * `Role` | |
|
|
277 | * `SourceSetProjection` | |
|
|
278 | ||
|
|
279 | as long as those objects stay on the identity side of the boundary. | |
|
|
280 | ||
|
|
281 | --- | |
|
|
282 | ||
|
|
283 | ## Where this principle is especially useful | |
|
|
284 | ||
|
|
285 | This approach is particularly effective when: | |
|
|
286 | ||
|
|
287 | * there is replayable observation via `all(...)` | |
|
|
288 | * identity objects are cheap and stable | |
|
|
289 | * aggregate content may be expensive | |
|
|
290 | * symbolic model and runtime model should remain separate | |
|
|
291 | * you want to avoid building a custom event system | |
|
|
292 | ||
|
|
293 | For Gradle-like models, this is often a very natural fit. | |
|
|
294 | ||
|
|
295 | --- | |
|
|
296 | ||
|
|
297 | ## When it is unnecessary | |
|
|
298 | ||
|
|
299 | If an object is: | |
|
|
300 | ||
|
|
301 | * small | |
|
|
302 | * cheap | |
|
|
303 | * and already fully represents its useful content | |
|
|
304 | ||
|
|
305 | then splitting identity and state may be overengineering. | |
|
|
306 | ||
|
|
307 | So this is not a universal rule. It is a tool to use when there is real tension between: | |
|
|
308 | ||
|
|
309 | * key | |
|
|
310 | * state | |
|
|
311 | * computation | |
|
|
312 | * runtime reference | |
|
|
313 | ||
|
|
314 | --- | |
|
|
315 | ||
|
|
316 | ## Practical conclusion | |
|
|
317 | ||
|
|
318 | The principle can be summarized like this: | |
|
|
319 | ||
|
|
320 | 1. **Identity objects** hold only identity and cheap selection metadata. | |
|
|
321 | 2. **Aggregate content** is not stored inside them, but retrieved through separate API calls. | |
|
|
322 | 3. Those API calls may perform: | |
|
|
323 | ||
|
|
324 | * lazy resolution | |
|
|
325 | * caching | |
|
|
326 | * heavy computation | |
|
|
327 | * materialization on demand | |
|
|
328 | 4. This makes it possible to use replayable mechanisms such as `all(...)` without destroying laziness where laziness actually matters. | |
|
|
329 | ||
|
|
330 | This is how you can combine: | |
|
|
331 | ||
|
|
332 | * simple observation | |
|
|
333 | * a clean model | |
|
|
334 | * no custom event bus | |
|
|
335 | * lazy materialization of heavy state | |
|
|
336 | ||
|
|
337 | --- | |
|
|
338 | ||
|
|
339 | # Short design note version | |
|
|
340 | ||
|
|
341 | A concise version of the same principle: | |
|
|
342 | ||
|
|
343 | > Use identity objects as cheap, observable keys. | |
|
|
344 | > Keep expensive or computed aggregate content out of them. | |
|
|
345 | > Resolve that content through separate APIs on demand. | |
|
|
346 | > This allows replayable observation (`all(...)`) without forcing premature materialization, and often removes the need for a custom event model. | |
| @@ -0,0 +1,306 | |||
|
|
1 | # Identity-first model: как сохранить lazy semantics без своих событий | |
|
|
2 | ||
|
|
3 | В системах конфигурации и сборки почти всегда возникает одно и то же напряжение: | |
|
|
4 | ||
|
|
5 | * хочется иметь **наблюдаемую модель**, на которую можно подписаться через что-то вроде `all(...)` | |
|
|
6 | * но не хочется, чтобы такая подписка **ломала ленивость** | |
|
|
7 | * ещё меньше хочется тащить в архитектуру собственную шину событий, порядок подписки и отдельный lifecycle | |
|
|
8 | ||
|
|
9 | Один из рабочих способов решить это — **разделить identity и вычисляемое состояние**. | |
|
|
10 | ||
|
|
11 | ## Суть принципа | |
|
|
12 | ||
|
|
13 | Идея простая: | |
|
|
14 | ||
|
|
15 | * отдельный объект отвечает только за **identity** и минимальные метаданные выбора | |
|
|
16 | * всё содержательное состояние агрегата получается **отдельными вызовами API** | |
|
|
17 | * эти вызовы уже могут быть lazy, дорогими, вычисляемыми, кэшируемыми, провайдерными — какими угодно | |
|
|
18 | ||
|
|
19 | То есть вместо одного “толстого” объекта мы получаем два слоя: | |
|
|
20 | ||
|
|
21 | ### 1. Identity layer | |
|
|
22 | ||
|
|
23 | Лёгкие объекты, которые: | |
|
|
24 | ||
|
|
25 | * дёшево создаются | |
|
|
26 | * не мутируют | |
|
|
27 | * безопасно наблюдаются eagerly | |
|
|
28 | * годятся как ключи и точки подписки | |
|
|
29 | ||
|
|
30 | ### 2. State / aggregate access layer | |
|
|
31 | ||
|
|
32 | Отдельные API, которые: | |
|
|
33 | ||
|
|
34 | * по identity находят или вычисляют содержимое | |
|
|
35 | * делают heavy work только по требованию | |
|
|
36 | * могут сохранять ленивую семантику | |
|
|
37 | ||
|
|
38 | Это особенно полезно там, где коллекция должна быть replayable, но при этом не должна тащить за собой дорогую материализацию. | |
|
|
39 | ||
|
|
40 | --- | |
|
|
41 | ||
|
|
42 | ## Проблема, которую это решает | |
|
|
43 | ||
|
|
44 | Рассмотрим типичную ситуацию в Gradle-подобной модели. | |
|
|
45 | ||
|
|
46 | Есть коллекция объектов, на которую адаптер хочет подписаться: | |
|
|
47 | ||
|
|
48 | ```java | |
|
|
49 | projections.getProjections().all(projection -> { | |
|
|
50 | ... | |
|
|
51 | }); | |
|
|
52 | ``` | |
|
|
53 | ||
|
|
54 | Если `projection` — это тяжёлый объект, внутри которого уже лежат: | |
|
|
55 | ||
|
|
56 | * вычисленные bindings | |
|
|
57 | * провайдеры на source sets | |
|
|
58 | * derived state | |
|
|
59 | * полу-материализованные сущности | |
|
|
60 | ||
|
|
61 | то `all(...)` начинает рано раскрывать то, что хотелось оставить ленивым. | |
|
|
62 | ||
|
|
63 | Появляются симптомы: | |
|
|
64 | ||
|
|
65 | * ломается lazy semantics | |
|
|
66 | * растёт связность | |
|
|
67 | * становится важен порядок применения плагинов | |
|
|
68 | * хочется заводить собственные события: `onCreated`, `onResolved`, `onMaterialized` | |
|
|
69 | ||
|
|
70 | Архитектура начинает скрипеть не потому, что `all(...)` плох, а потому что **слишком много смысла засунуто в наблюдаемый объект**. | |
|
|
71 | ||
|
|
72 | --- | |
|
|
73 | ||
|
|
74 | ## Решение: наблюдать identity, а не состояние | |
|
|
75 | ||
|
|
76 | Если сделать наблюдаемыми только identity objects, картина меняется. | |
|
|
77 | ||
|
|
78 | Например: | |
|
|
79 | ||
|
|
80 | ```java | |
|
|
81 | public interface SourceSetProjection extends Named { | |
|
|
82 | } | |
|
|
83 | ``` | |
|
|
84 | ||
|
|
85 | Такой объект содержит только identity: | |
|
|
86 | ||
|
|
87 | * `name` | |
|
|
88 | * возможно, ещё пару метаданных выбора | |
|
|
89 | * но не тяжёлое содержимое | |
|
|
90 | ||
|
|
91 | Тогда подписка: | |
|
|
92 | ||
|
|
93 | ```java | |
|
|
94 | projections.getProjections().all(projection -> { | |
|
|
95 | ... | |
|
|
96 | }); | |
|
|
97 | ``` | |
|
|
98 | ||
|
|
99 | больше не страшна. | |
|
|
100 | Она eagerly materializes только дешёвые ключи, а не весь агрегатный граф. | |
|
|
101 | ||
|
|
102 | Содержимое запрашивается отдельно: | |
|
|
103 | ||
|
|
104 | ```java | |
|
|
105 | Set<VariantLayerBinding> bindings = projections.getBindings(projection); | |
|
|
106 | NamedDomainObjectProvider<GenericSourceSet> sourceSet = | |
|
|
107 | materializer.getSourceSet(projection.getName()); | |
|
|
108 | ``` | |
|
|
109 | ||
|
|
110 | И вот эти вызовы уже могут быть: | |
|
|
111 | ||
|
|
112 | * lazy | |
|
|
113 | * вычисляемыми | |
|
|
114 | * кэшируемыми | |
|
|
115 | * привязанными к runtime lifecycle | |
|
|
116 | ||
|
|
117 | --- | |
|
|
118 | ||
|
|
119 | ## Пример | |
|
|
120 | ||
|
|
121 | ### Неудачный вариант | |
|
|
122 | ||
|
|
123 | Представим такой интерфейс: | |
|
|
124 | ||
|
|
125 | ```java | |
|
|
126 | public interface SourceSetProjection extends Named { | |
|
|
127 | Set<VariantLayerBinding> getBindings(); | |
|
|
128 | NamedDomainObjectProvider<GenericSourceSet> getSourceSet(); | |
|
|
129 | } | |
|
|
130 | ``` | |
|
|
131 | ||
|
|
132 | Проблемы тут сразу видны: | |
|
|
133 | ||
|
|
134 | * `SourceSetProjection` уже не identity object, а почти агрегат | |
|
|
135 | * внутри смешаны: | |
|
|
136 | ||
|
|
137 | * symbolic identity | |
|
|
138 | * relation data | |
|
|
139 | * runtime reference в чужой домен | |
|
|
140 | * подписка через `all(...)` начинает тащить за собой больше, чем хотелось бы | |
|
|
141 | ||
|
|
142 | На словах объект называется “projection”, а по факту внутри у него уже полсистемы. | |
|
|
143 | ||
|
|
144 | --- | |
|
|
145 | ||
|
|
146 | ### Более удачный вариант | |
|
|
147 | ||
|
|
148 | Разделяем ответственность: | |
|
|
149 | ||
|
|
150 | ```java | |
|
|
151 | public interface SourceSetProjection extends Named { | |
|
|
152 | } | |
|
|
153 | ``` | |
|
|
154 | ||
|
|
155 | ```java | |
|
|
156 | public interface SourceSetProjections { | |
|
|
157 | NamedDomainObjectCollection<SourceSetProjection> getProjections(); | |
|
|
158 | Set<VariantLayerBinding> getBindings(String sourceSetName); | |
|
|
159 | } | |
|
|
160 | ``` | |
|
|
161 | ||
|
|
162 | ```java | |
|
|
163 | public interface SourceSetMaterializer { | |
|
|
164 | NamedDomainObjectProvider<GenericSourceSet> getSourceSet(String sourceSetName); | |
|
|
165 | } | |
|
|
166 | ``` | |
|
|
167 | ||
|
|
168 | Теперь сценарий адаптера выглядит так: | |
|
|
169 | ||
|
|
170 | ```java | |
|
|
171 | projections.getProjections().all(projection -> { | |
|
|
172 | Set<VariantLayerBinding> bindings = | |
|
|
173 | projections.getBindings(projection.getName()); | |
|
|
174 | ||
|
|
175 | NamedDomainObjectProvider<GenericSourceSet> sourceSet = | |
|
|
176 | materializer.getSourceSet(projection.getName()); | |
|
|
177 | ||
|
|
178 | // apply adapter-specific policy | |
|
|
179 | }); | |
|
|
180 | ``` | |
|
|
181 | ||
|
|
182 | Что изменилось: | |
|
|
183 | ||
|
|
184 | * replayable подписка сохранилась | |
|
|
185 | * eager materialization допустима, потому что `SourceSetProjection` дешёвый | |
|
|
186 | * дорогое и вычисляемое состояние ушло в отдельные API | |
|
|
187 | * materialization остаётся под контролем одного владельца | |
|
|
188 | ||
|
|
189 | --- | |
|
|
190 | ||
|
|
191 | ## Почему это лучше событий | |
|
|
192 | ||
|
|
193 | Когда в модели смешаны identity и состояние, очень быстро хочется изобретать события: | |
|
|
194 | ||
|
|
195 | * `projectionCreated` | |
|
|
196 | * `projectionResolved` | |
|
|
197 | * `sourceSetAvailable` | |
|
|
198 | * `sourceSetMaterialized` | |
|
|
199 | ||
|
|
200 | Потому что в какой-то момент становится важно, **когда именно** объект уже “достаточно готов”. | |
|
|
201 | ||
|
|
202 | Если же наблюдаемый объект содержит только identity, а всё тяжёлое получается отдельными вызовами, событийная модель часто вообще не нужна. | |
|
|
203 | ||
|
|
204 | Получается более спокойная схема: | |
|
|
205 | ||
|
|
206 | * есть **identity registry** | |
|
|
207 | * есть **lookup API** для связей | |
|
|
208 | * есть **lazy materialization API** для тяжёлых сущностей | |
|
|
209 | ||
|
|
210 | То есть вместо событий: | |
|
|
211 | ||
|
|
212 | > “когда объект станет достаточно готов, я что-то сделаю” | |
|
|
213 | ||
|
|
214 | получается обычный и понятный flow: | |
|
|
215 | ||
|
|
216 | > “я вижу identity, а нужное состояние спрошу отдельно, когда оно мне действительно понадобится” | |
|
|
217 | ||
|
|
218 | Это проще и для reasoning, и для тестирования, и для эволюции API. | |
|
|
219 | ||
|
|
220 | --- | |
|
|
221 | ||
|
|
222 | ## Что именно должно жить в identity object | |
|
|
223 | ||
|
|
224 | Identity object не обязан быть “абсолютно пустым”. | |
|
|
225 | Он может содержать **метаданные выбора**, если они: | |
|
|
226 | ||
|
|
227 | * дешевы | |
|
|
228 | * стабильны | |
|
|
229 | * не требуют тяжёлой инициализации | |
|
|
230 | * не превращают объект в агрегат | |
|
|
231 | ||
|
|
232 | Например: | |
|
|
233 | ||
|
|
234 | * `id` | |
|
|
235 | * `name` | |
|
|
236 | * `kind` | |
|
|
237 | * `domain key` | |
|
|
238 | * maybe `type` | |
|
|
239 | ||
|
|
240 | Но он не должен содержать: | |
|
|
241 | ||
|
|
242 | * вычисляемое содержимое агрегата | |
|
|
243 | * ссылки времени выполнения на чужие домены | |
|
|
244 | * lazy providers на heavy objects | |
|
|
245 | * derived state, который может провоцировать раннюю материализацию | |
|
|
246 | ||
|
|
247 | Хорошая практическая формула: | |
|
|
248 | ||
|
|
249 | **identity object contains selection metadata; aggregate content is obtained separately.** | |
|
|
250 | ||
|
|
251 | --- | |
|
|
252 | ||
|
|
253 | ## Когда этот принцип особенно полезен | |
|
|
254 | ||
|
|
255 | Он особенно хорош, если: | |
|
|
256 | ||
|
|
257 | * есть replayable наблюдение через `all(...)` | |
|
|
258 | * identity-объекты дешёвые и почти immutable | |
|
|
259 | * содержимое агрегата может быть дорогим | |
|
|
260 | * нужен clean split между symbolic model и runtime model | |
|
|
261 | * хочется избежать собственной событийной шины | |
|
|
262 | ||
|
|
263 | Для Gradle-подобных моделей это вообще очень естественный приём. | |
|
|
264 | ||
|
|
265 | --- | |
|
|
266 | ||
|
|
267 | ## Когда он не нужен | |
|
|
268 | ||
|
|
269 | Если объект: | |
|
|
270 | ||
|
|
271 | * маленький | |
|
|
272 | * дешёвый | |
|
|
273 | * уже сам по себе и есть всё его содержимое | |
|
|
274 | ||
|
|
275 | то разделение на identity и отдельные lookup API может быть лишним. | |
|
|
276 | ||
|
|
277 | То есть этот принцип полезен не как догма, а как инструмент. | |
|
|
278 | Его стоит применять там, где реально есть риск смешения: | |
|
|
279 | ||
|
|
280 | * ключа | |
|
|
281 | * состояния | |
|
|
282 | * вычисления | |
|
|
283 | * runtime reference | |
|
|
284 | ||
|
|
285 | --- | |
|
|
286 | ||
|
|
287 | ## Практический итог | |
|
|
288 | ||
|
|
289 | Если сформулировать коротко, то принцип такой: | |
|
|
290 | ||
|
|
291 | 1. **Identity objects** содержат только identity и дешёвые метаданные выбора. | |
|
|
292 | 2. **Агрегатное содержимое** не хранится внутри них, а получается отдельными API-вызовами. | |
|
|
293 | 3. Эти API уже могут выполнять: | |
|
|
294 | ||
|
|
295 | * lazy resolution | |
|
|
296 | * кэширование | |
|
|
297 | * heavy computation | |
|
|
298 | * materialization | |
|
|
299 | 4. Благодаря этому можно безопасно использовать replayable механизмы вроде `all(...)`, не разрушая ленивую семантику там, где она действительно важна. | |
|
|
300 | ||
|
|
301 | Именно так удаётся совместить: | |
|
|
302 | ||
|
|
303 | * простое наблюдение | |
|
|
304 | * чистую модель | |
|
|
305 | * отсутствие собственных событий | |
|
|
306 | * ленивую материализацию тяжёлого состояния | |
| @@ -0,0 +1,185 | |||
|
|
1 | package org.implab.gradle.variants; | |
|
|
2 | ||
|
|
3 | import java.util.ArrayList; | |
|
|
4 | import java.util.HashMap; | |
|
|
5 | import java.util.HashSet; | |
|
|
6 | import java.util.List; | |
|
|
7 | import java.util.Map; | |
|
|
8 | import java.util.Set; | |
|
|
9 | import java.util.function.Consumer; | |
|
|
10 | import java.util.stream.Stream; | |
|
|
11 | ||
|
|
12 | import org.eclipse.jdt.annotation.NonNullByDefault; | |
|
|
13 | import org.gradle.api.Action; | |
|
|
14 | import org.gradle.api.NamedDomainObjectCollection; | |
|
|
15 | import org.gradle.api.NamedDomainObjectContainer; | |
|
|
16 | import org.gradle.api.NamedDomainObjectProvider; | |
|
|
17 | import org.gradle.api.Plugin; | |
|
|
18 | import org.gradle.api.Project; | |
|
|
19 | import org.implab.gradle.common.core.lang.Deferred; | |
|
|
20 | import org.implab.gradle.common.core.lang.Strings; | |
|
|
21 | import org.implab.gradle.common.sources.GenericSourceSet; | |
|
|
22 | import org.implab.gradle.common.sources.SourcesPlugin; | |
|
|
23 | import org.implab.gradle.variants.model.Layer; | |
|
|
24 | import org.implab.gradle.variants.model.Variant; | |
|
|
25 | import org.implab.gradle.variants.model.VariantsExtension; | |
|
|
26 | import org.implab.gradle.variants.sources.LayerProjectionRule; | |
|
|
27 | import org.implab.gradle.variants.sources.SourceSetMaterializer; | |
|
|
28 | import org.implab.gradle.variants.sources.SourceSetProjection; | |
|
|
29 | import org.implab.gradle.variants.sources.SourceSetProjections; | |
|
|
30 | import org.implab.gradle.variants.sources.VariantLayerBinding; | |
|
|
31 | import org.implab.gradle.variants.sources.VariantSourcesContext; | |
|
|
32 | import org.implab.gradle.variants.sources.VariantSourcesExtension; | |
|
|
33 | ||
|
|
34 | @NonNullByDefault | |
|
|
35 | public abstract class VariantSourcesPlugin implements Plugin<Project> { | |
|
|
36 | @Override | |
|
|
37 | public void apply(Project target) { | |
|
|
38 | // Apply the main VariantsPlugin to ensure the core variant model is available. | |
|
|
39 | target.getPlugins().apply(VariantsPlugin.class); | |
|
|
40 | target.getPlugins().apply(SourcesPlugin.class); | |
|
|
41 | // Access the VariantsExtension to configure variant sources. | |
|
|
42 | var variantsExtension = target.getExtensions().getByType(VariantsExtension.class); | |
|
|
43 | var objectFactory = target.getObjects(); | |
|
|
44 | ||
|
|
45 | var sources = SourcesPlugin.getSourcesExtension(target); | |
|
|
46 | ||
|
|
47 | var deferred = new Deferred<VariantSourcesContext>(); | |
|
|
48 | var layerProjectionRules = objectFactory.domainObjectContainer(LayerProjectionRule.class); | |
|
|
49 | ||
|
|
50 | var variantSourcesExtension = new VariantSourcesExtension() { | |
|
|
51 | @Override | |
|
|
52 | public NamedDomainObjectContainer<LayerProjectionRule> getLayerRules() { | |
|
|
53 | return layerProjectionRules; | |
|
|
54 | } | |
|
|
55 | ||
|
|
56 | @Override | |
|
|
57 | public void whenFinalized(Action<? super VariantSourcesContext> action) { | |
|
|
58 | deferred.whenResolved(action::execute); | |
|
|
59 | } | |
|
|
60 | }; | |
|
|
61 | target.getExtensions().add(VariantSourcesExtension.class, "variantSources", variantSourcesExtension); | |
|
|
62 | ||
|
|
63 | // create convention to automatically create layer projection rules for each | |
|
|
64 | // variant layer | |
|
|
65 | variantsExtension.getLayers().all(layer -> { | |
|
|
66 | // Automatically create a layer projection rule for each variant layer. | |
|
|
67 | variantSourcesExtension.layer(layer.getName(), rule -> { | |
|
|
68 | // Configure the source set name pattern based on the layer name. | |
|
|
69 | rule.getSourceSetNamePattern() | |
|
|
70 | .convention("{variant}{layerCapitalized}") | |
|
|
71 | .finalizeValueOnRead(); | |
|
|
72 | }); | |
|
|
73 | }); | |
|
|
74 | ||
|
|
75 | var projections = objectFactory.domainObjectContainer(SourceSetProjection.class); | |
|
|
76 | ||
|
|
77 | Map<String, Set<VariantLayerBinding>> projectionBindings = new HashMap<>(); | |
|
|
78 | ||
|
|
79 | var sourceSetProjections = new SourceSetProjections() { | |
|
|
80 | @Override | |
|
|
81 | public NamedDomainObjectCollection<SourceSetProjection> getProjections() { | |
|
|
82 | return projections; | |
|
|
83 | } | |
|
|
84 | ||
|
|
85 | @Override | |
|
|
86 | public Set<VariantLayerBinding> getBindings(String sourceSetName) { | |
|
|
87 | return projectionBindings.getOrDefault(sourceSetName, Set.of()); | |
|
|
88 | } | |
|
|
89 | ||
|
|
90 | @Override | |
|
|
91 | public Set<VariantLayerBinding> getBindings(SourceSetProjection projection) { | |
|
|
92 | return getBindings(projection.getName()); | |
|
|
93 | } | |
|
|
94 | }; | |
|
|
95 | ||
|
|
96 | Set<String> materializedSourceSets = new HashSet<>(); | |
|
|
97 | ||
|
|
98 | var materializer = new SourceSetMaterializer() { | |
|
|
99 | @Override | |
|
|
100 | public NamedDomainObjectProvider<GenericSourceSet> getSourceSet(String sourceSetName) { | |
|
|
101 | return materializedSourceSets.add(sourceSetName) | |
|
|
102 | ? sources.register(sourceSetName) | |
|
|
103 | : sources.named(sourceSetName); | |
|
|
104 | } | |
|
|
105 | }; | |
|
|
106 | ||
|
|
107 | var bindings = new VariantBindings(); | |
|
|
108 | ||
|
|
109 | target.afterEvaluate(t -> { | |
|
|
110 | // Once the project is evaluated, resolve the deferred context and finalize the | |
|
|
111 | // sources configuration. | |
|
|
112 | variantsExtension.getLayers().all(bindings::addLayer); | |
|
|
113 | variantsExtension.getVariants().all(bindings::addVariant); | |
|
|
114 | ||
|
|
115 | variantsExtension.getLayers().all(layer -> { | |
|
|
116 | // For each layer, apply the projection rules to generate source set projections. | |
|
|
117 | ||
|
|
118 | var rule = layerProjectionRules.maybeCreate(layer.getName()); | |
|
|
119 | var pattern = rule.getSourceSetNamePattern().getOrElse("{variant}{layerCapitalized}"); | |
|
|
120 | // Generate source set names based on the pattern and variant/layer information. | |
|
|
121 | // This is a simplified example; real implementation would need to consider | |
|
|
122 | // all variants and layers. | |
|
|
123 | var sourceSetName = pattern.replace("{layer}", layer.getName()) | |
|
|
124 | .replace("{variant}", "main") // Placeholder for actual variant name | |
|
|
125 | .replace("{layerCapitalized}", Strings.capitalize(layer.getName())); | |
|
|
126 | ||
|
|
127 | var projection = objectFactory.newInstance(SourceSetProjection.class, sourceSetName); | |
|
|
128 | projections.add(projection); | |
|
|
129 | // Bind the projection to the corresponding variant layer. | |
|
|
130 | projectionBindings.computeIfAbsent(sourceSetName, k -> new HashSet<>()) | |
|
|
131 | .add(new VariantLayerBinding(layer.getName(), projection)); | |
|
|
132 | }); | |
|
|
133 | ||
|
|
134 | var context = new VariantSourcesContext() { | |
|
|
135 | ||
|
|
136 | @Override | |
|
|
137 | public SourceSetProjections getProjections() { | |
|
|
138 | return sourceSetProjections; | |
|
|
139 | } | |
|
|
140 | ||
|
|
141 | @Override | |
|
|
142 | public SourceSetMaterializer getMaterializer() { | |
|
|
143 | return materializer; | |
|
|
144 | } | |
|
|
145 | ||
|
|
146 | // Implementation of the context that provides access to variant and layer | |
|
|
147 | // information. | |
|
|
148 | }; | |
|
|
149 | deferred.resolve(context); | |
|
|
150 | }); | |
|
|
151 | } | |
|
|
152 | ||
|
|
153 | class VariantBindings { | |
|
|
154 | private final Set<Layer> layers = new HashSet<>(); | |
|
|
155 | private final Set<Variant> variants = new HashSet<>(); | |
|
|
156 | ||
|
|
157 | private final List<Consumer<? super VariantLayerBinding>> listeners = new ArrayList<>(); | |
|
|
158 | ||
|
|
159 | void addLayer(Layer layer) { | |
|
|
160 | layers.add(layer); | |
|
|
161 | variants.stream() | |
|
|
162 | .map(variant -> VariantLayerBinding.of(variant, layer)) | |
|
|
163 | .forEach(this::notifyBindingAdded); | |
|
|
164 | } | |
|
|
165 | ||
|
|
166 | void addVariant(Variant variant) { | |
|
|
167 | variants.add(variant); | |
|
|
168 | layers.stream() | |
|
|
169 | .map(layer -> VariantLayerBinding.of(variant, layer)) | |
|
|
170 | .forEach(this::notifyBindingAdded); | |
|
|
171 | } | |
|
|
172 | ||
|
|
173 | void whenBindingAdded(Consumer<? super VariantLayerBinding> listener) { | |
|
|
174 | layers.stream() | |
|
|
175 | .flatMap(layer -> variants.stream().map(variant -> VariantLayerBinding.of(variant, layer))) | |
|
|
176 | .forEach(listener); | |
|
|
177 | listeners.add(listener); | |
|
|
178 | } | |
|
|
179 | ||
|
|
180 | private void notifyBindingAdded(VariantLayerBinding binding) { | |
|
|
181 | listeners.forEach(listener -> listener.accept(binding)); | |
|
|
182 | } | |
|
|
183 | } | |
|
|
184 | ||
|
|
185 | } | |
| @@ -0,0 +1,70 | |||
|
|
1 | package org.implab.gradle.variants.model; | |
|
|
2 | ||
|
|
3 | import java.util.HashSet; | |
|
|
4 | import java.util.Set; | |
|
|
5 | import java.util.function.Consumer; | |
|
|
6 | import java.util.stream.Stream; | |
|
|
7 | ||
|
|
8 | import org.gradle.api.Action; | |
|
|
9 | import org.gradle.api.Named; | |
|
|
10 | import org.gradle.api.provider.SetProperty; | |
|
|
11 | import org.implab.gradle.common.core.lang.Closures; | |
|
|
12 | import org.implab.gradle.common.core.lang.Strings; | |
|
|
13 | ||
|
|
14 | import groovy.lang.Closure; | |
|
|
15 | ||
|
|
16 | public interface VariantDefinition extends Named { | |
|
|
17 | /** | |
|
|
18 | * Role bindings declared inside this variant. | |
|
|
19 | * | |
|
|
20 | * The binding pair of role and layer names. | |
|
|
21 | */ | |
|
|
22 | SetProperty<RoleLayerBinding> getRoleBindings(); | |
|
|
23 | ||
|
|
24 | /** | |
|
|
25 | * Creates or returns an existing role binding and configures it. | |
|
|
26 | */ | |
|
|
27 | default void role(String name, Action<? super RoleSpec> action) { | |
|
|
28 | var spec = new RoleSpec(name); | |
|
|
29 | action.execute(spec); | |
|
|
30 | spec.accept(getRoleBindings()::add); | |
|
|
31 | } | |
|
|
32 | ||
|
|
33 | default void role(String name, Closure<?> closure) { | |
|
|
34 | role(name, Closures.action(closure)); | |
|
|
35 | } | |
|
|
36 | ||
|
|
37 | default void finalizeVariant() { | |
|
|
38 | getRoleBindings().finalizeValue(); | |
|
|
39 | } | |
|
|
40 | ||
|
|
41 | public static class RoleSpec { | |
|
|
42 | private final String name; | |
|
|
43 | private final Set<String> layerNames; | |
|
|
44 | ||
|
|
45 | public RoleSpec(String name) { | |
|
|
46 | this.name = name; | |
|
|
47 | this.layerNames = new HashSet<>(); | |
|
|
48 | } | |
|
|
49 | ||
|
|
50 | public String getName() { | |
|
|
51 | return name; | |
|
|
52 | } | |
|
|
53 | ||
|
|
54 | public Set<String> getLayerNames() { | |
|
|
55 | return layerNames; | |
|
|
56 | } | |
|
|
57 | ||
|
|
58 | public void layers(String name, String... extraNames) { | |
|
|
59 | Stream.concat(Stream.of(name), Stream.of(extraNames)) | |
|
|
60 | .map(Strings::requireNonBlank) | |
|
|
61 | .forEach(this.layerNames::add); | |
|
|
62 | } | |
|
|
63 | ||
|
|
64 | void accept(Consumer<? super RoleLayerBinding> consumer) { | |
|
|
65 | layerNames.stream() | |
|
|
66 | .map(layerName -> new RoleLayerBinding(name, layerName)) | |
|
|
67 | .forEach(consumer); | |
|
|
68 | } | |
|
|
69 | } | |
|
|
70 | } | |
| @@ -0,0 +1,26 | |||
|
|
1 | package org.implab.gradle.variants.sources; | |
|
|
2 | ||
|
|
3 | import org.gradle.api.Action; | |
|
|
4 | import org.gradle.api.Named; | |
|
|
5 | import org.gradle.api.provider.Property; | |
|
|
6 | ||
|
|
7 | /** | |
|
|
8 | * Projection rule for a layer. | |
|
|
9 | */ | |
|
|
10 | public interface LayerProjectionRule extends Named { | |
|
|
11 | ||
|
|
12 | /** | |
|
|
13 | * Pattern used to calculate the source set name. | |
|
|
14 | * Examples: | |
|
|
15 | * "{layer}" | |
|
|
16 | * "{variant}{layerCapitalized}" | |
|
|
17 | */ | |
|
|
18 | Property<String> getSourceSetNamePattern(); | |
|
|
19 | ||
|
|
20 | /** | |
|
|
21 | * Optional hook for future extension. | |
|
|
22 | */ | |
|
|
23 | default void configure(Action<? super LayerProjectionRule> action) { | |
|
|
24 | action.execute(this); | |
|
|
25 | } | |
|
|
26 | } No newline at end of file | |
| @@ -0,0 +1,15 | |||
|
|
1 | package org.implab.gradle.variants.sources; | |
|
|
2 | ||
|
|
3 | import org.gradle.api.NamedDomainObjectProvider; | |
|
|
4 | import org.implab.gradle.common.sources.GenericSourceSet; | |
|
|
5 | ||
|
|
6 | /** | |
|
|
7 | * Materializes symbolic source set names into actual GenericSourceSet instances. | |
|
|
8 | */ | |
|
|
9 | public interface SourceSetMaterializer { | |
|
|
10 | NamedDomainObjectProvider<GenericSourceSet> getSourceSet(String sourceSetName); | |
|
|
11 | ||
|
|
12 | default NamedDomainObjectProvider<GenericSourceSet> getSourceSet(SourceSetProjection projection) { | |
|
|
13 | return getSourceSet(projection.getName()); | |
|
|
14 | } | |
|
|
15 | } No newline at end of file | |
| @@ -0,0 +1,10 | |||
|
|
1 | package org.implab.gradle.variants.sources; | |
|
|
2 | ||
|
|
3 | import org.gradle.api.Named; | |
|
|
4 | ||
|
|
5 | /** | |
|
|
6 | * Represents a projected source set. This is an identity object and doesn't contain any state. | |
|
|
7 | * The name of the projection is used as the source set name by the {@link SourceSetMaterializer}. | |
|
|
8 | */ | |
|
|
9 | public interface SourceSetProjection extends Named { | |
|
|
10 | } | |
| @@ -0,0 +1,28 | |||
|
|
1 | package org.implab.gradle.variants.sources; | |
|
|
2 | ||
|
|
3 | import java.util.Set; | |
|
|
4 | ||
|
|
5 | import org.gradle.api.NamedDomainObjectCollection; | |
|
|
6 | ||
|
|
7 | /** | |
|
|
8 | * Registry of symbolic source set names produced by sources projection. | |
|
|
9 | * | |
|
|
10 | * Identity in this registry is the GenericSourceSet name. | |
|
|
11 | */ | |
|
|
12 | public interface SourceSetProjections { | |
|
|
13 | ||
|
|
14 | /** | |
|
|
15 | * Returns all source set projections. This is a separate | |
|
|
16 | */ | |
|
|
17 | NamedDomainObjectCollection<SourceSetProjection> getProjections(); | |
|
|
18 | ||
|
|
19 | /** | |
|
|
20 | * Returns all logical bindings projected into the given source set name. | |
|
|
21 | */ | |
|
|
22 | Set<VariantLayerBinding> getBindings(String sourceSetName); | |
|
|
23 | ||
|
|
24 | /** | |
|
|
25 | * Returns all logical bindings projected into the given source set name. | |
|
|
26 | */ | |
|
|
27 | Set<VariantLayerBinding> getBindings(SourceSetProjection projection); | |
|
|
28 | } No newline at end of file | |
| @@ -0,0 +1,27 | |||
|
|
1 | package org.implab.gradle.variants.sources; | |
|
|
2 | ||
|
|
3 | import org.implab.gradle.variants.model.Layer; | |
|
|
4 | import org.implab.gradle.variants.model.Variant; | |
|
|
5 | ||
|
|
6 | /** | |
|
|
7 | * Logical usage of a layer inside a variant. | |
|
|
8 | * Identity: (variantName, layerName) | |
|
|
9 | */ | |
|
|
10 | public interface VariantLayerBinding { | |
|
|
11 | Variant getVariant(); | |
|
|
12 | Layer getLayer(); | |
|
|
13 | ||
|
|
14 | public static VariantLayerBinding of(Variant variant, Layer layer) { | |
|
|
15 | return new VariantLayerBinding() { | |
|
|
16 | @Override | |
|
|
17 | public Variant getVariant() { | |
|
|
18 | return variant; | |
|
|
19 | } | |
|
|
20 | ||
|
|
21 | @Override | |
|
|
22 | public Layer getLayer() { | |
|
|
23 | return layer; | |
|
|
24 | } | |
|
|
25 | }; | |
|
|
26 | } | |
|
|
27 | } No newline at end of file | |
| @@ -0,0 +1,7 | |||
|
|
1 | package org.implab.gradle.variants.sources; | |
|
|
2 | ||
|
|
3 | public interface VariantSourcesContext { | |
|
|
4 | SourceSetProjections getProjections(); | |
|
|
5 | ||
|
|
6 | SourceSetMaterializer getMaterializer(); | |
|
|
7 | } | |
| @@ -0,0 +1,37 | |||
|
|
1 | package org.implab.gradle.variants.sources; | |
|
|
2 | ||
|
|
3 | import org.gradle.api.Action; | |
|
|
4 | import org.gradle.api.NamedDomainObjectContainer; | |
|
|
5 | import org.implab.gradle.common.core.lang.Closures; | |
|
|
6 | ||
|
|
7 | import groovy.lang.Closure; | |
|
|
8 | ||
|
|
9 | public interface VariantSourcesExtension { | |
|
|
10 | ||
|
|
11 | /** | |
|
|
12 | * Projection rules keyed by layer name. | |
|
|
13 | */ | |
|
|
14 | NamedDomainObjectContainer<LayerProjectionRule> getLayerRules(); | |
|
|
15 | ||
|
|
16 | /** | |
|
|
17 | * Creates or returns an existing layer rule and configures it. | |
|
|
18 | */ | |
|
|
19 | default LayerProjectionRule layerRule(String name) { | |
|
|
20 | return getLayerRules().maybeCreate(name); | |
|
|
21 | } | |
|
|
22 | ||
|
|
23 | /** | |
|
|
24 | * Creates or returns an existing layer rule and configures it. | |
|
|
25 | */ | |
|
|
26 | default LayerProjectionRule layer(String name, Action<? super LayerProjectionRule> action) { | |
|
|
27 | LayerProjectionRule rule = layerRule(name); | |
|
|
28 | action.execute(rule); | |
|
|
29 | return rule; | |
|
|
30 | } | |
|
|
31 | ||
|
|
32 | void whenFinalized(Action<? super VariantSourcesContext> action); | |
|
|
33 | ||
|
|
34 | default void whenFinalized(Closure<?> closure) { | |
|
|
35 | whenFinalized(Closures.action(closure)); | |
|
|
36 | } | |
|
|
37 | } No newline at end of file | |
General Comments 0
You need to be logged in to leave comments.
Login now
