Büyük Angular Uygulamasında Sınırlar: Feature Folder, Facade ve Katmanlar
Büyük bir Angular uygulamasında kodu feature'a göre klasörlemek, her feature'ı ui, state, data ve domain katmanlarına ayırmak ve feature'lar arası import'u yalnız public API (index.ts) üzerinden, lint ile zorlanan kurallarla serbest bırakmak gerekir; derleyici bu sınırları bilmez. Üç kestirmenin ng build'den geçip eslint-plugin-boundaries'e takıldığını gerçek çıktıyla gösteriyor, facade service'i ve API client katmanını işliyoruz.
Sürüm notu: Bu yazıdaki örnekler Angular 22 ve TypeScript ile denenmiştir.
Önce Tahmin Et
Bookstore'un Angular uygulaması büyüdü ve iki ekip var: biri Catalog'dan (kitap listesi, kitap detayı), diğeri Orders'tan (sepet, ödeme) sorumlu. Bir sprint içinde iki küçük istek geliyor. Catalog ekibi, liste sayfasında "3 adet alırsan" fiyatını göstermek istiyor ve Orders'taki fiyat kuralını doğrudan import ediyor:
import { Component, inject } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
import { toSignal } from '@angular/core/rxjs-interop';
import { priceCart } from '../../orders/domain/pricing';
import type { Book } from '../domain/book';
import { CatalogLookup } from '../state/catalog-lookup';
@Component({
selector: 'app-book-list-page',
imports: [CurrencyPipe],
template: `
@for (book of books(); track book.id) {
<p>{{ book.title }} · 3 copies: {{ threeCopies(book) | currency: 'EUR' }}</p>
}
`,
})
export class BookListPage {
protected readonly books = toSignal(inject(CatalogLookup).listBooks(), { initialValue: [] });
// Catalog, Orders'ın fiyat kuralını içeriden kullanıyor
protected threeCopies(book: Book): number {
return priceCart([{ bookId: book.id, title: book.title, unitPrice: book.price, quantity: 3 }]).total.amount;
}
}
Orders ekibi ise ödeme sayfasına "You may also like" listesi ekliyor; bunun için Catalog'un HTTP (Hypertext Transfer Protocol) client'ını içeriden çağırıyor, bir de acele bir düzeltme için siparişi facade'ı atlayarak doğrudan API (Application Programming Interface) client'tan gönderiyor:
export class CheckoutPage {
protected readonly facade = inject(CheckoutFacade);
// Kestirme 1: Catalog'un iç API client'ı
protected readonly suggestions = toSignal(inject(CatalogApi).getBooks(), { initialValue: [] });
// Kestirme 2: facade'ı atlayıp HTTP'ye doğrudan gitmek
private readonly api = inject(OrdersApi);
protected placeOrder(): void {
this.api.placeOrder(this.facade.cartLines()).subscribe(id => console.log(id));
}
}
Üç kestirme var: Catalog'un Orders'ın içine uzanması, Orders'ın Catalog'un içine uzanması ve bir component'in kendi feature'ındaki state katmanını atlaması. ng build bu kod için ne der? Pull request'te bu üç satırı kim yakalar? Ve altı ay sonra fiyat kuralını değiştirmek isteyen Orders ekibi, kuralın Catalog'da da kullanıldığını nereden öğrenir?
Derleyici Mimariyi Bilmez
Gerçek çıktı:
$ npx ng build
| Initial total | 254.26 kB | 69.70 kB
Application bundle generation complete. [0.774 seconds] - 2026-11-08T00:56:39.898Z
Build hatasız. TypeScript ve Angular derleyicisi için bir import, dosyanın var olup olmadığı ve tiplerin uyup uymadığı sorusudur; hangi klasörün hangisine bağımlı olabileceği bir derleyici kavramı değil. C#'ta en azından project referansları ve internal erişim belirleyicisi vardır; Modular Monolith serisinde module'lerin iç class'larını internal yaparak dışarıdan erişimi derleme anında engellemiştik. TypeScript'te export edilen her şey, doğru göreli yolu yazan herkese açık.
Bu yüzden üç kestirmenin üçü de bugün çalışıyor ve üçü de borç biriktiriyor. Catalog'un Orders'ın fiyat kuralını kullanması iki feature arasında iki yönlü bir bağımlılık yaratıyor: Orders zaten Catalog'dan kitap bilgisi alıyor, artık Catalog da Orders'a bağlı. Orders'ın Catalog'un HTTP client'ını kullanması, Catalog ekibinin backend'deki bir endpoint'i değiştirdiğinde kırılacak yerleri kendi feature'ının dışına taşıyor. Component'in facade'ı atlaması ise state'in iki yerden değiştirildiği bir uygulama demek: sipariş gönderildi ama facade'ın status'u hâlâ editing.
Feature'a Göre Klasörleme
Angular Style Guide proje yapısı için açık bir öneri veriyor: kodu uygulamanın feature'larına göre alt klasörlere ayır, components, services, directives gibi kodun tipine göre klasörler açma. Martin Fowler'ın 2015 tarihli "Presentation Domain Data Layering" yazısı aynı fikri katmanlar açısından söylüyor: küçük sistemlerde en üst seviyeyi teknik katmanlara göre bölmek kabul edilebilir, ama herhangi bir katman büyüdükçe en üst seviyeyi domain odaklı module'lere bölüp her birini kendi içinde katmanlamak gerekir. Bu, backend tarafında Vertical Slice Architecture serisindeki "layer'a değil feature'a göre" fikrinin ve Modular Monolith serisindeki module sınırlarının frontend'deki karşılığı.
Örnek uygulamanın yapısı (spec dosyaları hariç):
src/app/
catalog/
data/
catalog-api.ts
domain/
book.ts
state/
catalog-lookup.ts
ui/
book-list-page.ts
catalog.routes.ts
index.ts
orders/
data/
orders-api.ts
domain/
pricing.ts
state/
checkout-facade.ts
ui/
checkout-page.ts
index.ts
orders.routes.ts
shared/
util/
money.ts
app.config.ts
app.routes.ts
app.ts
Her feature kendi içinde dört klasöre sahip ve her birinin bir sorumluluğu var:
| Klasör | İçinde ne var | Neyi import edebilir |
|---|---|---|
ui | Sayfalar ve component'ler (container ve presentational) | Aynı feature'ın ui, state, domain klasörleri |
state | Facade'lar ve signal tutan service'ler | Aynı feature'ın state, data, domain klasörleri; izin verilen feature'ların public API'si |
data | API client'lar, DTO (Data Transfer Object) tipleri ve dönüşümleri | Aynı feature'ın data, domain klasörleri |
domain | Saf tipler ve iş kuralları | Yalnız aynı feature'ın domain'i |
| Feature kökü | index.ts ve route tanımı: feature'ın public API'si | Feature'ın kendi klasörleri |
Feature'ın dışarıya açtığı her şey index.ts'te duruyor:
// Catalog feature'ının public API'si: dışarıdan yalnız buradan import edilir
export { catalogRoutes } from './catalog.routes';
export type { Book } from './domain/book';
export { CatalogLookup } from './state/catalog-lookup';
Uygulamanın kök route'ları feature'ları yalnız bu public API üzerinden ve lazy loading ile yüklüyor. Orders'ın route tanımı ise facade'ı route seviyesinde provide ediyor; sayfadan çıkınca sepetin state'i de gidiyor:
import type { Routes } from '@angular/router';
export const routes: Routes = [
{ path: 'catalog', loadChildren: () => import('./catalog').then(m => m.catalogRoutes) },
{ path: 'checkout', loadChildren: () => import('./orders').then(m => m.ordersRoutes) },
{ path: '', pathMatch: 'full', redirectTo: 'catalog' },
];
import type { Routes } from '@angular/router';
import { CheckoutFacade } from './state/checkout-facade';
export const ordersRoutes: Routes = [
{
path: '',
// Facade route seviyesinde: sayfadan çıkınca state de gider
providers: [CheckoutFacade],
loadComponent: () => import('./ui/checkout-page').then(m => m.CheckoutPage),
},
];
index.ts, Modular Monolith serisinde gördüğümüz Contracts project'inin frontend'deki hafif karşılığı. Ama tek başına bir şey zorlamıyor; derleyici ../../catalog/data/catalog-api yolunu da aynı rahatlıkla kabul ediyor. Kuralın bir araca ihtiyacı var.
Sınırları Lint ile Zorlamak
ESLint için yazılmış eslint-plugin-boundaries (bu yazıda 7.4.2), dosyaları klasör pattern'lerine göre "element"lere ayırıp hangi elementin hangisini import edebileceğini policy'lerle tanımlamanı sağlıyor. Yukarıdaki tablonun kod hâli:
import boundaries from 'eslint-plugin-boundaries';
import tseslint from 'typescript-eslint';
export default [
{
files: ['src/app/**/*.ts'],
ignores: ['**/*.spec.ts'],
languageOptions: { parser: tseslint.parser },
plugins: { boundaries },
settings: {
'import/resolver': { typescript: { alwaysTryTypes: true } },
'boundaries/elements': [
{ type: 'shared', pattern: 'src/app/shared/*', capture: ['kind'] },
{ type: 'ui', pattern: 'src/app/*/ui', capture: ['feature'] },
{ type: 'state', pattern: 'src/app/*/state', capture: ['feature'] },
{ type: 'data', pattern: 'src/app/*/data', capture: ['feature'] },
{ type: 'domain', pattern: 'src/app/*/domain', capture: ['feature'] },
// Feature kökündeki dosyalar (index.ts, *.routes.ts): public API
{ type: 'public', pattern: 'src/app/*', capture: ['feature'] },
{ type: 'app', pattern: 'src/app' },
],
},
rules: {
'boundaries/dependencies': [2, {
default: 'disallow',
policies: [
// Herkes shared'i kullanabilir; shared yalnız kendini
{ allow: { to: { element: { type: 'shared' } } } },
{ from: { element: { type: 'app' } }, allow: { to: { element: { type: 'public' } } } },
// Feature içi katmanlar: ui → state → data → domain
{
from: { element: { type: 'public' } },
allow: { to: { element: { types: ['ui', 'state', 'data', 'domain', 'public'], captured: { feature: '{{ from.element.captured.feature }}' } } } },
},
{
from: { element: { type: 'ui' } },
allow: { to: { element: { types: ['ui', 'state', 'domain'], captured: { feature: '{{ from.element.captured.feature }}' } } } },
},
{
from: { element: { type: 'state' } },
allow: { to: { element: { types: ['state', 'data', 'domain'], captured: { feature: '{{ from.element.captured.feature }}' } } } },
},
{
from: { element: { type: 'data' } },
allow: { to: { element: { types: ['data', 'domain'], captured: { feature: '{{ from.element.captured.feature }}' } } } },
},
{
from: { element: { type: 'domain' } },
allow: { to: { element: { type: 'domain', captured: { feature: '{{ from.element.captured.feature }}' } } } },
},
// Feature'lar arası tek izin: orders, catalog'un public API'sini kullanabilir
{
from: { element: { types: ['ui', 'state'], captured: { feature: 'orders' } } },
allow: { to: { element: { type: 'public', captured: { feature: 'catalog' } } } },
},
],
}],
},
},
];
Birkaç detay önemli. capture, yoldaki feature adını yakalıyor ve policy'lerde {{ from.element.captured.feature }} ile "aynı feature" demeyi mümkün kılıyor. Varsayılan disallow; yani izin verilmeyen her bağımlılık hata. Feature'lar arası tek izin açıkça yazılmış: Orders, Catalog'un public API'sini kullanabilir, tersi değil. TypeScript import'larını çözebilmesi için eslint-import-resolver-typescript gerekiyor; bu ayar olmadan plugin import'ları çözemedi ve hiçbir ihlali raporlamadı, yani "lint temiz" sonucunun bir anlamı yoktu. Kuralı kurduktan sonra bilerek bir ihlal ekleyip yakalandığını görmek bu yüzden önemli.
Açılıştaki kod ile:
$ npx eslint src/app
src/app/catalog/ui/book-list-page.ts
4:27 error There is no policy allowing dependencies from elements of type "ui" and captured values: feature="catalog" to elements of type "domain" and captured values: feature="orders" boundaries/dependencies
src/app/orders/ui/checkout-page.ts
4:28 error There is no policy allowing dependencies from elements of type "ui" and captured values: feature="orders" to elements of type "data" and captured values: feature="catalog" boundaries/dependencies
5:27 error There is no policy allowing dependencies from elements of type "ui" and captured values: feature="orders" to elements of type "data" and captured values: feature="orders" boundaries/dependencies
✖ 3 problems (3 errors, 0 warnings)
Üç kestirmenin üçü de import satırında, mesajında hangi elementten hangisine gidildiği yazan bir hatayla yakalanıyor. Kod düzeltildikten sonra:
$ npm run lint
> bookstore09@0.0.0 lint
> eslint src/app
(exit code: 0)
Bu kontrol CI'da (Continuous Integration) build ile birlikte çalıştığında, kural code review'da birinin dikkatine kalmıyor. Nx kullanan monorepo'larda aynı işi @nx/enforce-module-boundaries kuralı project tag'leriyle yapıyor; araç değişiyor, fikir aynı: bağımlılık yönü bir karar, kararın bir yerde yazılı ve otomatik kontrol edilir olması gerekiyor.
Facade Service
Facade, GoF (Gang of Four: Gamma, Helm, Johnson ve Vlissides'in 1994 tarihli Design Patterns kitabı) kataloğundaki anlamıyla karmaşık bir alt sisteme tek, sade bir kapı açmak; Design Patterns serisindeki Facade Pattern yazısında .NET tarafını görmüştük. Angular'da bu kapının arkasında genelde state yönetimi ve HTTP var:
import { Injectable, computed, inject, signal } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { CatalogLookup } from '../../catalog';
import { OrdersApi } from '../data/orders-api';
import { type CartLine, priceCart } from '../domain/pricing';
type CheckoutStatus = 'editing' | 'placing' | 'placed' | 'failed';
// Component'in gördüğü tek şey: signal'lar ve niyet bildiren method'lar
@Injectable()
export class CheckoutFacade {
private readonly catalog = inject(CatalogLookup);
private readonly api = inject(OrdersApi);
private readonly lines = signal<readonly CartLine[]>([]);
private readonly status = signal<CheckoutStatus>('editing');
private readonly orderId = signal<string | null>(null);
readonly cartLines = this.lines.asReadonly();
readonly checkoutStatus = this.status.asReadonly();
readonly placedOrderId = this.orderId.asReadonly();
readonly totals = computed(() => priceCart(this.lines()));
readonly canPlaceOrder = computed(() => this.lines().length > 0 && this.status() === 'editing');
async addBook(bookId: number): Promise<void> {
const book = await firstValueFrom(this.catalog.findBook(bookId));
this.lines.update(lines => [...lines, { bookId, title: book.title, unitPrice: book.price, quantity: 1 }]);
}
changeQuantity(bookId: number, quantity: number): void {
const clamped = Math.min(Math.max(quantity, 1), 10);
this.lines.update(lines => lines.map(l => (l.bookId === bookId ? { ...l, quantity: clamped } : l)));
}
async placeOrder(): Promise<void> {
if (!this.canPlaceOrder()) return;
this.status.set('placing');
try {
this.orderId.set(await firstValueFrom(this.api.placeOrder(this.lines())));
this.status.set('placed');
} catch {
this.status.set('failed');
}
}
}
Component'in gördüğü yüzey küçük: birkaç salt okunur signal ve niyet bildiren dört method. Kitap bilgisinin Catalog'dan nasıl geldiği, siparişin hangi endpoint'e hangi gövdeyle gittiği, fiyatın nasıl hesaplandığı facade'ın arkasında. Sayfa artık yalnız facade'ı inject ediyor:
import { Component, inject } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
import { CheckoutFacade } from '../state/checkout-facade';
@Component({
selector: 'app-checkout-page',
imports: [CurrencyPipe],
template: `
@for (line of facade.cartLines(); track line.bookId) {
<p>{{ line.title }} x{{ line.quantity }}</p>
}
<p class="total">{{ facade.totals().total.amount | currency: 'EUR' }}</p>
<button [disabled]="!facade.canPlaceOrder()" (click)="facade.placeOrder()">Place order</button>
@if (facade.placedOrderId(); as id) {
<p class="confirmation">Order {{ id }} placed</p>
}
`,
})
export class CheckoutPage {
// Ne HttpClient ne store: yalnız facade
protected readonly facade = inject(CheckoutFacade);
}
Bunun iki karşılığı var. Birincisi test: sayfanın testi facade'ın yerine signal'lardan oluşan sahte bir object veriyor, HTTP ya da Catalog hiç devreye girmiyor. İkincisi değişebilirlik: yarın facade'ın içini NgRx Signal Store'a taşırsan, ya da sepeti sunucuda tutmaya karar verirsen, sayfanın tek satırı değişmiyor. Facade'ın kendi testi ise bütün zinciri gerçek HTTP katmanıyla (sahte backend'e karşı) doğruluyor:
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { CheckoutFacade } from './checkout-facade';
describe('CheckoutFacade', () => {
it('adds a book from Catalog and places the order', async () => {
TestBed.configureTestingModule({ providers: [CheckoutFacade, provideHttpClient(), provideHttpClientTesting()] });
const facade = TestBed.inject(CheckoutFacade);
const http = TestBed.inject(HttpTestingController);
const adding = facade.addBook(1);
http.expectOne('/api/catalog/books/1').flush({
id: 1, title: 'The Hobbit', authorName: 'J. R. R. Tolkien', listPrice: { amount: 14, currency: 'EUR' },
});
await adding;
facade.changeQuantity(1, 5);
const placing = facade.placeOrder();
const request = http.expectOne('/api/orders');
console.log(`body=${JSON.stringify(request.request.body)} total=${facade.totals().total.amount}`);
request.flush({ orderId: 'ORD-1001', total: 63, currency: 'EUR' });
await placing;
expect(facade.checkoutStatus()).toBe('placed');
expect(facade.placedOrderId()).toBe('ORD-1001');
});
});
$ npx ng test --watch=false --reporters=verbose
stdout | src/app/orders/state/checkout-facade.spec.ts > CheckoutFacade > adds a book from Catalog and places the order
body={"lines":[{"bookId":1,"quantity":5}]} total=63
✓ |bookstore09| src/app/orders/domain/pricing.spec.ts > priceCart > applies 10 percent from 3 copies
✓ |bookstore09| src/app/orders/state/checkout-facade.spec.ts > CheckoutFacade > adds a book from Catalog and places the order
✓ |bookstore09| src/app/orders/ui/checkout-page.spec.ts > CheckoutPage > renders whatever the facade exposes
Facade'ın bir sınırı da var. Her service'in önüne mekanik bir facade koymak, yalnız çağrıyı ileten ve hiçbir şeyi gizlemeyen bir katman üretir. Facade'ın değeri, arkasında gerçekten saklanmaya değer bir karmaşıklık olduğunda ortaya çıkıyor: birden çok kaynak, state geçişleri, hata durumları.
API Client Katmanı
data klasörü backend ile konuşan tek yer. Buradaki en önemli iş, backend'in sözleşmesini (DTO) uygulamanın kendi tiplerine çevirmek:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { type Observable, map } from 'rxjs';
import { money } from '../../shared/util/money';
import type { Book } from '../domain/book';
// Backend'in sözleşmesi: bu şekil yalnız bu dosyada yaşar
interface BookDto {
id: number;
title: string;
authorName: string;
listPrice: { amount: number; currency: string };
}
@Injectable({ providedIn: 'root' })
export class CatalogApi {
private readonly http = inject(HttpClient);
getBooks(): Observable<readonly Book[]> {
return this.http.get<BookDto[]>('/api/catalog/books').pipe(map(dtos => dtos.map(toBook)));
}
getBook(id: number): Observable<Book> {
return this.http.get<BookDto>(`/api/catalog/books/${id}`).pipe(map(toBook));
}
}
function toBook(dto: BookDto): Book {
return { id: dto.id, title: dto.title, author: dto.authorName, price: money(dto.listPrice.amount, dto.listPrice.currency) };
}
Backend authorName ve iç içe bir listPrice gönderiyor; uygulamanın geri kalanı author ve Money görüyor. Backend ekibi alan adını değiştirdiğinde düzeltilecek yer tek bir fonksiyon. DTO tipini component'lere kadar taşımak, backend'in her kararını UI'a (user interface) sızdırmak demek; bu, ASP.NET Core Web API serisinin DTO ve Mapping yazısında entity'yi doğrudan response olarak döndürmemek için verdiğimiz kararın aynısı, yalnız bu sefer istemci tarafında.
Domain Mantığını UI'dan Ayırmak
Serinin başından beri taşıdığımız indirim kuralı artık Orders'ın domain klasöründe, Angular'a, HTTP'ye ve ekrana dokunmayan bir fonksiyon:
import { type Money, money } from '../../shared/util/money';
export interface CartLine {
readonly bookId: number;
readonly title: string;
readonly unitPrice: Money;
readonly quantity: number;
}
export interface CartTotals {
readonly subtotal: Money;
readonly discount: Money;
readonly total: Money;
}
// İş kuralı: Angular'dan, HTTP'den ve ekrandan bağımsız
export function priceCart(lines: readonly CartLine[]): CartTotals {
const subtotal = lines.reduce((sum, l) => sum + l.unitPrice.amount * l.quantity, 0);
// 3 ve üzeri adet olan satırda yüzde 10 indirim
const discount = lines.reduce((sum, l) => sum + (l.quantity >= 3 ? (l.unitPrice.amount * l.quantity) / 10 : 0), 0);
return { subtotal: money(subtotal), discount: money(discount), total: money(subtotal - discount) };
}
Burada dürüst olmak gerekiyor: frontend'deki domain mantığı, backend'deki asıl kuralın bir kopyası. Sipariş tutarının doğrusunu sunucu hesaplamalı ve doğrulamalı; istemcideki hesap kullanıcıya anında geri bildirim vermek için var. Bu kopyayı bilinçli tutmanın yolu, onu tek bir yere (bu dosya) koymak ve sunucunun döndürdüğü tutarla farklı çıktığında hangisinin kazandığını açıkça kararlaştırmak. Kural birden çok component'in template'ine dağılmışsa, backend'deki bir değişikliğin frontend'de kaç yere dokunduğunu kimse bilmez.
Micro-Frontend'e Kısa Bakış
Sınırları çizdikten sonra akla gelen soru: feature'ları ayrı uygulamalara bölüp ayrı ayrı deploy etmek mantıklı mı? Cam Jackson'ın 2019'da martinfowler.com'da yayımlanan "Micro Frontends" yazısı bunu, bağımsız olarak teslim edilebilen frontend uygulamalarının daha büyük bir bütün içinde birleştirildiği bir mimari tarz olarak tanımlıyor. Angular dünyasında bu genelde Module Federation ya da Native Federation ile yapılıyor.
Aynı yazı bedelleri de sayıyor: ayrı build edilen paketlerin ortak bağımlılıkları tekrar indirmesiyle büyüyen payload, geliştirme ortamı ile production arasındaki farklar, daha fazla repository, pipeline ve sunucu ile artan operasyon ve yönetişim yükü. Bu bedel, ekiplerin gerçekten bağımsız deploy etmesi gerektiğinde (farklı yayın takvimleri, farklı organizasyonlar, ayrı ayrı ölçeklenen ürünler) ödenmeye değer. İki ekibin aynı repository'de çalıştığı, birlikte deploy ettiği bir uygulamada ise bu yazıdaki feature sınırları ve lint kuralları, micro-frontend'in getirdiği izolasyonun büyük kısmını bedelinin çok azına veriyor. Önce modüler bir monolith kur; sınırlar bir yıl boyunca tutuyorsa ve deploy bağımsızlığı gerçek bir ihtiyaçsa bölmeyi düşün. Backend'deki Modular Monolith ve Microservices tartışmasıyla aynı mantık.
Sık Düşülen Tuzaklar
Her şeyi shared'e atmak. İki feature'ın ihtiyaç duyduğu her şeyin shared'e taşındığı bir projede shared, kimsenin sahiplenmediği ve herkesin bağımlı olduğu en büyük klasöre dönüşür. shared'de yalnız domain bilgisi taşımayan şeyler kalsın: Money gibi temel tipler, genel UI parçaları, yardımcı fonksiyonlar. Bir kavram iki feature'da da anlam taşıyorsa, hangisinin sahibi olduğuna karar verip öbürüne public API ile aç.
Lint kurallarının çalıştığını doğrulamamak. Resolver eksikken plugin hiçbir ihlali raporlamadı ve sonuç "temiz" göründü. Kuralı kurduktan sonra bilerek bir ihlal ekle ve yakalandığını gör.
Public API'yi her şeyi export eden bir index.ts'e çevirmek. export * from './data/catalog-api' gibi satırlar sınırı kâğıt üzerinde bırakır. index.ts, öbür feature'ların gerçekten ihtiyaç duyduğu en küçük yüzeyi açmalı.
Feature'lar arası bağımlılığı iki yönlü yapmak. Orders Catalog'u kullanıyorsa, Catalog Orders'ı kullanmamalı. İki feature birbirine ihtiyaç duyuyorsa ya sınır yanlış çizilmiştir ya da ortak kavram üçüncü bir yere ait. Lint policy'sinde feature'lar arası izinleri açıkça, tek yönlü yazmak bunu görünür kılıyor.
Facade'ı god service'e çevirmek. Uygulamanın tek bir AppFacade'ı varsa ve her sayfa onu kullanıyorsa, sınırları kaldırıp tek bir yere toplamışsın demektir. Facade, bir feature'ın ya da bir sayfanın kapısı olmalı.
Küçük Bir Deneme
★☆☆ catalog'a bir kitap detay sayfası ekle ve ui, state, data klasörlerine dağıt. npm run lint çalıştır; sonra detay sayfasından CatalogApi'yi doğrudan inject edip hatanın mesajını oku.
★★☆ Catalog'un "3 adet alırsan" ihtiyacını sınırları bozmadan çöz. Fiyat kuralı kimin? Orders'ın index.ts'inden bir fonksiyon açmak, kuralı Catalog'a taşımak ve backend'den hazır fiyat istemek seçeneklerini, lint policy'sinde hangi satırı değiştirmen gerektiğiyle birlikte karşılaştır.
★★★ Üçüncü bir feature ekle: account (müşterinin geçmiş siparişleri). Orders'ın public API'sinden ne açılmalı, Account'tan Orders'a mı yoksa Orders'tan Account'a mı izin verilmeli? Kararını policy'ye yaz, bilerek ters yönde bir import ekleyip lint'in yakaladığını gör.
Bu yazıyla UI Mimari Pattern'leri serisi tamamlandı. Separated Presentation fikriyle başladık, MVC (Model-View-Controller), MVP (Model-View-Presenter) ve MVVM (Model-View-ViewModel) ile view'ı mantıktan ayırmanın üç farklı yolunu gördük, Angular'ın bu pattern'lerin neresinde durduğunu konuştuk, Flux ile veriyi tek yönde akıttık, Angular'da bu akışı signal'lar ve service'lerle kurduk, component ağacını container ve presentational parçalara böldük ve son olarak feature'lar arasındaki sınırları lint ile zorladık. Her adımda aynı soru vardı: bu kodu kim değiştirecek ve değiştirdiğinde neyi kırar? NgRx'in Store, Effects ve Signal Store ile tam anlatımı ilerideki Frontend kategorisindeki Angular'da State Yönetimi serisinde, component ve facade testlerinin derinlemesine anlatımı ise Test ve Kalite kategorisindeki Frontend Testleri serisinde seni bekliyor.