I'm starting to believe that one should focus on the workflow instead. After all, the reason for writing software as a tool is to simplify or automate some existing process. So focus on what people are already doing and what they're trying to achieve and find a way to remove repetitions, unnecessary delays, etc. Just look on what people are doing and find a way to do some parts of the process faster and better.
"As a user, I want to store my food and drinks in the refrigerator so they don't spoil"
"As a user, I want to store drinks in the refrigerator so I can drink them cold"
"As a user, I want to store my medicine in the refrigerator so it can stay below 25 C"
This requires buy-in from different parts of the company that are going to have their own opinions about how to do things. They can be surprising resistant to change.
We can develop a spec in a vacuum, but we can't be sure what users actually want from our refrigerator. Maybe a live penguin actually does belong in there, as in the famous children's novel.
Wikipedia has an example of a tag soup https://en.wikipedia.org/wiki/Tag_soup which is probably poster child example of this law:
<p>This is a malformed fragment of <em>HTML.</p></em>
What do we do here? Do we do what browsers have historically done? Or do we throw our hands up in the air when we see malformed tags?
The term "contained" above is restrictive, and probably needs further precision. Only things that thrive inside an opaque and sealed container would fit the definition above. Otherwise, things that can affect the quality of other items (i.e., gas emanation), things that need light, things that need oxygen, things that need a specific range of moisture, things that can grow and/or move, would all need their containers to provide these conditions and/or have a dedicated location inside the refrigerator.
* Jello that needs to be set
* Sprouted beans / seeds for which you want to restrict the rate of growth
Baking soda and charcoal deodorizer have been omitted because of the assumption that containers were sealed.
In terms of software, trying to decide up front for the user what they can do is the wrong approach. Even constraining them is flawed, with the exception of constraints for liability issues. What the user needs is the meas to express their decision making as to whether something should go into the fridge, so they need an input mechanism capable of doing selection, which has access to the context in which the item might be being placed into the fridge.
Does this sound like any configuration format you've ever used? The typical key-value stores which attempt to give you a declarative list of what you can put where, without the means to compute whether or not it might be a good idea first are almost ubiquitous, and the number of programs which take the right approach: give the user a proper, expressive programming language to configure the software, are few.
Just as it happens, we've developed ACME Refrigerator v2.0, which now allows you to store chocolate bars in it; a feature we missed in v1.0. Upgrade now for just $400.
The absurity of this idea with a fridge seems lost in software because we can upgrade at almost zero cost in just a few minutes, but that's only masking what is an obvious design flaw.
Which raises the question: What can you put in a software package definition?
I had a textbook that started to explain OOP by basically saying "A dog is a class. A dog has a tail that can be in a wagging state or not. A dog has a name..."
And I'm sitting there thinking "No, dog's don't 'have' names. We call them names and they respond to names. They sometimes know their name. It's a different kind of 'have' than having a tail. I have a name for my dog. Should the person class have a dogs_name member variable now?" A tail is a subset of a dog. A name is a property of the abstract system in which the dog resides.
A class is a model of something else. You can model a dog with an object, but you can't know how good of a model it is unless you dig down into it and see where it breaks down. It will break down, because software is an abstraction, and abstractions, being abstract, are not the things they are abstractions of. And there are not many things that behave like OO objects because they are far too simple to represent reality in any high-fidelity sense. And it's dishonest to pretend they do.
Not to say there isn't some pedagogical advantage to using the analogy, I just think a better one could be used. Like talking about messages or something.
Your example is modeling a real dog better than the sample you started with, but... what computer programs acts on a real, live dog, directly? Computer programs pretty much never interact directly with external reality, only with other software -- interfaces to hardware and other software systems. So software needs to be smart and clean about dealing with the actual interfaces. It's important to understand how reality will interact with the interfaces, of course, but relationships there are often irrelevant or misleading to designing the software.
So all of these poor students, when actually charged with designing a program dealing with, say cashiers, store managers, purchases, reports, etc., start off "well, cashiers and store managers are people, and actually both employees... I guess we need a company object in here", etc.. This almost always leads them into a mess, because they get lost in here before they even learn properly what the goals of their system are. E.g., this program just needs to run on the store managers PC, pull data from a dir full of files from the POS, and generate reports (in which the cashier is just another column value).
If they started OO as "modeling reality", just writing a program that does what's needed feels like cheating ("TODO: not really capturing the OO relationships here! rework for v2") -- and that's a bad thing.
This remark has a place in the discussion, but not the one you probably think.
scan label
if (grep "keep refridgerated after opening") AND (status OPEN) then
store in fridge else
ask
...but then I realised this is the AI contextual knowledge problem.It's the same with pasteurized milk. Unpasteurized milk is mostly safe to drink, but occasionally you get really sick. In a large population the difference is noticeable.
Again the same with the much stricter cleanliness rules for restaurants and the like. Usually it's safe to store prepared food without refrigeration for a day, restaurants mustn't. It's also mostly safe to cook on a dirty stove, restaurants mustn't. The difference between your home kitchen and the restaurant is the number of people served.
There are literary thousands of things we refrigerate that don't come in packages with neat "refrigirate" labels.
(More on-topic,) I think the point presented still holds. It's not possible to make an air-tight spec for absolutely everything.
"What should one store in a refrigerator?" is a better question.
In principle, you could dump all your clothes, toiletries, etc. in a pile on the airport checkin counter, and if system were organized enough, it could deliver all your stuff to hawaii and lay it all out on the hotel bed for you.
E.g., a robot would scan all your belongings at the airport, and dump them all into a big pipe... each item would get properly routed to your hotel room... kinda like how the internets work!
Is it just a question of organization? "Everything in this suitcase belongs to this person and should move together to the same destination."
The problem is in your questions. "Purpose" is not something that is a part of an item. It is a part of our mind.
I love this insight.
Using this analogy, isn't a suitcase roughly equivalent to a packet? It's tagged with source/destination, and basically standardised so the routing gear doesn't need to care about the specific items you are routing.
At certain points along a route, deep packet inspection is performed, and some packets will be modified to remove contents that the country in question does not want to route.
Example: eric lippert tries to model an RPG in OOP and tries to model the character "uses" command: http://ericlippert.com/2015/05/11/wizards-and-warriors-part-...
(The latter requirement rules out not only liquids but also e.g. a pile of salt or rice.)
The goal should never be to describe every single outcome or scenario- the goal should be to build understanding between business/product/development team so that they understand the point of the work that the software replaces. It's no different with hardware or really any new product creation. The goal is to create a conversation that gets us to solving the problem- when the spec gets to covering all cases the chances we get this wrong and stray from solving the actual problem goes up massively.
Also it's starting a conversation about the very topic..
The outdated approach of if (food.canSpoil()) { refrigerator.put(food) } was too burdensome and did not conform to all of the best practices.
A specification should be for a solution to a problem - "We need a way to keep xxxxx cool". That problem defines what needs to go into the refrigerator from the outset.
What's the actual problem space here ?
"Everything is vague to a degree you do not realize till you have tried to make it precise."
- Joey Tribbiani, Friends episode 10.11
-------------------
Scala solves this problem really well with typeclasses.
You can define a typeclass like
case class Refrigerated[T](thing: T) // this is just something holding things that have been refrigerated.
trait Refrigeratable[T] { // this is the typeclass
def refrigerate(thing: T): Refrigerated[T]
}
Now we have a refrigerated object that holds some object that's been refrigerated, and it looks like I'm about to do some class composition, but I'm not. Bear with me.Now, we have some sort of data class
case class Pillow(fluffiness: Int, warmness: Int)
Pretty straight forward, we now have a Pillow with measurable fluffiness and warmthNext, lets implement refrigerated for pillow.
implicit val pillowRefrigerable: Refrigerable[Pillow] = new Refrigerable[Pillow] {
def refrigerate(pillow: Pillow): Refrigerated[T] =
Refrigerated(pillow.copy(warmness = pillow.warmness / 2))
}
So there's an implementation of Refrigerable for pillow. Refrigerating a pillow just cuts the warmness in half.So actually using the Refrigerable stuff. There's two ways to go about this. One way is to just use the Refrigerable instance to refrigerate the pillow.
def refrigeratePillow(pillow: Pillow): Refrigerated[Pillow] = pillowRefrigerable.refrigerate(pillow)
Nothing special there. You could basically do that in any language. So how does scala do it better? Well, we can define a refrigerate method for anything Refrigerable like this: def refrigerate[T](thing: T)(implicit refrigerableInstance: Refrigerable[T]): Refrigerated[T] =
refrigerableInstance.refrigerate(thing)
Now this is really cool. Anything type that we have a Refrigerable instance for, can be used as an argument for refrigerate. And we don't have to explicitly state what Refrigerable instance we're using. Calling it looks like val refrigeratedThing = refrigerate(anythingWithARefrigerableInstanceHere)
Okay. We still haven't gained a whole lot. All we've done so far, is make it so we don't have to explicitly state what Refrigerable to use. So let's go an extra step. Let's make it so we just call refrigerate on the Object itself rather than passing the object as an argument to a method. This is possible, because scala let's us make extension methods for generics. implicit class RefrigerableOps[T](thing: T)(implicit refrigerableInstance: Refrigerable[T]) {
def refrigerate: Refrigerated[T] = refrigerableInstance.refrigerate(thing)
}
Okay, now this is really cool. We've defined a method for any type for which we have Refrigerable in scope. This means we can now refrigerate our pillow more easily: val refrigeratedPillow = myPillow.refrigerate
What?! That's great! And it gets better. Now that we've implemented this boilerplate, giving other things this wonderful syntax is easy. Let's say I want to refrigerate my cousin, because she's too hot. case class Cousin(hotness: Int)
implicit val refrigerableCousin: Refrigerable[Cousin] = new Refrigerable[Cousin] {
def refrigerate(cousin: Cousin) =
Refrigerated(cousin.copy(hotness = cousin.hotness - 1))
}
Now we can make our cousin less hot! Great! val refrigeratedCousin = cousin.refrigerate
This is basically a good way to make things open to extension without subclassing it's awesome!You can even do more to simplify making the Refrigerable instances. You can effectively make them one liners.