OOP design principles

In this section, we will look at some of the problems with the OOP approach and OOP solutions and FP solutions to these problems.

In general, OOP is being criticized in the following manner:

  • Binding a data structure to behavior is a mechanism of state encapsulation that hides the underlying problem instead of solving it.
  • A great deal of effort goes into making inheritance possible. Ironically, object-orientated patterns themselves favor composition over inheritance. Ultimately, in handling two responsibilities—subtyping and reusing—inheritance is not good with either subtyping or reusing.

OOP solutions to these problems include SOLID and DDD principles. The following are the SOLID principles:

  • The single responsibility principle (SRP)
  • The open/closed principle (OCP)
  • The Liskov substitution principle (LSP)
  • The interface segregation principle (ISP)
  • The dependency inversion principle (DIP)

Domain-driven Design (DDD) principles are proposed to solve OOP problems.

Also, FP addresses these problems by the following distinguishing characteristics:

  • Explicit management of state is avoided through immutability
  • Explicit return values are favored over implicit side-effects
  • Powerful composition facilities promote reuse without compromising encapsulation
  • The culmination of these characteristics is a more declarative paradigm

SRP

The SRP states that every class should have a single responsibility where a responsibility is defined as a reason to change.

This principle supports the anti-pattern where large classes play multiple roles. Classes can be large for a few reasons. A core principle of OOP is the binding of the data structure to behavior. The problem is that optimizing for data structure encapsulation not only weakens composition characteristics, but also hides the underlying problem of explicit state. As a result, OOP code typically contains many data structures with relatively few functions per data structure. Adding methods to a class brings pressure on the SRP and reducing the number of methods can either make the data structure difficult to compose or altogether useless. Furthermore, the simple syntactical cost of declaring a class often compels programmers to marginalize.

The FP counterpart

In FP, the fundamental unit of abstraction is the function. Given that a function has a single output, functions naturally have a single responsibility. One could certainly define an arbitrarily generic function, though this would not be intuitive. Moreover, functions are syntactically less resource-hungry.

OCP

The OCP states that software entities should be open for extension but closed for modification.

The ambiguity of this statement can be resolved through two variations of the principle:

  • Existing classes should be modified only in order to correct bugs. This restriction delivers the closed aspect of the principle. The open aspect is delivered through implementation inheritance or, in other words, inheritance with the goal of reusing rather than subtyping.
  • Openness through polymorphism, which by definition also provides for closure, as extensibility is supported through substitution rather than modification. Unfortunately, substitution often leads to accidental complexity, which must be addressed by yet another principle—the LSP.

The primary utility of the OCP is the confinement of cascading changes while providing extensibility. This is achieved by designing for extensibility and prohibiting changes to existing entities. Extensibility is attained by fancy tricks with abstract classes and virtual functions. Closure is achieved by encapsulation or rather by the hiding of moving parts.

The FP counterpart

In FP, functions can be substituted at will and as such, there is no need to design for extensibility. Functionality requiring parameterization is naturally declared as such. Instead of inventing a concept of a virtual method and inheritance, one can rely on an existing, elementary concept—the higher-order function.

LSP

The LSP states that objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

The LSP is essentially a restricted instance of subtyping, which aims to guarantee semantic portability across class hierarchies. Portability is achieved by ensuring that whatever is true of a base type is also true of all subtypes. Subclasses must not strengthen preconditions. They must accept all input and initial states that the base class accepts and subclasses must not weaken post-conditions. Behavioral expectations declared by the super class must be met by the subclass. These characteristics cannot be enforced by the type system alone.

The LSP as a relation of inheritance is thus deceptive, hence the need for a compensating principle. As such, the need for this principle demonstrates a pitfall in subtype (inclusion-based) polymorphism. Implicit factoring by class hierarchy imposes the needless inclusion of restrictions and requires complex principles to place a boundary on accidental complexity.

The FP counterpart

Functional languages favor parametric polymorphism with bounded quantification, thereby avoiding some of the pitfalls of inheritance. Informally, functional languages emphasize substitutability and de-emphasize implementation reuse as reuse is better achieved through composition. Most ambitions of the LSP are effectively trivial in FP languages.

ISP

The ISP states that many client-specific interfaces are better than one general-purpose interface. In other words, no client should be forced to depend on methods that it does not use.

In essence, ISP is a restatement of the SRP for interfaces and reflects the same underlying problem—the difficulty of balancing responsibility assignment, composition, and encapsulation in object-oriented design. On the one hand, it is desirable to encapsulate; on the other hand, it is desirable to compose. Furthermore, the problem with employing the ISP alone is that it doesn't directly protect against large classes and in some ways hides the problem.

The FP counterpart

Functional programming reduces the need for encapsulation by eschewing state and breeds composition at the core. There is no augmented concept of role-based interfaces because function roles are explicit at the onset. Functions are segregated by default.

DIP

The DIP states that one should depend upon abstractions. Do not depend upon concretions. In other words, high-level modules should be decoupled from low-level modules through abstractions. This principle states that code should be structured around the problem domain, and the domain should declare dependencies on required infrastructure as protocols. Dependencies thus point inward to the domain model.

