Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save douglasselias/d1826b46cd19e14a35ed145d1b98017a to your computer and use it in GitHub Desktop.
Save douglasselias/d1826b46cd19e14a35ed145d1b98017a to your computer and use it in GitHub Desktop.

Design Patterns are garbage

This is mostly a rant about some quotes from the book. Don't expect long chains of arguments, only brief commentary and personal reflections.

Yet experienced object-oriented designers do make good designs. Meanwhile new designers are overwhelmed by the options available and tend to fall back on non-object-oriented techniques they’ve used before. It takes a long time for novices to learn what good object-oriented design is all about.

Not true, OOP leads to bad design.

Christopher Alexander says, “Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice”

Even though Alexander was talking about patterns in buildings and towns, what he says is true about object-oriented design patterns. Our solutions are expressed in terms of objects and interfaces instead of walls and doors, but at the core of both kinds of patterns is a solution to a problem in a context.

Software is not tangible like buildings! There is no correlation between them!

2º principle: Favor object composition over class inheritance.

Ignore one of the pillars of OOP.

Here are some common causes of redesign along with the design pattern(s) that address them:

Creating an object by specifying a class explicitly. Specifying a class name when you create an object commits you to a particular implementation instead of a particular interface. This commitment can complicate future changes. To avoid it, create objects indirectly. Design patterns: Abstract Factory (87), Factory Method (107), Prototype (117).

This problem is specific of OOP.

Dependence on specific operations. When you specify a particular operation, you commit to one way of satisfying a request. By avoiding hard-coded requests, you make it easier to change the way a request gets satisfied both at compile-time and at run-time. Design patterns: Chain of Responsibility (223), Command (233).

Why is this a problem? What exactly is the problem?

Dependence on hardware and software platform. External operating system interfaces and application programming interfaces (APIs) are different on different hardware and software platforms. Software that depends on a particular platform will be harder to port to other platforms. It may even be difficult to keep it up to date on its native platform. It’s important therefore to design your system to limit its platform dependencies. Design patterns: Abstract Factory (87), Bridge (151).

You don't need Design Patterns to create abstractions for hardware or software platforms.

Dependence on object representations or implementations. Clients that know how an object is represented, stored, located, or implemented might need to be changed when the object changes. Hiding this information from clients keeps changes from cascading. Design patterns: Abstract Factory (87), Bridge (151), Memento (283), Proxy (207).

Why is this a problem?

Algorithmic dependencies. Algorithms are often extended, optimized, and replaced during development and reuse. Objects that depend on an algorithm will have to change when the algorithm changes. Therefore algorithms that are likely to change should be isolated. Design patterns: Builder (97), Iterator (257), Strategy (315), Template Method (325), Visitor (331).

Why is this a problem? What exactly is the problem?

Tight coupling. Classes that are tightly coupled are hard to reuse in isolation, since they depend on each other. Design patterns: Abstract Factory (87), Bridge (151), Chain of Responsibility (223), Command (233), Facade (185), Mediator (273), Observer (293).

You don't need Design Patterns to reduce coupling. Not always there is a way, or need to reduce coupling.

Extending functionality by subclassing. Customizing an object by subclassing often isn’t easy. Every new class has a fixed implementation overhead (initialization, finalization, etc.). Defining a subclass also requires an in-depth understanding of the parent class. For example, overriding one operation might require overriding another. An overridden operation might be required to call an inherited operation. And subclassing can lead to an explosion of classes, because you might have to introduce many new subclasses for even a simple extension. Design patterns: Bridge (151), Chain of Responsibility (223), Composite (163), Decorator (175), Observer (293), Strategy (315).

This problem is specific of OOP.

Inability to alter classes conveniently. Sometimes you have to modify a class that can’t be modified conveniently. Perhaps you need the source code and don’t have it (as may be the case with a commercial class library). Or maybe any change would require modifying lots of existing subclasses. Design patterns offer ways to modify classes in such circumstances. Design patterns: Adapter (139), Decorator (175), Visitor (331).

This problem is specific of OOP.


Paul R. Calder and Mark A. Linton. The object-oriented implementation of a document editor. In Object-Oriented Programming Systems, Languages, and Applications Conference Proceedings, pages 154-165, Vancouver, British Columbia, Canada, October 1992. ACM Press.

We will examine seven problems in Lexi’s design:

  1. Document structure. The choice of internal representation for the document affects nearly every aspect of Lexi’s design. All editing, formatting, displaying, and textual analysis will require traversing the representation. The way we organize this information will impact the design of the rest of the application.
  1. Formatting. How does Lexi actually arrange text and graphics into lines and columns? What objects are responsible for carrying out different formatting policies? How do these policies interact with the document’s internal representation?
  1. Embellishing the user interface. Lexi’s user interface includes scroll bars, borders, and drop shadows that embellish the WYSIWYG document interface. Such embellishments are likely to change as Lexi’s user interface evolves. Hence it’s important to be able to add and remove embellishments easily without affecting the rest of the application.
  1. Supporting multiple look-and-feel standards. Lexi should adapt easily to different look-and-feel standards such as Motif and Presentation Manager (PM) without major modification.
  1. Supporting multiple window systems. Different look-and-feel standards are usually implemented on different window systems. Lexi’s design should be as independent of the window system as possible.
  1. User operations. Users control Lexi through various user interfaces, including buttons and pull-down menus. The functionality behind these interfaces is scattered throughout the objects in the application. The challenge here is to provide a uniform mechanism both for accessing this scattered functionality and for undoing its effects.
  1. Spelling checking and hyphenation. How does Lexi support analytical operations such as checking for misspelled words and determining hyphenation points? How can we minimize the number of classes we have to modify to add a new analytical operation?

