مثال سبد خرید – قسمت 2 (مدل سازی Cart)

برای Product هامون یه مدل خیلی خیلی ساده زدم که به شکل زیر هست:

class Product{

  const Product(this.id, this.name, this.color);

  final int id;
  final String name;
  final Color color;
}

که سه تا پراپرتی id و name و color داخلش داریم.

یه دونه کلاس Cart برای سبد خریدم میسازم:

class Cart{

  factory Cart.instance(){
    return _instance ??= Cart._();
  }

  static Cart? _instance;

  Cart._();

  final Map<Product, int> _items = HashMap();

  void add(Product product){
    if(_items.containsKey(product)){
      throw Exception("Unexpected behavior: product ${product.name} already exists in the cart");
    }

    _items.putIfAbsent(product, () => 1);
  }

  void increaseQuantityOf(Product product, int quantity){
    if(!_items.containsKey(product)){
      throw Exception("Unexpected behavior: product ${product.name} does not exist in the cart");
    }

    _items[product] = _items[product]! + quantity;
  }

  void decreaseQuantityOf(Product product, int quantity){
    if(!_items.containsKey(product)){
      throw Exception("Unexpected behavior: product ${product.name} does not exist in the cart");
    }

    if(quantityOf(product) < quantity){
      throw Exception("Unexpected behavior");
    }

    _items[product] = _items[product]! - quantity;

    if(quantityOf(product) == 0){
      remove(product);
    }
  }

  void remove(Product product){
    if(!_items.containsKey(product)){
      throw Exception("Unexpected behavior: product ${product.name} does not exist in the cart");
    }

    _items.remove(product);
  }

  int quantityOf(product){
    if(!_items.containsKey(product)){
      throw Exception("Unexpected behavior: product ${product.name} does not exist in the cart");
    }

    return _items[product]!;
  }

  int get total{
    var result = 0;
    for(var qty in _items.values){
      result += qty;
    }
    return result;
  }
}



اولن که یه کلاس سینگلتون هست تا توی کل برنامه یه دونه ازش داشته باشم:

پ.ن: سینگلتون کردن توی اپلیکیشن کار قشنگی نیست و یه جورایی انتی پترن به حساب میاد و بهتره و باید این موضوع رو با Dependency Injection و توی Composition root مدیریت کنیم ولی توی این مثال نمیخوایم وارد اون مباحث بشیم و بنابراین از سینگلتون استفاده میکنیم.

توی این کلاس یه دونه Map دارم که هر product رو به تعدادش توی سبد مپ میکنه:

6 تا متد هم داخلش دارم:

  • add
  • increaseQuantityOf
  • decreaseQuantityOf
  • remove
  • quantityOf
  • total

total

متد یا پراپرتی total توی مپ میچرخه و quantity ها (تعداد محصولات) رو با هم جمع میکنه و ریترن میکنه:

پس این متد جمع تعداد کل محصولات داخل سبد هست.

quantityOf

متد quantityOf هم تعداد یه product خاص توی سبد رو میده:

اگه product از قبل توی سبد نباشه این متد یه exception میده. پس precondition این متد اینه که product از قبل توی سبد وجود داشته باشه.(کال کردن این متد با یه product ای که توی سبد نبوده نشونه ی باگ نرم افزار و اشتباه برنامه نویس هست, پس exception رو میگذاریم تا باگ برملا بشه)

پ.ن: precondition شروطی هستن که ورودی های یه متد باید پاس کنن تا اون متد بتونه کار خودش رو به خوبی انجام بده. توی متد quantityOf ورودی متد product هست و شرطی که باید پاس کنه اینه که از قبل توی سبد خرید ورود داشته باشه.

ایا هر سبد خریدی که نوشتی باید این مدلی باشه و این شرط و precondition رو داشته باشه؟

واضحه که نه.

این بیزینسی هست که من اینجا طراحی کردم و ممکنه توی هر شرایطی requirement های مختلفی داشته باشیم.

پ.ن: اکسپشن های مربوط به precondition ها رو هندل نمیکنیم (try–catch). فقط توی main میتونیم بگیرمشون. چون که نشونه ی باگ هستن و اصلن نباید اتفاق بیوفتن.

add

