Skip to content

Instantly share code, notes, and snippets.

@mageekguy
Last active August 29, 2015 14:19
Show Gist options
  • Save mageekguy/6f7339723c12b75bfb2d to your computer and use it in GitHub Desktop.
Save mageekguy/6f7339723c12b75bfb2d to your computer and use it in GitHub Desktop.
A teenager want alcool in east oriented manner!
<?php
/*
Un barman peut servir un verre d'alcool à un client :
1) s'il a plus de 18 ans
2) s'il dispose du ou des alcools nécessaires (dans le cas d'un cocktail) en stock.
*/
interface barman
{
function clientWantAlcoholDrinkOfName(client $client, alcoholDrinkName $alcoholDrinkName);
function clientAgeIs(age $age);
}
interface client
{
function barmanIs(barman $barman);
function newAlcoholDrink(drink $drink);
function ageIsRequiredByBarman(barman $barman);
function alcoolDrinkIsRefusedByBarman(barman $barman);
}
final class quantity
{
private
$value
;
function __construct($value)
{
$this->value = $value;
}
function __toString()
{
return (string) $this->value;
}
}
final class age
{
private
$value
;
function __construct($value)
{
$this->value = $value;
}
function __toString()
{
return (string) $this->value;
}
}
final class alcoholDrinkName
{
private
$value
;
function __construct($value)
{
$this->value = (string) $value;
}
function __toString()
{
return $this->value;
}
}
class drink
{
function __construct(alcoholDrinkName $alcoholDrinkName, quantity $quantity)
{
$this->name = $alcoholDrinkName;
$this->quantity = $quantity;
}
}
class legalBarman implements barman
{
private
$clientAgeIsLegal,
$client
;
function clientWantAlcoholDrinkOfName(client $client, alcoholDrinkName $alcoholDrinkName)
{
(new self)->giveAlcoholDrinkOfNameToClient($alcoholDrinkName, $client);
return $this;
}
function clientAgeIs(age $age)
{
if ($this->client)
{
$this->clientAgeIsLegal = (string) $age >= 18;
}
return $this;
}
private function giveAlcoholDrinkOfNameToClient(alcoholDrinkName $alcoholDrinkName, client $client)
{
$this->client = $client;
$this->client->ageIsRequiredByBarman($this);
if ($this->clientAgeIsLegal)
{
$this->client->newAlcoholDrink(new drink($alcoholDrinkName, new quantity(5)));
}
return $this;
}
}
abstract class teenager implements client
{
private
$age,
$drink
;
function __construct()
{
$this->age = rand(14, 18);
}
function alcoolDrinkIsRefusedByBarman(barman $barman)
{
echo 'Fuck you!' . PHP_EOL;
}
function newAlcoholDrink(drink $drink)
{
echo 'Thanks!' . PHP_EOL;
$this->drink = $drink;
return $this;
}
function ageIsRequiredByBarman(barman $barman)
{
$barman->clientAgeIs(new age($this->age + (18 - $this->age)));
return $this;
}
}
class teenagerWhichLikeMojito extends teenager
{
function barmanIs(barman $barman)
{
$barman->clientWantAlcoholDrinkOfName($this, new alcoholDrinkName('Mojito'));
return $this;
}
}
(new teenagerWhichLikeMojito)->barmanIs(new legalBarman);
@mageekguy
Copy link
Author

Thanks for your comments!
Firstly, the point of this gist is (a) AND (b) AND be a a reference to debate about implementation of east oriented code.
Concerning (a), this example is interesting because it's a real abstraction, and OOP is about abstraction. Moreover, everybody has a good perception of the domain, so it's easy to discuss about evolution (serve only not drunk client, and so on).
About "tell can also mean 'inform' as well as 'instruct' ", i'm not agree with you. I think that OOP is declarative, and only declarative, because an object can't be an abstraction if someone (or something, i.e. an another object) known its capabilities (if you know that a barman can take an order, it's not an abstraction, it's something which can take an order, or at least, its abstraction level is less than the abstraction level of something which accept the message "client want alcohol drink of name whiskey", because in this message, all informations is related to the client, and there is no information about the barman).
Using declarative API is a bit weird in PHP or an other language with a C syntax, but very expressive with Smalltalk or Objective-C : barman client: client wantAlcoholDrinkOfName: alcoholDrinkName.
About missing class called "transaction", i think that it is useless. One of the advantage of east oriented code/TDA is inversion of control, so the barman can go in the right state to serve the client when he receive the message "client want alcohol drink of name whiskey". Moreover, the new barman instance is visible only by the client, so, the rest of the world can't update its state during transaction, i.e. no race condition!
Concerning (b), and more specially the question "Why have all those interactions when you can just do everything in one function, where the client orders a drink, presenting their money and credentials in the same method call?", the answer is easy: east oriented code/TDA decrease greatly coupling between classes, so the code is more versatile and easy to update according to new requirement.
You can write something like $barman->clientWantAlcoholDrinkOfName($client, $alcoholDrinkName, $age, $money) but if in the future, the domain need sex of client, or religion, or any other kind of information, you're in dead lock, because to inject these new information, you should break the API.
PS: https://thesecretsquad.wordpress.com/2014/10/25/dazed-and-confuzzled/ is very interesting about OOP.

@grumpyjames
Copy link

Before you read the rest of this: I seriously suggest you try the limits of TDA workshop (it's a small world: it's even linked to from the comments on the post on chess you forwarded). I wrote that because I started to realise that conversations like this one generate more heat than light.

Re: abstraction. Let's say we add that the teenager must also provide their religion. In the one function version, the code flat out doesn't work; we're forced to make a code change in teenager to provide the extra parameter. In the conversational version, the code continues to run, but no-one is ever served a drink until, again, teenager receives a code change to send a further message about their religion.

The real goal is for our objects to interact in a way that results in the correct outcome - and to me that means being honest when two objects are coupled.

"if you know that a barman can take an order, it's not an abstraction"

...is our teenager expecting to send messages to every object in the bar in order to work out which of them are going to serve him a drink, then? ;-) Or does he/she simply not care about the outcome of telling everyone they want a drink? That doesn't sound like any teenager I know :-)

"east oriented code/TDA decrease greatly coupling between classes"

So, for my definition of TDA, I think this is true - but the teenager/barman program breaks the rule that information can only pass from the caller to the callee, as the barman interrogates each teenager for identification, and eventually passes a message (drink or not) back to the teenager. Only if messages are 'fire and forget' has decoupling really been achieved; as soon as we see that the teenager is expecting a response, we know we're not decoupled.

Finally, I am looking forward to watching the barman forking an instance of him/herself in order to serve me a beer the next time I am in my local pub ;-)

@TheSecretSquad
Copy link

@grumpyjames I agree with what you said.

Without a clear direction as to how we can reasonably expect the domain to evolve, it's hard to say what design is better than another. Basically, without a real set of requirements, it's difficult to discuss whether any design is good or bad. I think that's where your Limits of TDA helps.

