Published on

Representing knowledge: what, how and why

Authors

A problem

Imagine that we work in a software system which deals with appliances.

There are various types of appliances, each with its own set of properties and behaviors.

One of the most interesting attributes of an appliance is its connectivity with other appliances.

Let's assume it was already modeled and works perfectly.

But now, things changed (did they or it's just our understanding of those "things"?).

Turns out that some of the applicances include other appliances - making them "a container".

How to model that?

A collaborative modeling session

Imagine we gathered the entire team, including the domain experts, to discuss this, learn together and solve it together.

That's what real teams do, right? (you might be interested in reading about The ambiguity of team work).

After some cycles, we decided to model it in a really easy way so that as we undergo the real usage, from which we will learn from, then we shall evolve it.

The rule was pretty easy to understand: an appliance is a container if it is connected to any appliance of a type of "TYP-990" or "TYP-P0-1".

"Container-ism" was immediately used in one of the parts of the system to make a decision about a specific way of showing the attribute of "being" a container.

if(appliance.Connections.Any(c => 
    c.ConnectedAppliance.Type == "TYP-990" ||
    c.ConnectedAppliance.Type == "TYP-P0-1"
))
{
    // ...some important consequences
}

As all agreed, it was a good start. The rule was directly translated into code so everyone understood where did it come from.

Non-IT experts, who were present, were happy to see how their knowledge was directly used in the system - they were able to follow it too!

Amazing, isn't it?

Thanks to Connections public property, we were able to easily check if an appliance is a container.

It is used in other places of the system so it was a natural candidate to utilize it further.

Someone suggested we could put some comment there to explain we deal with a container under those circumstances.

if(appliance.Connections.Any(c => 
    c.ConnectedAppliance.Type == "TYP-990" ||
    c.ConnectedAppliance.Type == "TYP-P0-1"
)) // a container
{
    // ...some important consequences
}

Even better, now someone looking at this piece of code will immediately grasp the underlying concept.

Changes, changes never change.

As we were working on the system, we discovered that the rule was not that simple.

It turned out that there might be appliances that do not have any connections at all but still are containers.

Both type and size of the appliance were important pieces of information to determine if it is a container, provided that it has no connections.

Unfortunately, there was no "size" property on the Appliance class, so we needed to use dimensions to determine it.

We rushed to the code to make proper changes and express the new rule.

if(
appliance.Connections.Any(c => 
    c.ConnectedAppliance.Type == "TYP-990" ||
    c.ConnectedAppliance.Type == "TYP-P0-1"
) ||
(!appliance.Connections.Any() && (appliance.Dimensions.Width > 100 && appliance.Dimensions.Height > 100 && appliance.Dimensions.Depth > 100)) ||
(!appliance.Connections.Any() && (appliance.Type == "TYP-100" || appliance.Type == "TYP-101" || appliance.Type == "TYP-X0-1"))
) // a container
{
    // ...some important consequences
}

Well, it got a bit ugly. But at least the comment is there to explain what is going on.

All those pesky old systems!

There was additional problem - as we are integrating with an older system of ours, we discovered that those rules might apply and a given applicance might not be a container.

Fortunately for us, all appliances having a prefix M999- in its name determine that it comes from the old system - then for sure such appliance can't be a container and we should treat it as a regular one.

We needed to add this information to our rule.

if(
    (appliance.Connections.Any(c => 
        c.ConnectedAppliance.Type == "TYP-990" ||
        c.ConnectedAppliance.Type == "TYP-P0-1"
    ) ||
    (!appliance.Connections.Any() && (appliance.Dimensions.Width > 100 && appliance.Dimensions.Height > 100 && appliance.Dimensions.Depth > 100)) ||
    (!appliance.Connections.Any() && (appliance.Type == "TYP-100" || appliance.Type == "TYP-101" || appliance.Type == "TYP-X0-1"))) &&
    (!appliance.Name.Contains("M999-"));
) // a container
{
    // ...some important consequences
}

Well, our tiny rule evolved into something like...This.

When a senior speaks up

It was decided - this piece of code gets messy and hard to understand - even though we had our little comment (which saved our asses many times!)

One of the seniors suggested that we should extract this rule to a separate method.

Fortunately to us, C# allows expressing such rule as a property, as it was not requiring any other information to determine "container-ism".

Couple of keyboard shortcuts and we had it.

// somewhere in `Appliance` class
public bool IsContainer => 
    (appliance.Connections.Any(c => 
        c.ConnectedAppliance.Type == "TYP-990" ||
        c.ConnectedAppliance.Type == "TYP-P0-1"
    ) ||
    (!appliance.Connections.Any() && (appliance.Dimensions.Width > 100 && appliance.Dimensions.Height > 100 && appliance.Dimensions.Depth > 100)) ||
    (!appliance.Connections.Any() && (appliance.Type == "TYP-100" || appliance.Type == "TYP-101" || appliance.Type == "TYP-X0-1"))) &&
    (!appliance.Name.Contains("M999-"));

// and this is how our well known if statement looks like now
if(appliance.IsContainer)
{
    // ...some important consequences
}

Glory!

Victory!

The code started serving its capabilities even better than before.

So readable, so understandable, easy to maintain!

What, changes?

As we all know, if something has changed before has a higher probability of changing again.

Same happened to our rule.

As with the older system, appliances imported from a partners' systems also have special rules for determining if an appliance is a container.

However, this time it wasn't that easy.

It was Name property that was a target for checking, but each partner had its own unique name that got embedded as a prefix.

A suggestion from domain experts was to firstly check if the appliance is from a partner system and then apply the rule.

if(partners.Any(p => !appliance.Name.Contains(p.Name))
    && appliance.IsContainer)
{
    // ...some important consequences
}

It wasn't that readable anymore. Maybe another structure of the code would be better?

if(appliance.IsContainer(partners.Select(p => p.Name)))
{
    // ...some important consequences
}

It looks a bit weird that we need to pass the names of the partners to the IsContainer method but it does the job.

We decided to leave it as it is, for now.

Of course, we did not forget to update the IsContainer property, but let's stop here, dear Reader.

Time to reflect on what the hell just happened.

Code?

As you probably know, dear Reader, this code is just a toy example and no real software systems have such code in them.

Everything is readable and straightforward, right?

I hope you got my cynical tone.

"Damian, do you think I am that naive? No one writes code like this!", one might state.

Nothing is wrong with writing such code, in the first place.

"Every complex system that works is evolved from a simple system that worked", as John Gall stated once (I believe it was in the "The Systems Bible").

Same with our "code", right?

It was a subject of a natural evolution as we learned along the journey.

As you might remember, dear Reader, we typically deal with The ambiguity of code - and it's about something more profound.

The knowledge.

The knowledge and its application (you might be interested in The ambiguity of application).

As we saw during this tiny tale, it all started from a simple rule on how to determine if an appliance is a container.

Then it grew, grew and grew.

It was a natural process of learning and applying the knowledge - but the knowledge was actually leaking here, leaking there.

Putting the "a container" comment was a small exaggeration, but I believe one can imagine that this might have actually happened - as it was easy, right? (and we already know that Simple isn't easy).

Knowledge?

During the entire process, the team learned about various ways of determining "container-ism" - expressing an abstract idea, a concept of something being a container.

Some of those ways were related to other systems, some to the domain itself.

If one recaps, we started with how to determine if an appliance is a container.

By initially by adding a comment - "a container" - we represented what it is all about.

Then this what became a legitimate programming construct - a property.

And even though it was eventually expressed in the code by handy IsContainer property (which can be naturally translated to a method/a function/a field, in other languages than C#), we didn't capture details on why things are the way they are.

Nothing was mentioned about different reasons for determining "container-ism" - and that might hurt us in the future.

It is not "bad" to start with implementation details (how), as it is a natural way of learning as the understanding emerges in front of our eyes.

But we shouldn't stop there.

As we got to the point when we know what an abstract idea or a concept we are supposed to represent, let's go for it.

If suddenly we discover that this what has different whys behind it's hows, we should capture meaningful and sometimes subtle details, as they might be crucial for building the correct understanding.

What, how and why

We might not yet know how to represent something but we know what we want to represent - as in this case, the "container-ism".

I once heard a term "programming with wishful thinking", so focusing on capturing this what - as we don't need to know how it will be provided.

Naturally, we will step into how's territory - an implementation - but at some level of thinking it's just a secondary concern.

Why?

By articulating, so specifying the what we want to represent, we "drive" this how, using the inner knowledge we just built.

"Drive"? It might sound familiar to you, dear Reader, as I deliberately refer to Test-Driven Development (btw. it's not about "tests", if you're interested, check The ambiguity of TDD).

Of course, eventually, we might encounter dragons and behemoths, so it might be a good idea to capture the why - probably by placing comments.

Comments have their time and place, so don't be afraid to use them, especially when capturing why!

And this why might be anything: a description, a simple explanation, a reference to an external document or a github issue.

Start with how too!

It all sounds easy - start with what, follow with how, and if needed, capture why.

But is it really that easy?

There are many things that Alex Bunardzic mentioned and that sticked with me. One of them is this little and big gem:

Conclusion 🔍

It's not possible to understand the problem before we try the solution.

This might mean that to get to the what, we need to start with how.

(please note dear Reader that there's no "always"!)

As I understand those wise words, it's about the process of learning - as we jump into the arena of solving problems, we learn about the problem itself.

And as we saw during this tiny tale, it's all fine to grab all those hows and let whats to emerge.

Hows should be transformed into whats.

Language, abstractions and modeling

All of this sounds like a pixie dust, isn't it?

Should you always start with what? Cynically, I would respond: yes my acolyte, you should.

I hope you know it already, dear Reader, that I am afraid of dangerous quantifiers like "always", "never" and so on.

I don't belive there's a silver bullet, "a best practice" or "a golden standard" to achieve the Holy Grail of modeling (whatever actually it is).

Still, it's tempting to say that starting with specifying what is a good idea - as it involves working with Language of the problem and putting focus on the domain.

Every move incurrs The cost of modeling and it's up to us to decide what makes sense, now.

Conclusion 🔍

Start with what, continuously follow with how, and if needed, capture why.

This still might sound "magical" and one might reason on how to apply this knowledge (pun intended) in the real world problems - and it's a good question.

I believe that awareness is all of what we have so being aware of "what, how and why" might be a nice opener.

Next time dear Reader, when dealing with a problem and trying to find a solution, consider asking yourself those questions:

Question 🤔

What an abstract idea or a concept do I try to represent?

How can it be represented?

Why is it represented in this particular way?

Why are there various ways of representing it?

And wait for more interesting questions to emerge.

Happy modeling!