متد add یه product ای رو که قبلن توی سبد نبوده رو به سبد اضافه میکنه:

این متد هم یه preconditon داره و اونم این هست که product از قبل توی سبد نباشه. پس اگه این متد رو با یه product ای که قبلن توی سبد بوده کال کنیم, نشونه ی باگ و اشتباه برنامه نویسه!!!

این متد بعد از اضافه کردن product به سبد مقدار quantity ایش رو برابر با یک قرار میده که دلیلش کاملن واضحه.

increaseQuantityOf

متد increaseQuantityOf تعداد یه product ای که از قبل توی سبد بوده رو افزایش میده:

این متد هم یه precondition داره و اونم اینه که product از قبل توی سبد باشه.

در واقع با متد add یه product ای که قبلن توی سبد نبوده رو به سبد اضافه میکنیم و با متد increaseQuantityOf هم تعداد یه product که قبلن با متد add به سبد اضافه شده رو افزایش میدیم.

remove

متد remove کلن یه product رو از سبد پرت میکنه بیرون:

precondition اینم مشخصه. چیه؟

افرین.

product باید از قبل توی سبد باشه.

decreaseQuantityOf

متد decreaseQuantityOf هم تعداد یه product توی سبد رو کاهش میده:

این متد دو تا precondition داره:

  • یکی اینکه product از قبل توی سبد باشه.
  • و اون یکی هم اینکه تعدادی که میخوای از سبد حذف کنیم از تعدادی که قبلن توی سبد بوده بیشتر نباشه.

این متد اولن تعداد محصول رو توی سبد کاهش میده و بعدش اگه تعداد به صفر رسیده باشه اونو از سبد پرتش میکنه بیرون.

duplication duplication duplication

خب کدهای تکراری زیاد شد و اذیتم میکنه و داره روی اعصابم راه میره. کدوم کدهای تکراری؟ همون هایی که میخواد precondition ها رو چک کنه.

علاوه بر این کدها clean نیست و abstraction خوبی ندارن. کدوم کد ها؟ همون هایی که میخواد precondition ها رو چک کنه.

اگه duplication رو از بین ببریم خود به خود مشکل abstraction هم حل میشه و یه abstraction خوب هم اعمال میکنیم.

پس دو تا متد ویژه برای چک کردن precondition ها مینویسم تا از کدهای تکراری جلوگیری کنم:

حالا از این متد ها توی کد استفاده میکنم:

یه دونه دیگه از این متد ها هم مینویسم:

دو تا متد اولی هم duplication رو از بین بردن و هم باعث خوانا تر شدن کد شدن (abstraction بهتر). ولی متد سومی فقط با هدف خوانا تر شدن کد و ایجاد یه abstraction بهتر اعمال شد و duplication ای وجود نداشت که بخواد از بین ببره.

اضافه کردن دو متد دیگه

حالا طی یه حرکت انفجاری میخوام دو تا متد دیگه به این کلاس اضافه کنم. یکی متد contains:

که چک میکنه که ایا یه product توی سبد هست یا نه.

و یه متد دیگه هم که لیست product های سبد رو برمگیردونه:

YAGNI YAGNI YAGNI

این متد ها (مخصوصن دو تا متد اخر) که بدون هیچ نیاز سنجی از سمت کلاینت ها و همین جوری برای ارضای روح اضافه شدن, مسلمن و قطعن با اصل YAGNI در تناقض هستن.

ولی نگران نباش من میدونم که بهشون نیاز دارم و اضافشون کردم.

ولی توی یه اپ واقعی حتمن باید بر اساس نیاز کارتو جلو ببری و کد اضافه نزنی و YAGNI رو حلقه ی گوشت کنی.

Rename

حالا میخوام بعضی از متد ها رو rename کنم و بهترشون کنم.

اولین متد, متد add هست که نظرم اگه اسمش addNew باشه بهتره و بهتر هدف خودش رو میرسونه. درسته؟ add خالی ادم رو گیج میکنه.

دومین متد هم _cartShouldContainEnough هست که اینجوری تغییر نامش میدم:

شاید بگی مردک دیوانه چرا از اول اسم خوب نکذاشتی؟