I agree that the conversational aspect of it (the barman and client talking back and forth about the client's legality) is not ideal. Mimicking the real world is not our goal. I like what you said about 'fire and forget'. I tend to have objects send messages with as much context as they can reasonably have (not context about other objects, but about themselves), which passes the work onto another object.

One of the problems I see with the conversational design is that subtypes of client can acquire new properties that define legality. Lets say different locales require different laws and service rules. Updating the client interface requires updating the barman interface. Subtypes of barman who operate in locales that don't require this information from clients are still forced to implement it.

As the amount of properties needed to check a client increases, the amount of information the barman needs to store about clients increases. This is a lot of context about the client. The barman should be focused on coordinating drink service and other high level bar tasks. Perhaps initiating a check for legality is one of those tasks, but I think we would be better off finding a way to push the implementation to a collaborating object.

@mageekguy
Copy link
Author

Thanks to all for their comments!
To grumpyjames: If the teenager must also provide its religion to a barman, the method religionIsRequiredByBarman(barman $barman) should be add to the client interface, and the method clientReligionIs(religion $religion) should be add to the barman interface. All classes which implements client interface should be updated accordingly, but the developer should only add a new method, not modifying already exists methods. And it's the same for all classes which implements the barman interface. Moreover, the language will throw a warning if one of classes which implements these interfaces does not respect them, so the developer will be notify of any update in the conversational protocol and will be able to add the missing methods. From my point of view, it's pretty near the open/close principle.
Moreover, the conversational aspect imply that the caller has the control: in our context, to forward information to the barman, the client should respect its interface (the client SHOULD provide an age instance, so if the client handle its age differently internally (for example with a simple float), it's not a problem for the barman, and the client can be change its age abstraction without any impact for the barman.
And conversational aspect respect "fire and forget" philosophy: the client instance can not inject its age in the barman instance, it's not a problem for the barman because the barman has a fallback strategy in this case
To TheSecretSquad: it's the barman responsibility to check constraints before serve the client, not the client responsibility, because the client does not know the barman's domain. And a collaborating object is a good idea, but the interface between the client and this collaborating object will be always the barman, so…

@grumpyjames
Copy link

@TheSecretSquad : great! Good to know I'm not totally incomprehensible.

@mageekguy:

"If the teenager must also provide its religion to a barman, the method religionIsRequiredByBarman(barman $barman) should be add to the client interface, and the method clientReligionIs(religion $religion) should be add to the barman interface. All classes which implements client interface should be updated accordingly."

This is the very definition of strong coupling - to add a new feature we have to change both sides. We can't really talk about open/closed with two classes that work so closely together.

Let's compare (excuse the syntax; I haven't written any php for over ten years):

One function before:

// this is in teenager
function barmanIsAvailable($barman) {
     $barman->onDrinkRequest($this, mojito, $this->age);
}

// this is in barman
function onDrinkRequest($client, $drink, $age) {
     if ($age > 18) {
         $client->onDrink(makeDrink($drink)); 
     } else {
         $client->drinkRequestRefused();
     }  
}

one function after

function barmanIsAvailable($barman) {
     $barman->onDrinkRequest($this, mojito, $this->age, $this->religion);
}

function onDrinkRequest($client, $drink, $age, $religion) {
     if ($age > 18 && religion->permitsAlcohol()) { // technically this is not TDA, see note at the end
         $client->onDrink(makeDrink($drink)); 
     } else {
         $client->drinkRequestRefused();
     }  
}

conversation before (I'll skip the implementation)

interface barman {
    onDrinkRequest(client, drink);
    clientAgeIs(age);
}

interface teenager {
    // I'll skip the drink callbacks;
    onAgeRequest(barman);
}

conversation after

interface barman {
    onDrinkRequest(client, drink);
    clientAgeIs(age);
    clientReligionIs(religion);
}

interface teenager {
    // I'll skip the drink callbacks;
    onAgeRequest(barman);
    onReligionRequest(barman);
}

Both approaches change both caller and callee simultaneously, neither case preserves a working system until both teenager and barman have been updated (java wouldn't compile, I imagine php throws a runtime error of some sort).

We might be able to make this open closed if certain bits of ascertaining legality were optional, and we had an abstract way of talking about each bit of verification (although you'd have to use double dispatch to keep this logic inside the barman, unless you are in something loosely typed where method dispatch is dynamic).

re: the conversation suggesting the caller has control. In a pure tell don't ask system, control can only ever be held by the object currently acting on a message. Both approaches end up passing the barman exactly the same amount of information in the end (one could argue that the conversational approach might save an underage drinker having to reveal their religion, but that's not exactly high on my list of things to optimize for...).

Making religion tda

I'm just playing here, this is not meant to be a good design, just showing where this principle can lead if you apply it blindly.

before:

     if ($age > 18 && religion->permitsAlcohol()) { // technically this is not TDA, see note at the end
         $client->onDrink(makeDrink($drink)); 
     } else {
         $client->drinkRequestRefused();
     } 

after

    if ($age > 18 && religion->permitsAlcohol()) { // technically this is not TDA, see note at the end
         $religion->passDrinkIfAllowed($client, makeDrink($drink)); 
     } else {
         $client->drinkRequestRefused();
     }

// religion gains this method
    function passDrinkIfAllowed($client, $drink) {
         if ($this->permitsAlcohol) $client->onDrink($drink) else $client->drinkRequestRefused();
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment