aidoc.uz

6-qism · Tamoyillar va ichki mexanizm·15-bob

Clean Code, SOLID va DRY tamoyillari

Kod yozish qobiliyatidan kod loyihalash qobiliyatiga o'tish — Senior Flutter intervyularida eng ko'p tekshiriladigan mavzu: aniq nomlash, DRY'ning haqiqiy ma'nosi va SOLID'ning har bir harfi Dart/Flutter kodida qanday ko'rinishini oladi.

Junior dasturchi "ishlaydigan kod" yozadi. Middle dasturchi "to'g'ri ishlaydigan kod" yozadi. Senior dasturchi esa olti oydan keyin boshqa odam (yoki o'zi) tushunib, xavfsiz o'zgartira oladigan kod yozadi. Intervyularda SOLID va DRY haqidagi savollar ko'pincha aynan shu farqni tekshirish uchun beriladi — nomlarni yodlab olish emas, balki real kod bo'lagini ko'rib, "bu yerda nima yomon va nega" deb tushuntira olish kutiladi. Bu bobda har bir tamoyilni mavhum ta'rifdan emas, aynan Flutter loyihasida tez-tez uchraydigan holatlardan boshlab ko'rib chiqamiz.

1. Clean Code — nomlash, funksiyalar va formatlash#

Toza kodning katta qismi — hech qanday "aqlli" texnika emas, balki intizom. Uch narsa eng ko'p ta'sir qiladi: nomlash, funksiya hajmi va ichma-ichlik chuqurligi.

Nomlash — kod o'z-o'zini tushuntirishi kerak:

// YOMON — nima ekanini bilish uchun implementatsiyani o'qish kerak
bool check(User u) {
  return u.a > 18 && u.s == true;
}
 
// YAXSHI — funksiya nomi va parametr nomlari o'zi hujjat vazifasini bajaradi
bool isEligibleForPurchase(User user) {
  return user.age >= 18 && user.hasVerifiedEmail;
}

Qisqa nom yozish tezroq tuyuladi, lekin uni o'qish vaqti — yozish vaqtidan har doim ko'p marta ko'p bo'ladi (kod bir marta yoziladi, o'nlab marta o'qiladi). a, s, tmp, data2 kabi nomlar — kelajakdagi o'qiydigan odamga (ko'pincha shu siz, olti oydan keyin) qoldirilgan qarz.

Funksiyalar — bitta narsani qilishi kerak:

// YOMON — bitta funksiya validatsiya, hisob-kitob va UI yangilashni aralashtiradi
void submitOrder() {
  if (cartItems.isEmpty) {
    showDialog(context: context, builder: (_) => const AlertDialog(title: Text('Bo\'sh')));
    return;
  }
  double total = 0;
  for (final item in cartItems) {
    total += item.price * item.quantity;
    if (item.hasDiscount) {
      total -= item.price * item.quantity * 0.1;
    }
  }
  if (total > 1000000) {
    total *= 0.95; // katta buyurtmaga chegirma
  }
  setState(() {
    orderTotal = total;
    isSubmitting = true;
  });
  api.submitOrder(cartItems, total);
}
 
// YAXSHI — har bir funksiya bitta savolga javob beradi
double calculateOrderTotal(List<CartItem> items) {
  final subtotal = items.fold<double>(0, (sum, item) => sum + _itemTotal(item));
  return _applyBulkDiscount(subtotal);
}
 
double _itemTotal(CartItem item) {
  final base = item.price * item.quantity;
  return item.hasDiscount ? base * 0.9 : base;
}
 
double _applyBulkDiscount(double subtotal) {
  return subtotal > 1000000 ? subtotal * 0.95 : subtotal;
}
 
Future<void> submitOrder() async {
  if (cartItems.isEmpty) {
    _showEmptyCartError();
    return;
  }
  final total = calculateOrderTotal(cartItems);
  setState(() => isSubmitting = true);
  await api.submitOrder(cartItems, total);
}

Ikkinchi versiyada calculateOrderTotal va _applyBulkDiscount'ni UI'siz, alohida unit test bilan tekshirish mumkin — birinchi versiyada esa hisob-kitob mantig'ini sinash uchun butun widget'ni qurish kerak bo'ladi.

Ichma-ichlik chuqurligi va "early return":

// YOMON — 4 daraja ichma-ichlik, mantiqni kuzatish qiyin
String getDiscountLabel(User? user) {
  if (user != null) {
    if (user.isPremium) {
      if (user.yearsActive > 2) {
        if (user.hasNoRefunds) {
          return '20% chegirma';
        }
      }
    }
  }
  return 'Chegirma yo\'q';
}
 
// YAXSHI — "guard clause" bilan darhol chiqib ketish, asosiy mantiq tekis
String getDiscountLabel(User? user) {
  if (user == null) return 'Chegirma yo\'q';
  if (!user.isPremium) return 'Chegirma yo\'q';
  if (user.yearsActive <= 2) return 'Chegirma yo\'q';
  if (!user.hasNoRefunds) return 'Chegirma yo\'q';
  return '20% chegirma';
}