چون اول به ذهنم نرسیده بود و خودم رو درگیرش نکردم و هر موقع اسم بهتر به ذهنم برسه میام و تغییرش میدم. کدها و مقاله رو ویراش نکردم تا تو هم بفهمی که یه دیزاین خوب از اول بهترین نیست و در طول زمان خوب میشه.

اینم کد نهایی Cart:

class Cart{

  factory Cart.instance(){
    return _instance ??= Cart._();
  }

  static Cart? _instance;

  Cart._();

  final Map<Product, int> _items = HashMap();

  void addNew(Product product){
    _cartShouldnotContain(product);

    _items.putIfAbsent(product, () => 1);
  }

  void increaseQuantityOf(Product product, int quantity){
    _cartShouldContain(product);

    _items[product] = _items[product]! + quantity;
  }

  void decreaseQuantityOf(Product product, int quantity){
    _cartShouldContain(product);

    _cartShouldHaveEnough(product, quantity);

    _items[product] = _items[product]! - quantity;

    if(quantityOf(product) == 0){
      remove(product);
    }
  }

  void remove(Product product){
    _cartShouldContain(product);

    _items.remove(product);
  }

  bool contains(Product product){
    return _items.containsKey(product);
  }

  int quantityOf(product){
    _cartShouldContain(product);

    return _items[product]!;
  }

  int get total{
    var result = 0;
    for(var qty in _items.values){
      result += qty;
    }
    return result;
  }

  List<Product> get products => _items.keys.toList();

  void _cartShouldContain(Product product){
    if(!contains(product)){
      throw Exception("Unexpected behavior: product ${product.name} does not exist in the cart");
    }
  }

  void _cartShouldnotContain(Product product){
    if(contains(product)){
      throw Exception("Unexpected behavior: product ${product.name} already exists in the cart");
    }
  }

  void _cartShouldHaveEnough(Product product, int quantity){
     if(quantityOf(product) < quantity){
      throw Exception("Unexpected behavior: cart does not have enough product");
    }
  }
}


پ.ن: متد هایی که قرار بود precondition ها رو چک کنن رو هم private کردم که از بیرون قابل دسترس نباشن!!!

میبینی چه کد خوب و تر و تمیزی میشه نوشت که هر متد فقط یه کار انجام بده و اسم هر متد داد بزنه که داره چکار میکنه؟

حالا معماری بزن و 100 تا فولدر درست کن ولی لاجیکت خوب نباشه, چه فایده؟؟!!!

بگدریم… بریم سر کدهای UI که اونجا میخوام تلافی کنم و کثیف کاری کنم!!!

کدهای UI رو توی قسمت بعدی بهت نشون میدم تا این قسمت خیلی طولانی نشده و خو بتونی روی کدها فکر کنی و بعد که درکشون کردی بری قسمت بعدی.

ولی قبل پایان این قسمت میخوام یه سوال ازت بپرسم…

چرا این همه متد داخل کلاس Cart گذاشتم؟

برای مثال خود Map یه متد به اسم contains داره و میتونستم به جای اینکه متد contains رو بگذارم توی کلاس Cart, فیلد _items رو پابلیک میکردم تا از بیرون قابل دسترس باشه و هر کسی خواست متد contsinsKey رو روی خود map کال کنه.

یا میتونستم به جای گتر products خود items رو پابلیک کنم.

دلیل این همه متد داخل Cart چیه وقتی که همش رو میتونم روی خود Map هم بزنم؟

ایا دیووانه ام.

جواب این سوال رو بچه های دوره OOD به خوبی میدونن و البته خارج از بحث این دوره هست و میتونه یه چالش برات باشه تا درموردش تحقیق کنی…

نکته ی نام گذاری

توی نام گذاری متد ها من اولین پارامتر متد رو هم جزو نام متد حساب میکنم(اگه امکانش باشه…نه همیشه)

نمیگم addProduct و اسمش رو میکذارم add و اولین پارامتر که اسمش product هست به اسم اضافه میشه موقع خوندن

یا نمیگم quantityOfProduct و میگم quantityOf و اولین پارامتر که product هست موقع خوندن به اسم اضافه میشه

یا نمیگم increaseQuantityOfProduct و میگم increaseQuantityOf و …

دیدگاهتان را بنویسید

error: Alert: Content is protected !!