We’ve applied eight different patterns to Lexi’s design:

  1. Composite (163) to represent the document’s physical structure,
  2. Strategy (315) to allow different formatting algorithms,
  3. Decorator (175) for embellishing the user interface,
  4. Abstract Factory (87) for supporting multiple look-and-feel standards,
  5. Bridge (151) to allow multiple windowing platforms,
  6. Command (233) for undoable user operations,
  7. Iterator (257) for accessing and traversing object structures, and
  8. Visitor (331) for allowing an open-ended number of analytical capabilities without complicating the document structure’s implementation.

None of this challenges need Design Patterns.

You should focus on data structure and algorithms, like the VS Code team: https://code.visualstudio.com/blogs/2018/03/23/text-buffer-reimplementation


Patterns

Abstract Factory

ET++ [WGM88] uses the Abstract Factory pattern to achieve portability across different window systems (X Windows and SunView, for example). The WindowSystem abstract base class defines the interface for creating objects that represent window system resources (MakeWindow, MakeFont, MakeColor, for example). Concrete subclasses implement the interfaces for a specific window system. At run-time, ET++ creates an instance of a concrete WindowSystem subclass that creates concrete system resource objects.

Overengineer.

Builder

Applied to this example, the Builder pattern separates the algorithm for interpreting a textual format (that is, the parser for RTF documents) from how a converted format gets created and represented.

Overengineer.

Factory Method

The Unidraw graphical editing framework [VL90] uses this approach for reconstructing objects saved on disk. Unidraw defines a Creator class with a factory method Create that takes a class identifier as an argument. The class identifier specifies the class to instantiate. When Unidraw saves an object to disk, it writes out the class identifier first and then its instance variables. When it reconstructs the object from disk, it reads the class identifier first.

A more esoteric example in Smalltalk-80 is the factory method parserClass defined by Behavior (a superclass of all objects representing classes). This enables a class to use a customized parser for its source code. For example, a client can define a class SQLParser to analyze the source code of a class with embedded SQL statements. The Behavior class implements parserClass to return the standard Smalltalk Parser class. A class that includes embedded SQL statements overrides this method (as a class method) and returns the SQLParser class.

Overengineer.

Prototype

Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.

So in our music editor, each tool for creating a music object is an instance of GraphicTool that’s initialized with a different prototype. Each GraphicTool instance will produce a music object by cloning its prototype and adding the clone to the score.

Overengineer. It's just copying data.

Use the Prototype pattern when a system should be independent of how its products are created, composed, and represented

Why???

Singleton

Ensure a class only has one instance, and provide a global point of access to it.

Overengineer. It's just a global variable.

Structural Patterns

Adapter

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.

Basic common sense to adapt one interface/library for your software.

Bridge

Decouple an abstraction from its implementation so that the two can vary independently.

Consider the implementation of a portable Window abstraction in a user interface toolkit. This abstraction should enable us to write applications that work on both the X Window

Overengineer. In C you can use header files to solve this.

Composite

Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

The RTL Smalltalk compiler framework [JML92] uses the Composite pattern extensively. RTLExpression is a Component class for parse trees. It has subclasses, such as BinaryExpression, that contain child RTLExpression objects.

It's just a tree, not a pattern.

Decorator

Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Sometimes we want to add responsibilities to individual objects, not to an entire class. A graphical user interface toolkit, for example, should let you add properties like borders or behaviors like scrolling to any user interface component.

Overengineer.

Facade

Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.

Consider for example a programming environment that gives applications access to its compiler subsystem. This subsystem contains classes such as Scanner, Parser, ProgramNode, BytecodeStream, and ProgramNodeBuilder that implement the compiler. Some specialized applications might need to access these classes directly. But most clients of a compiler generally don’t care about details like parsing and code generation; they merely want to compile some code. For them, the powerful but low-level interfaces in the compiler subsystem only complicate their task.

To provide a higher-level interface that can shield clients from these classes, the compiler subsystem also includes a Compiler class. This class defines a unified interface to the compiler’s functionality. The Compiler class acts as a facade: It offers clients a single, simple interface to the compiler subsystem.

It's just commmon sense abstracting a set of operations in one function.

Flyweight

Use sharing to support large numbers of fine-grained objects efficiently.

ET++ [WGM88] uses flyweights to support look-and-feel independence.5 The look-and-feel standard affects the layout of user interface elements (e.g., scroll bars, buttons, menus—known collectively as “widgets”) and their decorations (e.g., shadows, beveling). A widget delegates all its layout and drawing behavior to a separate Layout object. Changing the Layout object changes the look and feel, even at run-time.

It's just a basic optimization technique, not a pattern.

Proxy

Provide a surrogate or placeholder for another object to control access to it.

One reason for controlling access to an object is to defer the full cost of its creation and initialization until we actually need to use it. Consider a document editor that can embed graphical objects in a document. Some graphical objects, like large raster images, can be expensive to create. But opening a document should be fast, so we should avoid creating all the expensive objects at once when the document is opened. This isn’t necessary anyway, because not all of these objects will be visible in the document at the same time.

It's just lazy loading, a basic optimization technique.

Behavioral Patterns

Chain of Responsibility

Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.

Consider a context-sensitive help facility for a graphical user interface. The user can obtain help information on any part of the interface just by clicking on it. The help that’s provided depends on the part of the interface that’s selected and its context; for example, a button widget in a dialog box might have different help information than a similar button in the main window. If no specific help information exists for that part of the interface, then the help system should display a more general help message about the immediate context—the dialog box as a whole, for example.

This example is overengineer. It's just function composition or pipeline.

Command

Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

parameterize objects by an action to perform, as Menultem objects did above. You can express such parameterization in a procedural language with a callback function, that is, a function that’s registered somewhere to be called at a later point. Commands are an object-oriented replacement for callbacks.

support undo. The Command’s Execute operation can store state for reversing its effects in the command itself. The Command interface must have an added Unexecute operation that reverses the effects of a previous call to Execute. Executed commands are stored in a history list. Unlimited-level undo and redo is achieved by traversing this list backwards and forwards calling Unexecute and Execute, respectively.

It's just a function. As mentioned, undo and redo only needs a list or a stack.

Interpreter

Given a language, define a represention for its grammar along with an interpreter that uses the representation to interpret sentences in the language.

Creating the abstract syntax tree. The Interpreter pattern doesn’t explain how to create an abstract syntax tree. In other words, it doesn’t address parsing. The abstract syntax tree can be created by a table-driven parser, by a hand-crafted (usually recursive descent) parser, or directly by the client.

This is not a pattern this is a program! So compilers are patterns too? Nonsense!

Iterator

Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

to provide a uniform interface for traversing different aggregate structures (that is, to support polymorphic iteration).

When you actually need to be generic? If you know the data structure you are working with then there is no need for this 'pattern'. It doesn't solve a real problem.

Mediator

Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.

Often there are dependencies between the widgets in the dialog. For example, a button gets disabled when a certain entry field is empty. Selecting an entry in a list of choices called a list box might change the contents of an entry field. Conversely, typing text into the entry field might automatically select one or more corresponding entries in the list box. Once text appears in the entry field, other buttons may become enabled that let the user do something with the text, such as changing or deleting the thing to which it refers.

Overengineering.

Memento

Without violating encapsulation, capture and externalize an object’s internal state so that the object can be restored to this state later.

In general, the ConstraintSolver’s public interface might be insufficient to allow precise reversal of its effects on other objects. The undo mechanism must work more closely with ConstraintSolver to reestablish previous state, but we should also avoid exposing the ConstraintSolver’s internals to the undo mechanism.

Abiding to weird rules of OOP design. Overengineering.

Observer

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Yeah, in fact observer is just a way to register function, which will be called after another function.

State

Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.

Overengineering. But is similar to FSM.

Strategy

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

It's just function pointers and first-class functions.

Template Method

Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.

Overengineering.

Visitor

Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

Overengineering.

(CLOS actually supports multiple dispatch.) Languages that support double- or multiple dispatch lessen the need for the Visitor pattern.

Book Conclusion

It’s possible to argue that this book hasn’t accomplished much. After all, it doesn’t present any algorithms or programming techniques that haven’t been used before. It doesn’t give a rigorous method for designing systems, nor does it develop a new theory of design—it just documents existing designs. You could conclude that it makes a reasonable tutorial, perhaps, but it certainly can’t offer much to an experienced object-oriented designer.

Once you’ve absorbed the design patterns in this book, your design vocabulary will almost certainly change. You will speak directly in terms of the names of the design patterns. You’ll find yourself saying things like, “Let’s use an Observer here,” or, “Let’s make a Strategy out of these classes.”

'Let's overengineer this.'

People learning object-oriented programming often complain that the systems they’re working with use inheritance in convoluted ways and that it’s difficult to follow the flow of control. In large part this is because they do not understand the design patterns in the system. Learning these design patterns will help you understand existing object-oriented systems.

Have you considered that OOP that leads to bad design?

Learning these patterns will help a novice act more like an expert.

How this helps? By overengineering?

Moreover, describing a system in terms of the design patterns that it uses will make it a lot easier to understand. Otherwise, people will have to reverse-engineer the design to unearth the patterns it uses.

This is because design patterns are overengineering!

Data structures and algorithms are much more fundamental than design patterns.

Conclusion

A better book would be showing the development of the Lexi editor. Showing the initial design and its flaws, its potential solutions, then solving with design patterns, preferably with all of them.

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