The reason that this principle is an inversion is because typical architectures promoted by OOP (via layer architecture) exhibit dependency graphs where high-level modules consume low-level modules directly. Initially, this dependency graph seems natural as, in expressing domain models in code, one inevitably depends upon the constructs of the language. Procedural programming allows dependencies to be encapsulated by procedures.

Subtype polymorphism defers procedure implementation. Unfortunately, the use of protocols is often overlooked to express domain dependencies in OOP implementations. Given that infrastructure code is typically more voluminous, the focus of the code drifts away from the domain. DDD was devised in part to balance this drift.

The FP counterpart

The declarative and side-effect-free nature of FP provides dependency inversion. In OOP, high-level modules depend on infrastructure modules primarily to invoke side-effects. In FP, side-effects are more naturally triggered in response to domain behavior as opposed to being directly invoked by domain behavior. Thus, dependencies become not merely inverted, but pushed to outer layers altogether.

DDD

DDD is an approach to software development for complex needs by connecting the implementation to an evolving model.

Concepts

Concepts of the model include the following:

  • Context: The setting in which a word or statement appears that determines its meaning.
  • Domain: An ontology, influence, or activity. The subject area to which the user applies a program is the domain of the software.
  • Model: A system of abstractions that describes selected aspects of a domain and can be used to solve problems related to that domain.
  • Ubiquitous language: A language structured around the domain model and used by all team members to connect all the activities of the team with the software.

Premise

The premise of DDD is as follows:

  • Placing the project's primary focus on the core domain and domain logic
  • Basing complex designs on a model of the domain
  • Initiating a creative collaboration between technical and domain experts to iteratively refine a conceptual model that addresses particular domain problems

Building blocks

In DDD, there are artifacts to express, create, and retrieve domain models that are explored from an FP perspective in the following sections.

Aggregate

A collection of objects that are bound together by a root entity, otherwise known as an aggregate root. The aggregate root guarantees the consistency of changes being made within the aggregate by forbidding external objects from holding references to its members.

The concept of the aggregate remains in FP; however, it is not represented in terms of a class. Instead, it can be expressed as a structure, including a set of aggregate states, initial state, set of commands, set of events, and function-mapping the set of commands to the set of events given a state. Cohesion is provided by a module mechanism.

Immutable value objects

Immutable value objects are objects that contain attributes but have no conceptual identity. They should be treated as immutable.

In a previous chapter, we saw that Swift provides immutable product and sum types with auto-implemented structural equality, which addresses this pattern trivially. Heavy reliance on state in OOP makes references first-class citizens rather than the structure of the data itself.

Domain events

A domain event is a domain object that defines an event.

Domain events are powerful mechanisms to keep domain models encapsulated. This can be accomplished by allowing various observers from outer layers to register for a domain event (signal).

The problem with domain events in OOP is that the typical implementation is complex and relies on side-effects. Event observations are typically declared in the composition root and thus, it is not immediately obvious from the perspective of the producer which observers will be invoked. In FP, a domain event is simply a value returned by a function in an aggregate. Observers can be explicitly registered as filters.

Furthermore, FRP can handle domain events very effectively. On the other hand, returning domain events from aggregate methods in OOP is prohibitive due to the lack of union types and pattern matching.

Intention-revealing interface

In imperative OOP code, intent leaks through side-effects and focuses on the how rather than the what. Always having to bind behavior to the data structure can also be problematic.

As FP is more declarative, function names and interfaces tend to be more focused on intent rather than the the underlying mechanics. In addition, the interfaces of side-effect-free functions are by nature more revealing because behavior is made explicit through the return value. As a result, in addition to the purely linguistic benefit of naming with intent, intent is also encoded by the type system. This is not to say that expressing intent is effortless in FP—only that it is better supported by the FP paradigm.

Side-effect-free functions

Side-effects are in direct opposition to encapsulation, yet all too often they are the most useful tools.

Unlike imperative programming, FP avoids side-effects. This pattern is yet another example of how a well-crafted object-oriented design converges upon a functional style.

Assertions

Like many patterns rooted in imperative object-oriented design, assertions claim to use implicit side-effects.

As with intention-revealing interfaces, assertions in FP languages are automatically encoded in the return type of a function in addition to the function name.

Conceptual contours

Conceptual contours emerge when domain knowledge is spread throughout the code to a sufficient degree. In OOP, this can be achieved by carefully following the principles of DDD.

In FP, conceptual contours emerge more readily, once again due to the declarative and side-effect-free nature of the paradigm. Specifically, clients of the domain model can rely on cohesive functionality attained with composition and yet still have access to constituents without breaking encapsulation.

Closure of operations

Closure of operations illustrates yet another example of coercing composition and structure upon object-oriented designs.

Essentially, closure simplifies reasoning about a problem by restricting the domain of the discourse. The example of a functional implementation of a domain exhibits this characteristic at a fundamental level. The operation of applying a domain event is closed under the set of domain states. In terms of persistence, this naturally translates to event-sourcing but also supports persistence in a key-value store or ORM with no required modification.

Declarative design

The overall intent of the aforementioned patterns is to cultivate a declarative design. As witnessed, FP is inherently more declarative and therefore more accommodating in this regard. Through declarative design, we can distill distinguishing characteristics of the domain better and reduce or eliminate coupling to orthogonal concerns of infrastructure. Consequently, re-usability, testability, correctness, maintainability, and productivity are tremendously enhanced.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset