Revisiting the personal asset management use case

When designing reusable software, we often create abstractions as data types and associate behaviors with them. One way to model behaviors is to leverage a type hierarchy. Following the Liskov Substitution Principle, we should be able to substitute a type with a subtype when a function is called.

Let's revisit the abstract type hierarchy of managing personal assets from Chapter 2Modules, Packages, and Type Concepts:

We can define a function called value for determining the value of any asset. Such a function can be applied to all the types in the Asset hierarchy if we assume that all the asset types have some kind of monetary value attached to them. Following that line of thought, we can say that almost every asset exhibits the HasValue trait.

Sometimes, behaviors can only be applied to certain types in the hierarchy. For example, what if we want to define a trade function that only works with liquid investments? In that case, we would define trade functions for Investment and Cash but not for House and Apartments.

A liquid investment refers to a security instrument that can be traded easily in the open market. The investor can quickly convert a liquid instrument into cash and vice versa. In general, most investors would like a portion of their investment to be liquid in the case of an emergency.

Investments that are not liquid are called illiquid.

Programmatically, how do we know which asset types are liquid? One way is to check the type of the object against a list of types that represent liquid investments. Suppose that we have an array of assets and need to find out which one can be traded quickly for cash. In this situation, the code may look something like this:

function show_tradable_assets(assets::Vector{Asset})
for asset in assets
if asset isa Investment || asset isa Cash
println("Yes, I can trade ", asset)
else
println("Sorry, ", asset, " is not tradable")
end
end
end

The if condition in the preceding code is a bit ugly, even in this toy example. If we have more types in the condition, then it gets worse. Of course, we can create a union type to make it a little better:

const LiquidInvestments = Union{Investment, Cash}

function show_tradable_assets(assets::Vector{Asset})
for asset in assets
if asset isa LiquidInvestments
println("Yes, I can trade ", asset)
else
println("Sorry, ", asset, " is not tradable")
end
end
end

There are a few issues with this approach:

  • The union type has to be updated whenever we add a new liquid asset type. This kind of maintenance is bad from a design perspective because the programmer must remember to update this union type whenever a new type is added to the system.
  • This union type is not available for extension. If other developers want to reuse our trading library, then they may want to add new asset types. However, they cannot change our definition of the union type because they do not own the source code. 
  • The if-then-else logic may be repeated in many places in our source, whenever we need to do things differently for liquid and illiquid assets.

These problems can be solved using the holy traits pattern.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.24.134