DEV Community

loading...
Cover image for Object Design Style Guide Summary

Object Design Style Guide Summary

migueldevelopez profile image MiguelDevelopez Updated on ・7 min read

How you should create and use your objects

I’m currently reading an interesting book called Object Design Style Guide, wrote by Matthias Noback, about how to create objects as best as possible, so I decided to bring it up here and show you some tips and directives I found more interesting. Of course, I recommend you read the whole book if you want to go deep into this topic.
image

1º Introduction to some OOP concepts

In this book, inheritance plays a small role, even though it’s supposed to be one of the pillars of object-oriented programming. In practice, using inheritance mostly leads to a confusing design.
In this book, we’ll use inheritance mainly in two situations:

  • When defining interfaces for dependencies (dependency injection and inversion).
  • When defining a hierarchy of objects, such as when defining custom exceptions that extend from built-in exception classes. In most other cases we’d want to actively prevent developers to extend from our classes. You can do so by adding the final keyword in front of the class. It will be more explained later. The composition is highly recommended over the inheritance.

A little space to the test about that the basic structure of the unit test is:
The basic structure of each test method is Arrange - Act - Assert:
1 Arrange: Bring the object that we’re testing (also known as the SUT, or Subject Under Test) into a certain known state.
2 Act: Call one of its methods.
3 Assert: Make some assertions about the end state.

Shit's getting real. There are 2 types of objects:

  • Service objects that either perform a task or return a piece of information. Objects of the first type will be created once, and then be used any number of times, but nothing can be changed about them. They have a very simple lifecycle. Once they’ve been created, they can run forever, like little machines with specific tasks. These kinds of services are immutables. Service objects are do-ers, and they often have names indicating what they do: renderer, calculator, repository, dispatcher, etc.
  • Objects that will hold some data, and optionally expose some behavior for manipulating or retrieving that data, this kind is used by the first type to complete their tasks. These objects are the materials that the services work with. There are two subtypes: value objects and models/entities, but don't get ahead of ourselves.

2º Focus on Services

There is a pile of suggestions about how a service should be, I’ll make the list hiper-summarizing them:

  • To create a service use dependency injection to make the service ready for use immediately after instantiation and test double. So the dependencies should be declared explicitly. Here is an example of this, you can see how the parameter $appConfig is only used to get the directory of the cache so… Instead of injecting the whole configuration object, make sure you only inject the values that the service actually needs.
interface Logger
{
    public function log(string $message): void;
}

final class FileLogger implements Logger
{
    private Formatter $formatter;

    // Formatter is a dependency of FileLogger
    public function __ construct(Formatter $formatter) 
    {
        $this->formatter = $formatter;
    }

    public function log(string $message): void
    {
        formattedMessage = $this->formatter->format($message) ;
        // ….
    }
}
Enter fullscreen mode Exit fullscreen mode
  • When possible you should keep together all the related configuration values that belong together. Service shouldn’t get the entire global configuration object injected, only the values that it needs.

WRONG WAY

final class MySQLTableGateway
{
    public function __construct(
        string $host,
        int $port,
        string $username,
        string $password,
        string $database,
        string $table
    ) {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

GOOD WAY

final class MySQLTableGateway
{
    public function __construct(
        ConnectionConfiguration $connectionConfiguration,
        string $table
    ) {
        // $table is the name of the table, It isn’t necessary to make the connection 
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Avoid service locators (a service from which you can retrieve other services) and inject the dependencies that you need explicitly.
  • All the constructor arguments should be required because the code will be unnecessarily complicated. If you have the temptation to put it as an optional dependency you can use the null object.
  • Services should be immutable, that is, impossible to change after they have been fully instantiated because the behavior could be so unpredictable.

So... avoid something like this:

final class EventDispatcher
{
    private array $listeners = [];

    public function addListener(
        string $event,
        callable $listener
    ): void {
        $this->listeners[event][] = $listener;
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Only assign properties or throw exceptions because of some validation error in the constructor.
final class FileLogger implements Logger
{
    private string $logFilePath;

    public function __construct(string $logFilePath)
    {
        // $logFilePath should be properly set up, so we just need a safety check
        if (! is_writable($logFilePath)) {
            throw new InvalidArgumentException(
                'Log file path  . $logFilePath .  should be writable
            );
        }
        $this->logFilePath = $logFilePath;
    }

    public function log(string message): void
    {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Ideally, create objects to avoid the hidden dependencies, for example, the function json_encode() or a class from PHP like DateTime

WRONG WAY

final class ResponseFactory
{
    public function createApiResponse(array $data): Response
    {
        // json_encode is a hidden dependency
        return new Response(
        json_encode(
            $data,
            JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT), ['Content-Type' => 'application/json']
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

GOOD WAY

final class JsonEncoder
{
    / **
    * throws RuntimeException
    */
    public function encode(array $data): string
    {
        try {
            return json_encode(
                $data,
                JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT
            );
        // we can throw our own exception, with more specific info
        } catch (RuntimeException previous) {
            throw new RuntimeException(
                'Failed to encode data: ' . var_export($data, true),
                0,
                previous
            );
        }
    }
}

final class ResponseFactory
{
    private JsonEncoder $jsonEncoder;

    // JsonEncoder can be injected as a dependency
    public function __construct(JsonEncoder $jsonEncoder)
    {
        $this->jsonEncoder = $jsonEncoder;
    }

    public function createApiResponse(data): Response
    {
        return new Response($this->jsonEncoder->encode($data));
    }
}
Enter fullscreen mode Exit fullscreen mode

And you can do the same with the date() and big core utilities of your language, and your application layer will be so decoupled.

3º other objects

3.1 value object and model/entity

The main suggestions are:

  • Validate the objects in the constructor, it will assure you that you only have valid objects in your application, every object will be what it was intended to be. You should throw exceptions in the constructor in case the data is not valid. The book suggests avoiding using custom exceptions for invalid argument exceptions, for this kind of RuntimeExceptions indicates that… more about it below.
final class Coordinates
{
    public function _construct(float $latitude, float $longitude)
    {
        if ($latitude > 90 || $latitude < -90) {
            throw new InvalidArgumentException(
                'Latitude should be between -90 and 90'
            );
        }
        $this->latitude = $latitude;
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Don't use property fillers, later we’ll see where you can use them with an example.
  • Entity/model should be identifiable with a unique id, value objects don’t because they only wrap one or more primitive-type values.
  • To add more semantic to a constructor the named constructors appear, those are static methods with domain-specific names that allow your code to have a better name than the typical new class().

Standard way

$salesOrder = new SalesOrder();
Enter fullscreen mode Exit fullscreen mode

Better way

$salesOrder = SalesOrder::place();
Enter fullscreen mode Exit fullscreen mode

You can put the __construct method as private to avoid using it and call the constructor inside the place() method.

final class DecimalValue
{
    private int value;
    private int precision;

    private function __construct(int $value, int $precision)
    {
        this.value = value;
        Assertion.greaterOrEqualThan($precision, 0);
        $this->precision = $precision;
    }

    public static function fromInt(
        int $value,
        int $precision
    ): DecimalValue {
        return new DecimalValue($value, $precision);
    }

    public static function fromFloat(
        float $value,
        int $precision
    ): DecimalValue (
        return new DecimalValue(
            (int) round($value * pow(10, precision)),
            $precision
        ):
    }
}
Enter fullscreen mode Exit fullscreen mode
  • One of the best points of the value objects is that, if you validate in their constructor, when you see a value object you will know that it contains validated information and you will not have to validate this information in other points of the code.
  • Test the behavior of an object and the constructor in the way it will fail, don't create a test just to check if the values are correct.
public function it_can_be_constructed(): void
{
    $coordinates = new Coordinates(60.0, 100.0);
    assertEquals(60.0, $coordinates->latitude());
    assertEquals(100.0, $coordinates->longitude());
}
Enter fullscreen mode Exit fullscreen mode
  • In summary, a value object does not only represent domain concepts. They can occur anywhere in the application. A value object is an immutable object that wraps primitive-type values.

3.2 DTO (Data transfer object)

The rules of 3.1 don't fit well with this type of object, the DTO. While in the value object and model we want consistency and validity of the data, in the DTO we just want (nobody expected it by the name) to transfer data from one point to another.

  • A DTO can be created using a regular constructor.
  • Its properties can be set one by one.
  • All of its properties are exposed, so make them public and access them directly without getters.
  • Its properties contain only primitive-type values.
  • Properties can optionally contain other DTOs or simple arrays of DTOs.
  • You can use property fillers when needed.
final class ScheduleMeetup
{
    public string $title;
    public string $date;

    public static function fromFormData(
        array $formData
    ): ScheduleMeetup {
        $scheduleMeetup = new ScheduleMeetup();
        $scheduleMeetup->title = $formData['title'];
        $scheduleMeetup->date = $formData['date'];

        return $scheduleMeetup;
    }
}
Enter fullscreen mode Exit fullscreen mode

Well, the objects have methods where they contain the behavior, there are two kinds of methods, queries to retrieve information and commands to perform a task, but both of them could be designed with the same ‘template’, that is:
1º Checking parameters, throwing errors if there is something wrong.
2º Do what the method has to do, throwing errors if necessary.
3º Check postcondition checks. This won’t be necessary if you have good tests, but for example, if you are working with legacy code it could be good for safety checks.
4º Returns if is a query method.

You’ve seen that the exceptions are a good part of a code, some cases where using a custom exception is very useful:
1º If you want to catch a specific exception type higher up

try {
    // possibly throws ‘SomeSpecific’ exception
} catch (SomeSpecific $exception) {
    // …
}
Enter fullscreen mode Exit fullscreen mode

2º If there are multiple ways to instantiate a single type of exception

final class CouldNotDeliverOrder extends RuntimeException
{
    public static function itWasAlreadyDelivered(): CouldNotDeliverOrder
    {
        // ...
    }
    public static function insufficientQuantitiesInStock(): CouldNotDeliverOrder
    {
         //...
    }
}
Enter fullscreen mode Exit fullscreen mode

3º If you want to use named constructors for instantiating the exception

final class CouldNotFindProduct extends RuntimeException
{
    public static function withId(
        ProductId $productld
    ): CouldNotFindProduct (
        return new CouldNotFindProduct('Could not find a product with ID . $productld);
    }
}
throw CouldNotFindProduct .withId(/* ... */);
Enter fullscreen mode Exit fullscreen mode

And you don’t have to put ‘Exception’ in the name of the exception class, instead, use explicit names like InvalidEmailAddress or CouldNotFindProduct.

And that’s all folks, there are soooo many more examples in the book, so I encourage you to take a look at it. If you want the second part of the book, let me know in the comments.

Sources and more info

Discussion (5)

Collapse
lluismf profile image
Lluís Josep Martínez

"In practice, using inheritance mostly leads to a confusing design." => aqui he dejado de leer

Collapse
migueldevelopez profile image
MiguelDevelopez Author • Edited

I personally agree with Noback's opinion on this one, I prefer composition by far, but it would be great if you share with me the reasons that you disagree.

Collapse
lluismf profile image
Lluís Josep Martínez

Inheritance only leads to a confusing design if misused. It's still a powerful tool for OOP, you simply need to know when and why to use it and do it wisely. There are crappy OOP designs either with inheritance AND composition. Lots of good arguments here softwareengineering.stackexchange....

Thread Thread
leocavalcante profile image
Leo Cavalcante

"need to know when and why to use" - This argument can be applied to literally everything, then there is no point in discussing at all. Even something like, Idk, global vars for example, you can say "Well, you just need to know when and why to use". The eval() function: "Well, you just need to know when and why to use". Gotos statements: "Well, you just need to know when and why to use".
Literally everything can go bad if misused, that is a weak argument IMO.

The point is that Inheritance easily leads to misuse which causes confusion and makes it an error prone tool. Good tools should avoid users to make mistakes and the point is that Inheritance isn't one of this tools, that is what the author means by "mostly leads to".

Thread Thread
lluismf profile image
Lluís Josep Martínez

Well, I don't apply it to literally everything, that's a poor argument. I still think that inheritance is a very powerful tool, as is polymorphism. You can't have one without the other. If you don't want to use it because you think is bad, it's your choice. I have literally hundreds of use cases in which it's exactly the tool to use.

Forem Open with the Forem app