조수 표면을 앱에 붙이는 패널이 독립적으로 세 번 자랐어. 나중에 전체 트리를 훑어서 계보가 정확히 셋이라는 게 확인됐지. 이 강의는 그중 두 번째가 결정 대상이 된 순간에 대한 거야. 한 관례는 공용 저장소로 승격돼 있었어. 두 번째는 가족의 다른 구석에서 자라서 앱 셋에 손으로 복사됐고. 목적은 같고 기계는 달랐어. 하나는 호스트가 감싸는 레이아웃 형제로 패널을 그렸고, 다른 하나는 밀어내기가 아예 없는 맨 고정 오버레이로 그렸어. 그러니까 내용을 옆으로 밀지 않고 덮어버렸지.
세 번째 복사본에서, 이 가족 자기 휴리스틱으로 보면 두 번째 관례는 승격을 벌었어. 네 번째 채택자가 있었는데 그땐 아무도 몰랐고. 그게 어떻게 드러났는지는 3 강이 다뤄.
맞는 규칙 안에 든 함정
여기가 재밌는 대목이야. 셋의 규칙을 기계적으로 적용하면 국소적으로 맞고 전체적으로 터무니없는 답이 나와. 승격시키면 이제 중복을 없애려고 존재하는 저장소가 같은 목적에 다른 기계를 가진 컴포넌트 둘을 품게 돼. 둘 다 공용 층의 권위를 달고. 패널에 대한 앞으로의 모든 변경을 두 번 해야 해. 새 앱마다 골라야 하고. 중복이 없어진 게 아니라 공식화된 거야.
결정은 대신 합병이었어. 이미 있는 공용 껍데기를 채택하고, 복사본들을 퇴역시키고, 두 번째 계보를 통째로 끝내는 것. 앱 넷이 하루에 변환됐고, 퇴역한 관례의 파일은 가족 어디에도 안 남았어.
패턴을 승격시키기 전에, 공용 층이 이미 같은 목적의 뭔가를 쥐고 있는지 물어. 쥐고 있으면 두 번째를 들이지 말고 합병해. 셋의 규칙은 "이게 추출을 벌었나"에 답해. "이게 이미 추출된 것과 같은 일인가"엔 안 답하고. 둘은 다른 질문이야. 중복 제거가 목적인 저장소가 중복이 제일 비싸고 제일 안 보이는 자리야. 사본 둘 다 정당해 보이거든.
합병이 치른 값, 그리고 드러낸 것
채택은 공짜가 아니었고, 뭘 치렀는지는 이 합병에 고유해. 변환하는 앱 넷 중 하나가 자기 컨트롤을 툴팁으로 설명해. 컨트롤 옆에 형제 노드를 만들어내는 컴포넌트인데, 그건 컨트롤에 속성을 아무리 얹어도 표현이 안 돼. 그래서 공용 껍데기가 두 번째 주입 모양을 길렀어. 컨트롤을 앱 자기 노드로 감싸기. 이미 있던 것 (컨트롤에 속성 병합하기) 옆에. 둘 다 선택이고, 둘 다 안 쓰는 소비자는 안 건드려졌어.
이게 챙겨갈 패턴이야. 앱들이 공용 층으로 수렴하는 게, 공용 층이 자기한테 뭐가 없었는지 알아내는 방법이기도 해. 합병은 한 방향이 아냐. 변환된 앱은 공용 판본만큼 오래 같은 문제를 풀어왔고, 그중 한 부분을 공용 판본이 표현 못 하는 방식으로 풀어놨어.
그다음 날 퇴역한 세 번째 계보, 그러니까 같은 수렴의 나중 물결은 앱마다 훨씬 비쌌어. 다음 트랙이 그걸 전부 값 매기고. 조용히 사라질 뻔한 권한, 프레임 위의 구역, 그리고 한 앱이 모두한테 돌려준 배치 처방.
Code
계보 둘, 그리고 세 번째 복사본에서의 결정·text
BEFORE - two conventions, same job
shared chrome second convention (hand-copied)
├── app A ├── app D ORIGIN. A bare fixed overlay
├── app B │ with no packing at all, so it
└── app C │ COVERED the content - a live
│ violation of the family's own
│ rule, in the app that started
│ the convention
├── app E flex sibling, host wraps it
├── app F flex sibling
└── app G a FOURTH adopter, unknown
until a full-tree sweep found
it AFTER the first three were
converted (lesson 3's story)
At app F the second convention hit its third copy. By the rule of
three, it has earned promotion.
THE MECHANICAL ANSWER (locally correct, globally absurd)
shared repo
├── SharedPanel <- for apps A, B, C
└── SecondPanel <- for apps D, E, F
Two components, one purpose, both authoritative. Every future
change made twice; every new app forced to choose. Duplication
formalized, not removed.
THE ANSWER TAKEN
shared repo
└── SharedPanel <- ALL SEVEN, second lineage extinct
What THIS merge cost, per converting app:
app D converting FIXED a live violation - its bare overlay had
been covering the host content
app E its flex-sibling host wrapper dropped for the shared shell
app F same
app G same, converted last, after the sweep surfaced it
What THIS merge grew in the shared component: ONE seam. App D
explains its controls with a tooltip, which renders a SIBLING node
beside the control - and no number of attributes ON the control can
express that. So the chrome gained a second injection shape (wrap
the control in the app's own node) beside the one it had (merge
attributes onto it).
A LATER WAVE of the same convergence - the third lineage, retired
the next day - cost far more per app. Track 8 prices it in full.