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);
@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