Simpler brainstorming with a Class Diagram
Scaling Android Architecture #1
Welcome in the first episode of the Scaling Android Architecture series đ. Before we jump into the Android Studio and make some nice coding letâs take a look on the tool which is very useful when you make architectural decisions in the project. This tool is called a UML Class Diagram.
UML what â
Some of you might have no idea what UML is or just heard this name somewhere without any wider context. Let me start with a short introduction then.
UML stands for Unified Modeling Language and is widely used in many areas of the software development industry. It includes dozens of different types of diagrams which help us express software solutions in the graphical way.
Just no diagrams, just let me code đ°
There are companies, projects and teams where everything is documented using different types of diagrams. In the Waterfall methodology a whole system is first described with formal documents including the UML diagrams. Only then it can be moved to the implementation phase where devs transform documentation into the code. You can even find big enterprise software solutions which are able to generate a working(ish) application from the UML structures.
Sounds cool right? đ Luckily we are living in the Agile world where UML is still found as a very useful tool, but in a different and more friendly way.
âšď¸ Diagrams are much easier to understand compare to words. The visual form allows us to clearly illustrate to the rest of the team what we are thinking about. Less misunderstanding == simpler communication.
This way we can propose different solutions, discuss them with the team and verify if everyone find them behave as expected. All of it without writing a single line of code.
Diagrams are fast and simple to use when we donât treat them as a complete documentation of a whole system. Even if the system is big and complex we can narrow down the content of the diagram to fit the context of the discussion, skip unimportant parts and simplify what can be simplified.
Representing architecture with a Class Diagram
Across many different UML diagrams we can find one which is very useful when talking about architecture and application structure. This diagram is called a Class Diagram. Letâs start with some basic concepts.
Class
As youâve probably noticed by the name, Class Diagram is used to represent classes of the application. In the simplest form the UML class is represented as a rectangle with the name inside. Single diagram can contain many different classes with different names. Each of them represent an actual class
from our codebase.
class SearchProductViewModel {
...
}
class ProductDetailsViewModel {
...
}
class ProductsRepository {
...
}
Association
Each application works thanks to the communication between classes. When one class uses a different class we say that there is an Association between them.
Across different types of Associations the most basic one is called a Unidirectional Association. This Association occurs when one class has a reference to a different class. For example when our ViewModel
has a Repository
reference passed to the constructor.
This kind of Association is represented as a solid line with an arrow on one end. The direction of this arrow is very important. Arrow should always point from the class which holds a reference to the class which is referenced.
class SearchProductViewModel(
private val productsRepository: ProductsRepository
) {
...
}
class ProductDetailsViewModel(
private val productsRepository: ProductsRepository
) {
...
}
class ProductsRepository {
...
}
Generalization
In the object oriented programming we sometimes introduce a common abstractions for our classes. One very well known is the androidx.lifecycle.ViewModel
abstraction which we widely use in the applications. Our View Models depend on this abstraction but they donât have a reference to it.
Instead of it they extend the Lifecycle View Model base class to inherit some common behaviour of it. In the UML notation inheritance is represented as special Association type called Generalization.
We used here a special notation to mark View Model as an abstract class
. All the blocks in the UML Class Diagram represent a class
by default. If we want to indicate that this is a some special type of unit we use the <<*>>
notation on top of the name.
abstract class ViewModel {
...
}
class SearchProductViewModel(
private val productsRepository: ProductsRepository
) : ViewModel() {
...
}
class ProductDetailsViewModel(
private val productsRepository: ProductsRepository
) : ViewModel() {
...
}
class ProductsRepository {
...
}
Realization
Passing an instance of the class to the constructor of other class is not the only way how classes can depend on each other. You probably know the Dependency Inversion principle from the famous set of SOLID principles. We often put interfaces
in our codebase to invert some dependencies.
When a class
implements an interface
there is also an Association between them. Same as for the inheritance the class does not have a reference to an instance of an interface
so we canât use a Unidirectional Association here.
To represent this case there is a next type of an Association called Realization. Simply saying it informs us that given class
realizes a behaviour defined by the interface
.
class SearchProductViewModel(
private val productsRepository: ProductsRepository
) {
...
}
class ProductDetailsViewModel(
private val productsRepository: ProductsRepository
) {
...
}
interface ProductsRepository {
...
}
class FakeProductsRepository : ProductsRepository {
...
}
Attributes and Methods
Associations help us understand how certain classes use each other. Our Class Diagram can be more detailed than that. It can express what exact data is kept by the class and what operations it offers. Data kept inside the class is called an Attribute while operations are represented as Methods.
To add Attributes and Methods to the class we add separate sections to the rectangular block. For classes the first block is reversed for Attributes and the second for Methods. Interfaces do not keep data so they have only one extra section just for Methods.
Additionally we can include visibility modifiers in front of Attribute or Method name. They are mostly the same as we usually see in the programming language: public (+), private (-), protected (#).
class SearchProductViewModel(
private val productsRepository: ProductsRepository
) {
var searchInput by mutableStateOf("")
private set
val matchingProducts: StateFlow<List<Product>> ...
fun searchProduct(name: String) ...
fun clearSearch() ...
}
class ProductDetailsViewModel(
private val productId: String,
private val productsRepository: ProductsRepository
) {
val product: StateFlow<Product> ...
fun addToCart() ...
fun showComments() ...
}
class ProductsRepository {
suspend fun getAllProducts(): List<Product>
suspend fun getProduct(id: String): Product
suspend fun searchProduct(name: String): List<Product>
}
class FakeProductsRepository : ProductsRepository {
private val productsCache = Cache<Product>()
override fun getAllProducts(): List<Product> ...
override fun getProduct(id: String): Product ...
override fun searchProduct(name: String): List<Product> ...
}
Packages and Modules
When building a real world application we always try to group our code to be better organized and easier to understand. For this purpose we usually rely on packages and modules. Both of them can be reflected on a diagram in the same way, allowing us to put multiple classes into a single group.
Representing model with a Class Diagram
Defining an architecture using Class Diagram is one great use case for it. Second one is describing the data model. Sometimes we work on the application which is more a thick client than a thin one. In such a case we donât only load data from the backend and display it on the screen. We need to represent some business problems as a model of our application. When building a model there are couple of UML notations which are especially useful.
Association Multiplicity
A Multiplicity of an Association determines how many instances of the class are referenced. By default, when we donât specify any Multiplicity then it means that only one instance is referenced. If we want to reflect different cases we can use the following notation.
0..1
means zero or one instance of the class. We can treat it as an optional reference.*
means zero or more instances of the class. It refers to passing some collection of a given type. This collection can be empty or contain multiple instances of the same class.1..*
means 1 or more instances of this class. Here we can use collection as previously but we need to apply some extra validation to check if collection has at least one element.
data class Product(
// One instance
val price: Price,
// Zero or one instance
val image: Image?,
// Zero or many instances
val comments: List<Comment>,
// One or many instances
val colors: List<Color>,
) {
init {
// Check if we have at least one Color
if (colors.isEmpty()) {
error("Product needs to have at least one Color")
}
}
}
Aggregation and Composition
When thinking about data we also need to take under consideration a lifecycle of this data. For example we have a class Category
which refers to multiple instances of the Product
class.
Now what happens when we delete a Category
object? Should we also remove all the Products
which are referenced by this Category
? Or maybe we want to keep them so they can be moved to a different Category
?
In order to express these concerns UML notation offers two more types of Association: Aggregation and Composition. Both of them are represented as a solid line with a rhombus at the end.
Aggregation means that referenced instances have an independent lifecycle from the parent. Once parent is removed they can still exist in the system. Here the Association rhombus is infilled.
On the other hand the Compositions represented by the filled rhombus indicates that all the referenced instances share the same lifecycle with the parent. Once parent is removed from the system they should be destroyed as well.
â ď¸ In the Aggregation and Composition the rhombus is located on the opposite side of the association â on the owning side, not the referenced side.
Image
can life independently from the Product
. When we remove Product
we still keep an Image
in the system so it can be attached to a different Product
.
Comment
has the same lifecycle as aProduct
. When we remove the Product
there is no point to keep the related Comments
. New product which looks the same might have an improved quality so there will be no point to keep old comments.
UML tools
And at the end I would like recommend you some tools which you can use to create UML diagrams on your own.
Diagrams.net
A free tool which we you can quickly connect to you Google Drive. We can create new diagram directly from the Drive by opening a file creation menu, going to More and selecting a diagrams.net option.
Lucidchart
For those who look for some better alternative I highly recommend to check out the Lucidchart app. In the free version it offers only a limited functionality and files space. However the UX of the editor is very smooth and diagrams look shiny our of the box.