08 Comprehensive Case Study Grasping Dart's Core Features

08 Comprehensive Case Study Grasping Dart’s Core Features #

Hello, I’m Chen Hang.

In the previous two articles, I first learned with you the basic structure and syntax of Dart programs and understood the fundamental elements of the Dart language world, which are the type system and how they represent information. Then, I guided you through the basic principles of object-oriented design in Dart, and you learned about concepts like functions, classes, and operators, which are common in other programming languages but have their own differences and typical usage in Dart, allowing you to understand how Dart handles information.

As you can see, Dart incorporates the advantages of other programming languages, making it simple and concise, yet powerful when it comes to expressing and processing information. As the saying goes, knowledge comes from practice. So today, I’ll use a comprehensive case study to connect the scattered knowledge about Dart that we’ve learned so far, hoping that you can try it out and learn how to code with Dart.

With the knowledge points we’ve covered so far and the practice from today’s comprehensive case study, I believe you have mastered 80% of the most commonly used features in Dart, and you can start using Flutter without much language barrier. As for the remaining 20% of the features, I won’t focus on explaining them in this column because they are less commonly used. If you’re interested in this part, you can visit the official documentation for further understanding.

Furthermore, I will delve into the topic of asynchronous and concurrent programming in Dart in the 23rd article titled “How Does the Single-Threaded Model Ensure a Smooth UI?”.

Case Introduction #

Today, the case I chose is to first write a shopping cart program using Dart, but without using any Dart-specific features. Then, starting from this program, we gradually add Dart language features to transform it into a program that conforms to Dart design principles. In this process of transformation, you will further appreciate the charm of Dart.

First, let’s take a look at what a basic shopping cart program looks like without using any Dart syntax features.

// Define the Item class
class Item {
  double price;
  String name;
  Item(name, price) {
    this.name = name;
    this.price = price;
  }
}

// Define the ShoppingCart class
class ShoppingCart {
  String name;
  DateTime date;
  String code;
  List<Item> bookings;

  price() {
    double sum = 0.0;
    for(var i in bookings) {
      sum += i.price;
    }
    return sum;
  }

  ShoppingCart(name, code) {
    this.name = name;
    this.code = code;
    this.date = DateTime.now();
  }

  getInfo() {
    return 'Shopping Cart Information:' +
          '\n-----------------------------' +
          '\nUsername: ' + name+ 
          '\nDiscount Code: ' + code + 
          '\nTotal Price: ' + price().toString() +
          '\nDate: ' + date.toString() +
          '\n-----------------------------';
  }
}

void main() {
  ShoppingCart sc = ShoppingCart('John', '123456');
  sc.bookings = [Item('Apple',10.0), Item('Pear',20.0)];
  print(sc.getInfo());
}

In this program, I defined the Item class and the ShoppingCart class. They both contain an initialization constructor that assigns the parameter information passed into the main function to the object’s internal properties. The basic information of the shopping cart is output through the getInfo method in the ShoppingCart class. In this method, I used string concatenation to format and combine various information, and then return it to the caller.

When running this program, assuming everything goes well, the basic information of the shopping cart object sc, including the username, discount code, total price, and date, will be printed to the command line.

Shopping Cart Information:
-----------------------------
Username: John
Discount Code: 123456
Total Price: 30.0
Date: 2019-06-01 17:17:57.004645
-----------------------------

The functionality of this program is very simple: we initialize a shopping cart object, add items to the shopping cart, and finally print out the basic information. As you can see, without using any Dart syntax features, this code is not significantly different from Java, C++, or even JavaScript in terms of syntax.

In terms of expressing and handling information, Dart maintains a simple and concise style. Next, let’s start with expressing information and see how Dart optimizes this code.

Class Abstraction Refactoring #

First, let’s take a look at the initialization part of the Item class and ShoppingCart class. In their constructors, the initialization work only involves assigning the parameters passed in from the main function to instance variables.

In other programming languages, it is very common to assign the initialization parameters to instance variables within the body of the constructor. In Dart, however, we can use syntax sugar and initializer lists to simplify this assignment process, thus eliminating the body of the constructor altogether:

class Item {
  double price;
  String name;
  Item(this.name, this.price);
}

class ShoppingCart {
  String name;
  DateTime date;
  String code;
  List<Item> bookings;
  price() {...}
  // Removed the body of the constructor
  ShoppingCart(this.name, this.code) : date = DateTime.now();
...
}

This reduces the code by 7 lines! Through this refactoring, we have made two new observations:

  • Firstly, both the Item class and ShoppingCart class have a name property, which represents the name of the item in the former and the username in the latter.
  • Secondly, the Item class has a price property, and the ShoppingCart class has a price method. They both represent the current price.

Considering that the name and price properties (method) have the same name and type, and they serve a similar purpose in terms of information expression, I can further abstract a new base class called Meta based on these two classes, and use it to store the price and name properties.

At the same time, considering that in the ShoppingCart class, the price property is only used to calculate the price of the items in the shopping cart (rather than for data access like in the Item class), I have rewritten the get method of the price property in the ShoppingCart class:

class Meta {
  double price;
  String name;
  Meta(this.name, this.price);
}
class Item extends Meta{
  Item(name, price) : super(name, price);
}

class ShoppingCart extends Meta{
  DateTime date;
  String code;
  List<Item> bookings;
  
  double get price {...}
  ShoppingCart(name, this.code) : date = DateTime.now(),super(name,0);
  getInfo() {...}
}

With this class abstraction refactoring, the dependencies between different classes in the program become clearer. However, there are still two lengthy methods in this program that seem out of place, namely the get method of the price attribute that calculates the price in the ShoppingCart class, and the getInfo method that provides basic information about the shopping cart. Next, we will refactor these two methods separately.

Method Refactoring #

Let’s first take a look at the get method of the price attribute:

double get price {
  double sum = 0.0;
  for(var i in bookings) {
    sum += i.price;
  }
  return sum;
}

In this method, I used a common summing algorithm seen in other programming languages. It iterates over the bookings list of Item objects, accumulating and adding their prices together.

However, in Dart, we can achieve this summing operation by overloading the + operator of the Item class and using the reduce operation on the list object to perform a fold operation (you can imagine merging all the items in the shopping cart into a single bundled item).

Furthermore, since the function body consists of only one line, we can further simplify the implementation using Dart’s arrow function syntax:

class Item extends Meta{
  ...
  // Overload the + operator to merge items into a bundled item
  Item operator+(Item item) => Item(name + item.name, price + item.price); 
}

class ShoppingCart extends Meta{
  ...
  // Change the summing operation to a fold operation
  double get price => bookings.reduce((value, element) => value + element).price;
  ...
  getInfo() {...}
}

As you can see, this code is much more concise! Next, let’s take a look at how we can optimize the getInfo method.

In the getInfo method, we combine the basic information of the ShoppingCart class through string concatenation, which is a common practice in other programming languages. However, in Dart, we can completely eliminate the inelegant string concatenation and achieve string formatting and combination by inserting variables or expressions into a multi-line string declaration:

getInfo () => '''
购物车信息:
-----------------------------
  用户名: $name
  优惠码: $code
  总价: $price
  Date: $date
-----------------------------
''';

After removing unnecessary string escape characters and concatenation code, the getInfo method looks much clearer.

After optimizing the internal implementation of the ShoppingCart class and the Item class, let’s take a look at the main function and analyze from the perspective of the caller where further optimizations can be made.

Optimization of Object Initialization #

In the main function, we initialize a shopping cart object for a user named “张三” with a discount code of “123456” using the following code:

ShoppingCart sc = ShoppingCart('张三', '123456') ;

However, we can optimize this initialization method from two aspects:

  • Firstly, we want to provide the caller with a more explicit way to initialize the ShoppingCart object by allowing them to specify the initialization parameters using the “parameter name: parameter key-value” syntax. In Dart, we can achieve this by adding {} to the parameters when declaring a function.
  • Secondly, a shopping cart object always requires a username, but it may or may not have a discount code. Therefore, we need to provide an initialization method for the shopping cart object without a discount code, and determine the correct order of calling multiple initialization methods and the initialization method of the parent class.

Following this approach, we begin the transformation of the ShoppingCart class.

It is important to note that since the discount code can be empty, we also need to make the getInfo method compatible. Here, I use the a ?? b operator, which simplifies the ternary expression (a != null) ? a : b commonly used in other languages.

class ShoppingCart extends Meta {
  // Default initialization method, forwarded to `withCode`
  ShoppingCart({name}) : this.withCode(name:name, code:null);
  
  // `withCode` initialization method, using syntactic sugar and initializer lists for assignment, and calling the parent class's initialization method
  ShoppingCart.withCode({name, this.code}) : date = DateTime.now(), super(name, 0);

  // The `??` operator means if `code` is not `null`, use its original value; otherwise, use the default value of "没有"
  getInfo () => '''
购物车信息:
-----------------------------
  用户名: $name
  优惠码: ${code??"没有"}
  总价: $price
  Date: $date
-----------------------------
''';
}

void main() {
  ShoppingCart sc = ShoppingCart.withCode(name:'张三', code:'123456');
  sc.bookings = [Item('苹果',10.0), Item('鸭梨',20.0)];
  print(sc.getInfo());

  ShoppingCart sc2 = ShoppingCart(name:'李四');
  sc2.bookings = [Item('香蕉',15.0), Item('西瓜',40.0)];
  print(sc2.getInfo());
}

Running this program will print the shopping cart information for both ‘张三’ and ‘李四’ to the console:

购物车信息:
-----------------------------
  用户名: 张三
  优惠码: 123456
  总价: 30.0
  Date: 2019-06-01 19:59:30.443817
-----------------------------

购物车信息:
-----------------------------
  用户名: 李四
  优惠码: 没有
  总价: 55.0
  Date: 2019-06-01 19:59:30.451747
-----------------------------

Regarding the printing of shopping cart information, we encapsulate the behavior of printing the information into the ShoppingCart class. However, the behavior of printing information is a very common feature that may be required not only by the ShoppingCart class but also by the Item objects.

To achieve this, we need to encapsulate the printing behavior into a separate class called PrintHelper. However, since the ShoppingCart class already inherits from the Meta class, and Dart does not support multiple inheritance, how can we reuse the PrintHelper class?

This is where the “mixin” concept, which I mentioned in the previous article, comes into play. As you may recall, we simply need to use the with keyword when using it.

Let’s try adding the PrintHelper class and adjusting the declaration of the ShoppingCart class:

abstract class PrintHelper {
  printInfo() => print(getInfo());
  getInfo();
}

class ShoppingCart extends Meta with PrintHelper {
...
}

After applying the mixin transformation, we have encapsulated all the behaviors of a shopping cart into the ShoppingCart class. Furthermore, for the caller, we can use the cascading operator .. to call multiple functions and access member variables on the same object. Using the cascading operator avoids the need for creating temporary variables and makes the code more fluent:

void main() {
  ShoppingCart.withCode(name:'张三', code:'123456')
    ..bookings = [Item('苹果',10.0), Item('鸭梨',20.0)]
    ..printInfo();

  ShoppingCart(name:'李四')
    ..bookings = [Item('香蕉',15.0), Item('西瓜',40.0)]
    ..printInfo();
}

Great! With the unique language features of Dart, we have finally transformed this shopping cart code into a concise, direct, and powerful Dart-style program.

Summary #

That’s all for today’s sharing. Today, we started with a shopping cart prototype that has no obvious syntax differences from Java, C++, or even JavaScript, and gradually transformed it into a program that conforms to Dart’s design principles.

First, we used syntactic sugar for constructor and initializer lists to simplify the process of assigning member variables. Then, we overloaded the “+” operator and used the fold() function to implement price calculation, and used multi-line strings and embedded expressions to eliminate unnecessary string concatenation. Finally, we reorganized the inheritance relationship between classes and optimized the object initialization call by using mixins, multiple constructors, and optional named parameters.

Below is the complete code of today’s comprehensive shopping cart case. I hope you can practice more in your IDE and experience the transformation process, thereby gaining a deeper understanding of Dart’s key syntactic features that make the code more concise, direct, and powerful. You can also find the code before and after the transformation in Dart_Sample on GitHub:

class Meta {
  double price;
  String name;
  // Member variable initialization sugar syntax
  Meta(this.name, this.price);
}

class Item extends Meta {
  Item(name, price) : super(name, price);
  // Overload the + operator to merge item objects into combo items
  Item operator+(Item item) => Item(name + item.name, price + item.price); 
}

abstract class PrintHelper {
  printInfo() => print(getInfo());
  getInfo();
}

// with indicates that another class's member variables and functions are reused in a non-inherited manner
class ShoppingCart extends Meta with PrintHelper {
  DateTime date;
  String code;
  List<Item> bookings;
  // Calculate the sum by folding
  double get price => bookings.fold(0, (previousValue, element) => previousValue + element.price);
  // Default initialization function, forwarded to withCode function
  ShoppingCart({name}) : this.withCode(name: name, code: null);
  // Initialization method withCode, assign value using syntactic sugar and initializer list, and call the parent class's initialization method
  ShoppingCart.withCode({name, this.code}) : date = DateTime.now(), super(name,0);

  // ?? operator means if code is not null, use the original value, otherwise use the default value "none"
  @override
  getInfo() => '''
Shopping Cart Information:
-----------------------------
Name: $name
Promo Code: ${code??"none"}
Total Price: $price
Date: $date
-----------------------------
''';
}

void main() {
  ShoppingCart.withCode(name: '张三', code: '123456')
  ..bookings = [Item('苹果',10.0), Item('鸭梨',20.0)]
  ..printInfo();

  ShoppingCart(name: '李四')
  ..bookings = [Item('香蕉',15.0), Item('西瓜',40.0)]
  ..printInfo();
}

Thought Exercise #

Please extend the implementation of the shopping cart program to support:

  1. The quantity attribute for each product;
  2. Adding product list information (including product name, quantity, and unit price) to the shopping cart information, to achieve the basic functionality of a receipt.

Feel free to leave me a comment in the comment section to share your thoughts. I will be waiting for you in the next article! Thank you for listening, and feel free to share this article with more friends to read together.