Izohlar (comment'lar) haqida: yaxshi kod ko'pincha izohsiz o'zini tushuntiradi. Izoh yozish kerak bo'lgan payt — **"nega"**ni tushuntirish kerak bo'lganda, "nima"ni emas:

// YOMON — kod nima qilayotganini qaytaradi, hech qanday qo'shimcha ma'lumot bermaydi
// i ni 1 ga oshiramiz
i++;
 
// YAXSHI — kodning o'zi ko'rinmaydigan sababni tushuntiradi
// API 0-based sahifalash ishlatadi, lekin UI 1-based ko'rsatadi — shu farqni bu yerda tuzatamiz
final apiPage = uiPage - 1;

Agar izoh "bu funksiya X qiladi" deb kod aytayotganini qaytarsa — bu izoh emas, bu kod yomon yozilganining belgisi (funksiya nomini yaxshiroq tanlash kerak edi).

2. DRY — bu "ko'chirma-qo'yish qilmang" degani emas#

DRY (Don't Repeat Yourself) tamoyili ko'pincha noto'g'ri tushuniladi — "bir xil ko'rinishdagi kod ikki joyda bo'lmasin" deb. Aslida DRY bilim (knowledge) takrorlanmasligi haqida, matn emas. Ikki joyda bir xil ko'rinadigan kod — agar ular turli sabablarga ko'ra o'zgarishi mumkin bo'lsa — aslida bir xil bilim emas, tasodifiy o'xshashlik, xolos.

// Ikkala funksiya ham "email formatini tekshiradi" — lekin turli sabab bilan
bool isValidLoginEmail(String email) => email.contains('@') && email.length > 5;
bool isValidNewsletterEmail(String email) => email.contains('@') && email.length > 5;

Bu ikkovini "DRY qilib" bitta isValidEmail()ga birlashtirish tuyulishi mumkin — lekin agar login uchun email tekshiruvi keyinchalik qattiqroq (masalan, domain whitelist) bo'lishi kerak bo'lsa-yu, newsletter uchun yumshoqroq qolishi kerak bo'lsa, bitta umumiy funksiya ikkala tomonni ham buzadigan o'zgarishga majbur qiladi. Bu yerda haqiqiy bilim bitta — "email @ belgisini o'z ichiga olishi kerak" — shuni birlashtirish to'g'ri, ammo "login uchun yaroqlilik qoidasi" va "newsletter uchun yaroqlilik qoidasi" — ikkita alohida bilim, ularni sun'iy ravishda bitta funksiyaga majburlash keyinchalik if (isLogin) {...} else {...} bilan to'lib ketadi.

Flutter'da duplikatsiyani olib tashlashning uch yo'li:

// 1. Widget ekstraksiyasi — bir xil UI bir necha joyda takrorlansa
class PriceTag extends StatelessWidget {
  const PriceTag({super.key, required this.amount, this.isDiscounted = false});
  final double amount;
  final bool isDiscounted;
 
  @override
  Widget build(BuildContext context) {
    return Text(
      '${amount.toStringAsFixed(0)} so\'m',
      style: TextStyle(
        color: isDiscounted ? Colors.red : Colors.black,
        decoration: isDiscounted ? TextDecoration.lineThrough : null,
      ),
    );
  }
}
 
// 2. Extension method — bir xil transformatsiya mantig'i takrorlansa
extension DateFormatting on DateTime {
  String toUzbekDate() => '$day.${month.toString().padLeft(2, '0')}.$year';
}
// Ishlatish: order.createdAt.toUzbekDate()
 
// 3. Mixin — bir nechta State klassida bir xil hulq-atvor takrorlansa
mixin LoadingStateMixin<T extends StatefulWidget> on State<T> {
  bool isLoading = false;
 
  Future<void> withLoading(Future<void> Function() action) async {
    setState(() => isLoading = true);
    try {
      await action();
    } finally {
      if (mounted) setState(() => isLoading = false);
    }
  }
}

Uchtasining farqi: widget ekstraksiyasi — vizual takrorlanish uchun; extension — mavjud tipga xulq-atvor qo'shish, lekin yangi holat (state) yo'q; mixin — bir nechta klassga umumiy holat va xulq-atvorni qo'shish kerak bo'lganda (compositiondan ko'ra ko'proq "aralashtirish"). Agar munosabat "is-a" bo'lsa (mixin haqiqatan ham shu klassning bir turi) — mixin; agar "has-a" yoki "can-do" bo'lsa — composition (obyektni maydon sifatida ushlab turish) ko'pincha to'g'riroq tanlov, chunki u testlash va almashtirishni osonlashtiradi.

3. SOLID — S: Single Responsibility Principle#

Bir klass — bitta o'zgarish sababiga ega bo'lishi kerak. Flutter'da bu ko'pincha "widget ham ma'lumot oladi, ham uni ko'rsatadi" muammosi sifatida uchraydi.

// YOMON — widget o'zi HTTP so'rov yuboradi, xatoni boshqaradi VA UI chizadi
class UserProfileScreen extends StatefulWidget {
  const UserProfileScreen({super.key});
  @override
  State<UserProfileScreen> createState() => _UserProfileScreenState();
}
 
class _UserProfileScreenState extends State<UserProfileScreen> {
  User? user;
  bool isLoading = true;
  String? error;
 
  @override
  void initState() {
    super.initState();
    http.get(Uri.parse('https://api.example.com/user')).then((response) {
      if (response.statusCode == 200) {
        setState(() {
          user = User.fromJson(jsonDecode(response.body));
          isLoading = false;
        });
      } else {
        setState(() {
          error = 'Xatolik yuz berdi';
          isLoading = false;
        });
      }
    });
  }
 
  @override
  Widget build(BuildContext context) {
    if (isLoading) return const CircularProgressIndicator();
    if (error != null) return Text(error!);
    return Text(user!.name);
  }
}

Bu klassning o'zgarish sabablari kamida uchta: (1) API endpoint yoki so'rov formati o'zgarsa, (2) xato ko'rsatish uslubi o'zgarsa, (3) UI dizayni o'zgarsa. Har uchalasi uchun ham bitta faylni tahrirlash kerak bo'ladi — bu SRP buzilishining belgisi.

// YAXSHI — ma'lumot olish mantig'i alohida, widget faqat holatga qarab chizadi
class UserRepository {
  Future<User> fetchCurrentUser() async {
    final response = await http.get(Uri.parse('https://api.example.com/user'));
    if (response.statusCode != 200) {
      throw UserFetchException('Foydalanuvchini yuklab bo\'lmadi');
    }
    return User.fromJson(jsonDecode(response.body));
  }
}
 
class UserProfileController extends ChangeNotifier {
  UserProfileController(this._repository);
  final UserRepository _repository;
 
  User? user;
  bool isLoading = true;
  String? error;
 
  Future<void> load() async {
    isLoading = true;
    notifyListeners();
    try {
      user = await _repository.fetchCurrentUser();
    } catch (e) {
      error = 'Xatolik yuz berdi';
    } finally {
      isLoading = false;
      notifyListeners();
    }
  }
}
 
class UserProfileScreen extends StatelessWidget {
  const UserProfileScreen({super.key, required this.controller});
  final UserProfileController controller;
 
  @override
  Widget build(BuildContext context) {
    if (controller.isLoading) return const CircularProgressIndicator();
    if (controller.error != null) return Text(controller.error!);
    return Text(controller.user!.name);
  }
}

Endi UserRepositoryni haqiqiy HTTP so'rovsiz sinash mumkin (mock bilan), UserProfileControllerni widget'siz sinash mumkin, va UserProfileScreen — sof funksiya kabi, faqat berilgan holatga qarab chiziladi (uni testWidgetsda holatni to'g'ridan-to'g'ri berib, tez sinash mumkin).

4. SOLID — O: Open/Closed Principle#

Kod kengaytirishga ochiq, o'zgartirishga yopiq bo'lishi kerak — yangi xulq-atvor qo'shish uchun mavjud, sinovdan o'tgan kodni tahrirlash shart bo'lmasligi kerak.

// YOMON — yangi to'lov turi qo'shilganda shu funksiyani har safar o'zgartirish kerak
double calculateFee(String paymentType, double amount) {
  if (paymentType == 'card') {
    return amount * 0.029;
  } else if (paymentType == 'bank') {
    return amount * 0.01;
  } else if (paymentType == 'crypto') {
    return amount * 0.015;
  }
  throw ArgumentError('Noma\'lum to\'lov turi');
}

Har safar yangi to'lov usuli qo'shilganda shu funksiya o'zgaradi — bu esa allaqachon ishlayotgan card va bank filiallarini ham qaytadan sinovdan o'tkazish xavfini keltirib chiqaradi.

// YAXSHI — strategy pattern: yangi turi = yangi klass, mavjud kod o'zgarmaydi
abstract class PaymentFeeStrategy {
  double calculateFee(double amount);
}
 
class CardFeeStrategy implements PaymentFeeStrategy {
  @override
  double calculateFee(double amount) => amount * 0.029;
}
 
class BankFeeStrategy implements PaymentFeeStrategy {
  @override
  double calculateFee(double amount) => amount * 0.01;
}
 
class CryptoFeeStrategy implements PaymentFeeStrategy {
  @override
  double calculateFee(double amount) => amount * 0.015;
}
 
// Yangi "InstallmentFeeStrategy" qo'shish uchun faqat yangi klass yoziladi,
// mavjud strategiyalarga tegilmaydi:
class FeeCalculator {
  FeeCalculator(this.strategy);
  final PaymentFeeStrategy strategy;
 
  double calculate(double amount) => strategy.calculateFee(amount);
}

5. SOLID — L: Liskov Substitution Principle#

Bazaviy klass ishlatilgan har qanday joyda, uning istalgan subtipini xavfsiz qo'yish mumkin bo'lishi kerak — subtip kutilgan xulq-atvorni "toraytirmasligi" yoki kutilmagan istisno (exception) tashlamasligi kerak.

// YOMON — subtip bazaviy klass shartnomasini buzadi
abstract class Bird {
  void fly();
}
 
class Sparrow extends Bird {
  @override
  void fly() => print('Chumchuq uchyapti');
}
 
class Penguin extends Bird {
  @override
  void fly() => throw UnsupportedError('Pingvin ucha olmaydi!'); // LSP buzilishi!
}
 
// Har qanday joyda "Bird b = ...; b.fly();" deb yozilgan kod Penguin bilan qulaydi —
// garchi Penguin ham "Bird" bo'lsa-da.
// YAXSHI — hierarxiya haqiqiy imkoniyatlarga mos quriladi
abstract class Bird {
  void eat();
}
 
abstract class FlyingBird extends Bird {
  void fly();
}
 
class Sparrow extends FlyingBird {
  @override
  void eat() => print('Chumchuq yeyapti');
  @override
  void fly() => print('Chumchuq uchyapti');
}
 
class Penguin extends Bird {
  @override
  void eat() => print('Pingvin yeyapti');
  // fly() umuman yo'q — chunki Penguin FlyingBird emas
}

Bu — mashhur "kvadrat-to'rtburchak" muammosining Flutter'dagi analogi. Amaliy Flutter holatida LSP buzilishi ko'pincha shunday ko'rinadi:

// YOMON — override qilingan metod bazaviy klassdan qattiqroq shart qo'yadi
class Repository {
  Future<List<Item>> fetchItems({int? limit}) async { /* ... */ return []; }
}
 
class CachedRepository extends Repository {
  @override
  Future<List<Item>> fetchItems({int? limit}) async {
    if (limit == null) {
      throw ArgumentError('CachedRepository uchun limit majburiy!'); // LSP buzilishi
    }
    // ...
    return [];
  }
}

Repository tipidagi o'zgaruvchiga CachedRepository obyektini berib, limitsiz chaqirgan har qanday chaqiruvchi kod kutilmaganda qulaydi — garchi Repository shartnomasi limitni ixtiyoriy deb e'lon qilgan bo'lsa-da.

Dart 3'ning sealed klasslari bu turdagi muammolarni compile vaqtida ushlashga yordam beradi — kompilyator barcha subtiplarni switchda hisobga olishni majburlaydi, shuning uchun yangi variant qo'shilganda kompilyator sizga "bu yerni ham yangilashni unutmadingizmi" deb eslatadi:

sealed class PaymentResult {}
class PaymentSuccess extends PaymentResult { final String txId; PaymentSuccess(this.txId); }
class PaymentFailure extends PaymentResult { final String reason; PaymentFailure(this.reason); }
class PaymentPending extends PaymentResult {}
 
String describe(PaymentResult result) => switch (result) {
  PaymentSuccess(:final txId) => 'Muvaffaqiyatli: $txId',
  PaymentFailure(:final reason) => 'Xato: $reason',
  PaymentPending() => 'Kutilmoqda',
  // Agar yangi PaymentResult subtipi qo'shilsa-yu shu yerga ishlov qo'shilmasa,
  // kompilyator "switch is not exhaustive" xatosini beradi — LSP'ni buzish qiyinlashadi.
};

6. SOLID — I: Interface Segregation Principle#

Klientlar ular foydalanmaydigan metodlarga bog'liq bo'lishga majburlanmasligi kerak — katta, "hamma narsani qamrab oluvchi" interfeys o'rniga bir nechta kichik, maqsadli interfeys yaxshiroq.

// YOMON — bitta katta interfeys, har bir implementatsiya kerak bo'lmagan metodlarni ham stub qiladi
abstract class Repository<T> {
  Future<List<T>> getAll();
  Future<T> getById(String id);
  Future<void> create(T item);
  Future<void> update(T item);
  Future<void> delete(String id);
  Future<void> syncWithServer();
  Future<void> clearCache();
}
 
// Faqat o'qish uchun kerak bo'lgan "TagRepository" ham yozish/o'chirish metodlarini
// stub qilishga majbur bo'ladi:
class TagRepository implements Repository<Tag> {
  @override
  Future<List<Tag>> getAll() async => [];
  @override
  Future<Tag> getById(String id) async => throw UnimplementedError();
  @override
  Future<void> create(Tag item) async => throw UnsupportedError('Tag faqat o\'qiladi');
  @override
  Future<void> update(Tag item) async => throw UnsupportedError('Tag faqat o\'qiladi');
  @override
  Future<void> delete(String id) async => throw UnsupportedError('Tag faqat o\'qiladi');
  @override
  Future<void> syncWithServer() async {}
  @override
  Future<void> clearCache() async {}
}
// YAXSHI — kichik, maqsadli interfeyslar, kerak bo'lganlarigina implement qilinadi
abstract class ReadableRepository<T> {
  Future<List<T>> getAll();
  Future<T> getById(String id);
}
 
abstract class WritableRepository<T> {
  Future<void> create(T item);
  Future<void> update(T item);
  Future<void> delete(String id);
}
 
abstract class SyncableRepository {
  Future<void> syncWithServer();
}
 
// TagRepository endi faqat kerakli interfeysni implement qiladi:
class TagRepository implements ReadableRepository<Tag> {
  @override
  Future<List<Tag>> getAll() async => [];
  @override
  Future<Tag> getById(String id) async => throw UnimplementedError();
}
 
// To'liq CRUD kerak bo'lgan repository esa barcha kerakli interfeyslarni birlashtiradi:
class ProductRepository implements ReadableRepository<Product>, WritableRepository<Product> {
  @override
  Future<List<Product>> getAll() async => [];
  @override
  Future<Product> getById(String id) async => throw UnimplementedError();
  @override
  Future<void> create(Product item) async {}
  @override
  Future<void> update(Product item) async {}
  @override
  Future<void> delete(String id) async {}
}

Bu yondashuvning amaliy foydasi — funksiya faqat ReadableRepository<T> qabul qilsa, u yozish imkoniyati bo'lmagan repository bilan ham xavfsiz chaqirilishi mumkin, va kod o'qigan odam funksiya imzosining o'zidan "bu funksiya faqat o'qiydi, yozmaydi" degan ma'lumotni darhol oladi.

7. SOLID — D: Dependency Inversion Principle#

Yuqori darajadagi modullar quyi darajadagi modullarga emas, ikkalasi ham abstraksiyalarga bog'liq bo'lishi kerak. Bu — SOLID'ning eng amaliy foydali tamoyili, chunki u to'g'ridan-to'g'ri testlanuvchanlikka olib keladi.

// YOMON — controller aniq HTTP implementatsiyasiga qattiq bog'langan (tightly coupled)
class OrderController extends ChangeNotifier {
  final _dio = Dio(); // aniq implementatsiya to'g'ridan-to'g'ri yaratiladi
 
  Future<void> submitOrder(Order order) async {
    await _dio.post('https://api.example.com/orders', data: order.toJson());
  }
}
// Bu klassni sinash uchun HAQIQIY tarmoq so'rovisiz iloj yo'q — chunki Dio() ichkarida yaratilgan.
// YAXSHI — controller abstraksiyaga bog'liq, aniq implementatsiya tashqaridan uzatiladi
abstract class OrderApi {
  Future<void> submit(Order order);
}
 
class DioOrderApi implements OrderApi {
  DioOrderApi(this._dio);
  final Dio _dio;
 
  @override
  Future<void> submit(Order order) async {
    await _dio.post('https://api.example.com/orders', data: order.toJson());
  }
}
 
class OrderController extends ChangeNotifier {
  OrderController(this._api); // "dependency injection" — bog'liqlik tashqaridan kelyapti
  final OrderApi _api;
 
  Future<void> submitOrder(Order order) async {
    await _api.submit(order);
  }
}
 
// Testda haqiqiy tarmoqsiz, soxta (fake) implementatsiya bilan sinash mumkin:
class FakeOrderApi implements OrderApi {
  bool wasCalled = false;
  @override
  Future<void> submit(Order order) async => wasCalled = true;
}
 
test('submitOrder OrderApi.submit ni chaqiradi', () async {
  final fakeApi = FakeOrderApi();
  final controller = OrderController(fakeApi);
  await controller.submitOrder(Order(id: '1'));
  expect(fakeApi.wasCalled, isTrue);
});

Flutter ekotizimida bu tamoyil amalda uch shaklda uchraydi:

Constructor injection
Eng oddiy va aniq usul — bog'liqlik konstruktorga parametr sifatida beriladi, sehr yo'q
Service locator (get_it)
Bog'liqliklar markazlashgan registrda ro'yxatdan o'tkaziladi, kerakli joyda GetIt.I<T>() orqali olinadi
Riverpod provider
Provider'lar o'zi DI konteyneri vazifasini bajaradi — testda provider'ni override qilib, fake implementatsiya berish mumkin

Uchtasi ham bitta maqsadga xizmat qiladi — widget/controller aniq implementatsiyani emas, interfeysni bilishi, shunda 11-bobdagi testlash strategiyasida ko'rilgan mocking/fake yondashuvlar tabiiy ravishda ishlaydi. Agar klass o'z bog'liqliklarini o'zi (= SomeService()) yaratsa — uni izolyatsiyada sinash deyarli imkonsiz bo'lib qoladi.

8. KISS va YAGNI — SOLID'ning "tormozi"#

SOLID'ni haddan tashqari qo'llash o'z muammosini yaratadi — shuning uchun ikkita qo'shimcha tamoyil uni muvozanatlaydi:

  • KISS (Keep It Simple, Stupid) — ikkita yechim bir xil muammoni hal qilsa, soddarog'ini tanlang. Bitta if/else yetarli bo'lgan joyda strategy pattern qurish — soddalikni qurbon qilib, murakkablik qo'shadi.
  • YAGNI (You Aren't Gonna Need It) — "kelajakda kerak bo'lishi mumkin" degan taxmin bilan abstraksiya qurmang. Open/Closed tamoyili haqiqiy, kuzatilgan kengayish ehtiyoji borida qo'llanadi, gipotetik ehtiyojda emas.

9. Interyvuda qanday so'raladi — kod hidi (code smell) topish#

Real intervyularda kamdan-kam "SOLID nima, ro'yxatlang" deb so'raladi. Ko'pincha kod bo'lagi ko'rsatilib, "bu yerda nima muammo, qaysi tamoyil buzilgan va qanday tuzatasiz" deb so'raladi. Ikkita mini-mashq:

Mashq 1:

class ReportGenerator {
  void generate(String type) {
    if (type == 'pdf') { /* PDF yaratish logikasi, 40 qator */ }
    else if (type == 'excel') { /* Excel yaratish logikasi, 35 qator */ }
    else if (type == 'csv') { /* CSV yaratish logikasi, 20 qator */ }
  }
}

Qaysi tamoyil buzilgan? — Open/Closed (yangi format qo'shilganda shu klass o'zgaradi) va Single Responsibility (bitta klass uchta butunlay boshqa formatlash mantig'ini bilishi shart emas). Yechim: har bir format uchun alohida ReportFormatter strategiyasi.

Mashq 2:

class AnalyticsService {
  void logEvent(String name);
  void logScreen(String name);
  void flushToDisk();
  void uploadToServer();
  void clearAllData();
}
// Bitta ekran faqat logEvent ishlatadi, lekin uni sinash uchun 5 ta metodni ham
// mock/stub qilish kerak bo'ladi.

Qaysi tamoyil buzilgan? — Interface Segregation. Yechim: EventLogger (faqat logEvent/logScreen) va AnalyticsStorage (faqat flushToDisk/uploadToServer/ clearAllData) kabi kichikroq interfeyslarga bo'lish.

Ko'p uchraydigan xato javoblar#

Savol/vaziyatXato javobTo'g'ri javob
"DRY nima?""Bir xil kodni ikki marta yozmaslik"Bir xil bilimni takrorlamaslik — tasodifan o'xshash, lekin turli sababga ko'ra o'zgaradigan kodni birlashtirish shart emas
"SOLID'ni qachon qo'llaysiz?""Har doim, har bir klassda"O'zgarish ehtimoli yuqori, murakkablik oqlanadigan joylarda — YAGNI bilan muvozanatlab
"Open/Closed qanday amalga oshiriladi?""Kodni to'g'ridan-to'g'ri o'zgartiraman, testlarni yangilayman"Mavjud kodni o'zgartirmay, yangi klass/strategiya orqali kengaytiraman
"Dependency Injection nima uchun kerak?""Kodni 'professional' ko'rsatish uchun"Klassni bog'liqliklarisiz, izolyatsiyada sinash imkonini beradi — asosiy maqsad testlanuvchanlik
"Interfeys qancha katta bo'lishi kerak?""Kattaroq — kamroq fayl, qulayroq"Har bir klient faqat o'ziga kerakli metodlarga bog'liq bo'ladigan darajada kichik

Tez-tez so'raladigan intervyu savollari#

Savol: SOLID'ning har bir harfini Flutter kontekstida bitta jumlada tushuntiring.

Javob: S — bitta klass bitta o'zgarish sababiga ega (masalan, widget UI'ni, controller biznes mantiqni bilishi). O — yangi xulq-atvor mavjud kodni o'zgartirmasdan, yangi klass orqali qo'shiladi (strategy pattern). L — subtipni bazaviy tip o'rniga qo'yganda dastur buzilmasligi kerak (override qattiqroq shart qo'ymasligi). I — klient faqat o'ziga kerakli metodlarga bog'liq bo'lishi kerak (kichik interfeyslar). D — yuqori darajadagi kod aniq implementatsiyaga emas, abstraksiyaga bog'liq bo'ladi (bu — testlanuvchanlikning asosiy manbai).

Savol: StatefulWidgetning o'zi SOLID'ni buzadimi?

Javob: Yo'q, StatefulWidgetning o'zi neytral — muammo unga qancha mas'uliyat yuklanganida boshlanadi. Agar State klassi faqat UI holatini (masalan, animatsiya kontrolleri, scroll pozitsiyasi) boshqarsa — bu to'g'ri. Agar u HTTP so'rov, biznes qoidalar va formatlashni ham o'z ichiga olsa — Single Responsibility buziladi. Yechim StatefulWidgetni yo'q qilish emas, balki biznes mantiqni alohida controller/notifier klassiga chiqarish.

Savol: DRY va Single Responsibility bir-biriga zid kelishi mumkinmi?

Javob: Ha, ba'zan. Ikki joyda bir xil ko'rinadigan, lekin turli sababga ko'ra o'zgaradigan kodni "DRY qilib" bitta klassga birlashtirish aslida Single Responsibility'ni buzadi — chunki endi bitta klass ikkita turli mas'uliyatga xizmat qiladi. Bunday holatda SRP ustunroq: ular alohida qolishi, faqat haqiqatan bir xil bo'lgan qismi (masalan, umumiy validatsiya funksiyasi) chiqarib olinishi kerak.

Savol: abstract class va Dart'dagi implicit interfeyslar orasidagi farq nima, va bu Interface Segregation'ga qanday ta'sir qiladi?

Javob: Dart'da har qanday klass avtomatik ravishda implicit interfeys hisoblanadi — implements SomeClass deb yozganda, uning implementatsiyasi emas, faqat metod imzolari (signature) meros olinadi. abstract class esa qo'shimcha ravishda umumiy implementatsiyani ham berish imkonini beradi (abstract metodlar bilan bir qatorda oddiy metodlar ham bo'lishi mumkin). ISP nuqtai nazaridan bu farq muhim emas — ikkalasida ham kichik, maqsadli "shartnoma" yaratish printsipi bir xil qo'llanadi, farq faqat implementatsiyani qayta ishlatish kerakmi-yo'qmi degan amaliy tanlovda.

Savol: get_it va Riverpod'ning ikkalasi ham DI vositasi — ular orasidagi asosiy farq nima?

Javob: get_it — klassik service locator: bog'liqliklar global registrda ro'yxatdan o'tkaziladi, kerakli joyda GetIt.I<SomeService>() orqali qidirib topiladi. Bu — runtime'da ishlaydi, ya'ni agar kerakli tip ro'yxatdan o'tkazilmagan bo'lsa, xato faqat ishga tushirilganda (runtime'da) ma'lum bo'ladi. Riverpod'da esa provider'lar compile-timeda tekshiriladigan, widget daraxti orqali uzatiladigan grafik hosil qiladi — noto'g'ri ishlatilgan provider ko'pincha kompilyatsiya bosqichidayoq aniqlanadi, va testda provider'ni overrideWith bilan almashtirish get_itdagi global registrni qo'lda tozalashdan ko'ra tozaroq, izolyatsiyalangan usul hisoblanadi.

Savol: Open/Closed tamoyilini qo'llash uchun har doim abstract class yoki interfeys kerakmi?

Javob: Ko'pincha ha, lekin yagona yo'l emas — Dart'da funksiyalarni ham birinchi darajali qiymat (first-class value) sifatida uzatish mumkin, shuning uchun oddiy hollarda strategy pattern o'rniga funksiya parametri yetarli bo'ladi: double calculateFee(double amount, double Function(double) feeStrategy). Abstract class strategiya o'zi bilan qo'shimcha holat (state) yoki bir nechta bog'liq metodni olib yurishi kerak bo'lganda afzalroq, oddiy bitta hisob-kitob uchun esa funksiya parametri kifoya va ortiqcha murakkablikni oldini oladi (YAGNI).

Savol: Liskov Substitution buzilishini test orqali qanday aniqlash mumkin?

Javob: Amaliy usul — bazaviy klass uchun yozilgan bir xil test to'plamini har bir subtipga qo'llab ko'rish ("Liskov Substitution Test"). Agar bazaviy klass uchun yozilgan test biror subtipda kutilmagan istisno tashlasa yoki mantiqan boshqa natija bersa — bu LSP buzilishining aniq belgisi. Bu ayniqsa mockito/mocktail bilan bir xil interfeysning turli implementatsiyalarini sinaganda foydali texnika.

Savol: Nega Dependency Inversion "yuqori darajadagi modul quyi darajadagi modulga bog'liq bo'lmasligi kerak" deb ataladi — "inversion" (teskarilash) so'zi nimani anglatadi?

Javob: An'anaviy (inversiyasiz) arxitekturada yuqori darajadagi modul (masalan, biznes mantiq) to'g'ridan-to'g'ri quyi darajadagi modulni (masalan, aniq HTTP kutubxonasi) chaqiradi — bog'liqlik strelkasi yuqoridan pastga yo'nalgan. Dependency Inversion'da bu strelka "teskarilanadi": endi quyi darajadagi modul (HTTP implementatsiyasi) yuqori darajadagi modul belgilagan abstraksiyaga (interfeysga) mos kelishi majbur qilinadi — bog'liqlik strelkasi abstraksiya tomon, ikkala yo'nalishdan ham "ichkariga" qarab yo'naladi. Shu sabab nomi "inversion" (teskarilash).

Savol: Kichik shaxsiy loyihada (bitta dasturchi, tez MVP) SOLID'ni to'liq qo'llash oqlanadimi?

Javob: Ko'pincha yo'q — bu YAGNI bilan to'qnashadi. Bitta dasturchi, qisqa muddatli MVP'da har bir klass uchun interfeys yaratish, DI konteyner sozlash — tezlikni pasaytiradi, foyda esa (kelajakda kengayish, jamoada ishlash) hali mavjud emas. Amaliy yondashuv: SRP va aniq nomlashni (arzon, doim foydali) darhol qo'llash, lekin Open/Closed va to'liq DI'ni loyiha o'sib, haqiqiy kengayish ehtiyoji paydo bo'lganda kiritish.

Savol: Bloc/Cubit arxitekturasi SOLID tamoyillarining qaysilarini "majburlaydi"?

Javob: Bloc arxitekturasi tabiiy ravishda Single Responsibilityni majburlaydi — UI faqat statega qarab chiziladi, biznes mantiq esa Bloc/Cubit ichida joylashadi, ular orasidagi chegara framework darajasida qat'iy. Shuningdek Dependency Inversionni ham qulaylashtiradi — Bloc konstruktori odatda repository abstraksiyasini qabul qiladi, bu esa testda fake repository berishni tabiiy holga keltiradi. Biroq Open/Closed yoki Interface Segregation'ni avtomatik ta'minlamaydi — bular hali ham dasturchining loyihalash qaroriga bog'liq.

Savol: "God object" (hamma narsani biladigan bitta ulkan klass) qaysi SOLID tamoyilini eng ko'p buzadi, va uni qanday bosqichma-bosqich tuzatasiz?

Javob: Eng bevosita — Single Responsibility, lekin odatda unga ergashib Interface Segregation ham buziladi (barcha klientlar shu bitta katta klassga bog'liq bo'lib qoladi). Tuzatish bosqichma-bosqich olib boriladi: avval klassning barcha metodlarini mas'uliyat guruhlariga ajratib chiqish (masalan, "ma'lumot olish", "validatsiya", "formatlash"), so'ng har bir guruhni alohida klassga ko'chirish, so'ngra asl klassni shu yangi klasslarni koordinatsiya qiluvchi yupqa "fasad"ga aylantirish — bir martalik katta refaktoring emas, kichik, sinovdan o'tkaziladigan qadamlar bilan.

Savol: Extension method orqali DRY qilish qachon noto'g'ri tanlov bo'lishi mumkin?

Javob: Extension method holatga (state) ega bo'la olmaydi va faqat mavjud tipga "tashqi" xulq-atvor qo'shadi — agar takrorlanayotgan mantiq aslida holat va konfiguratsiyani ham o'zida saqlashi kerak bo'lsa (masalan, keshlash, hisoblash natijasini saqlab qolish), extension noto'g'ri tanlov bo'ladi va uning o'rniga alohida klass yoki mixin kerak bo'ladi. Shuningdek, agar extension juda ko'p, turli mavzudagi metodlarni bitta faylga to'plasa, bu ham o'z-o'zidan Single Responsibility'ni buzadigan "extension God object"ga aylanib qolishi mumkin.

Savol: Refaktoring qilishda testlar yo'q bo'lsa, SOLID tamoyillarini qo'llash xavfli bo'ladimi?

Javob: Ha, sezilarli darajada. SOLID asosidagi refaktoring (masalan, klassni bo'lish, interfeys ajratish) kodning tashqi xulq-atvorini o'zgartirmasligi kerak — lekin buni faqat testlar tasdiqlay oladi. Testsiz refaktoring qilish — "ishlayotgan narsani ishlamay qo'yish" xavfini oshiradi. Amaliy tartib: avval mavjud xulq-atvorni qamrab oluvchi (hech bo'lmasa asosiy yo'llar bo'yicha) test yozish, keyin SOLID asosida refaktoring qilish, testlar hamon o'tishini tekshirish — faqat shundan keyin "xavfsiz refaktoring qildim" deyish mumkin.

Amaliy topshiriq

Kod hidini toping va SOLID asosida refaktoring qiling

Quyidagi klassni tasavvur qiling: NotificationService — push xabar yuborish, xabarni SQLite bazasiga saqlash, xabar matnini uch tilga tarjima qilish va foydalanuvchi sozlamalariga qarab filtrlashning barchasini bitta metodda (sendNotification()) bajaradi. Qaysi SOLID tamoyil(lar)i buzilganini aniqlang, klassni mas'uliyatlarga ajrating (interfeys va konstruktor injection bilan) va yangi tuzilmani qisqa kod misolida yozing.

Maslahatni ko'rsatish

Avval qaysi tamoyil(lar) buzilganini yozing, keyin faqat shu muammoni hal qiladigan minimal refaktoringni qiling — ortiqcha abstraksiya qo'shmang (YAGNI'ni unutmang).