Object Oriented Programming - ttulka/programming GitHub Wiki
Good programmers write simple code. (Yegor Bugayenko)
Building Blocks
All existing OOP languages encourage us to treat objects as "data structures with attached procedures", which is a totally wrong and dangerous misconception.
In an ideal OOP language we would have classes and functions inside them. Not Java methods as micro-procedures, which we have now, but true functions in a pure FP paradigm with a single exit point.
In pure OOP we don't need operators inherited from procedural languages like C. We don't need if
, for
and switch
. We need classes If
, For
, and Switch
.
Class is a factory, active manager of objects. Not a template of objects - a template is passive.
- A class should be named by what it is, not what it does.
- A properly designed class must not have any public methods that don't implement at least one interface.
- Utility classes are a terrible anti-pattern in OOP.
- Don't use singletons. Just encapsulate the "singleton" object in all objects that may need it.
Object is a representative of its encapsulated data. Not a connector between its inner and outside world. Not a collection of procedures to call to manipulate the data inside.
- Data structures vs. objects: Data structures are transparent, while objects are solid. Data structures are glass boxes, while objects are black boxes. Data structures are passive, while objects are active.
- An object should not do too much or know too much.
- Objects are not and should not be aware of their clients.
- Objects are defined by "contract". They don't violate their contract. [a]
- All data is private. If it's not private, that it's part of the contract and can't be changed. [a]
- An object is defined by what it can do, not by how it does. [a]
- Looking at an object from the outside, you should have no idea how it's implemented. [a]
- Never ask an object for information that you need to do something; rather, ask the object that has the information to do the work for you. [a]
- It must be possible to make any change to the way an object is implemented, no matter how significant that change, by modifying the single class that defines that object. [a]
Method is the name given to the block of code that is executed in response to a specific message. Each message in an object's protocol must have a corresponding method.
Domain might be a business enterprise or a type of business, A domain might be nothing more than a focused community of objects collectively providing a particular set of services.
- It's never appropriate to tell yourself: "This is what the code will look like." - understand the domain first.
- The longer a software developer works in a domain, the more effective his software work will be.
Application is a community of objects focused on accomplishing a well-defined set of collective responsibilities.
Factory Pattern, in Java, works as an extension to the new
operator. In the end, we still use the new
operator, but conceptually, there is not much difference. In a perfect OOP language, this functionality would be available in the new
operator.
- You don't need OO encapsulation with immutability, OO encapsulation is a feature for mutability.
- You only need to encapsulate whatever is changing.
Object Thinking
The main disadvantage of procedural systems is in debugging and maintenance. The shared data creates "coupling" relationships (undesirable dependencies) between subroutines. When you change one subroutine, you affect others. [a]
Object-oriented systems are networks of intercooperating agents that communicate by means of some messaging system. The objects are peers - there's no one object that's clearly in charge, issuing directives to the other objects. [a]
OO systems are usually more complex than procedural systems but easier to maintain. The idea is to organize the inevitable complexity inherent in real computer programs, not to eliminate it. [a]
-
Objects are not something you do; objects are a way that you think.
-
People are a harder problem to address than tools, techniques, and process.
-
It is no longer possible to believe that either method or process (or both) is an adequate substitute for better people.
-
The true difference among programming languages are those that reflect philosophical ideals and values.
-
Even if the programming language is OOP benign, it is still possible to write nonobject code.
-
Just using a particular language (like Java) doesn't mean you build objects; only object thinking about decomposition assures that you build objects.
-
Classes with "controller" or "manager" in the name are a sign of traditional thinking over object thinking.
-
Isomorphism of the modules (objects) in problem and solution space is essential quality for software.
-
Object identification and responsibility assignment are based on the structure of the problem domain rather then on the structure of some potential computer program solution.
-
If you think about design using an implementation language, your designing will be enhanced or restricted by that language.
-
Problem domain is modeled and decomposed without consideration of how would be implemented.
-
If decomposition is based on a "natural" partitioning of the domain, the resultant models and software components will be simpler to implement. If, instead, decomposition is based "artificial", or computer-derived, abstractions such as memory structures, operations, or functions, the opposite results will accrue.
-
Only if performance mandates cannot be satisfied with effective design is it appropriate to consider selecting a language based on whether it provides more direct access to and control of hardware.
A bottle is a thing, while a number is an idea. The power of OOP is that it lets you model ideas.
Four Presuppositions to Object Thinking
- Everything is an object.
- Simulation of a problem domain drives object discovery and definition.
- Object must be composable.
- Distributed cooperation and communication must replace hierarchical centralized control.
Everything is an Object
Any decomposition, however complicated the domain, will result in the identification of a relatively few kinds of objects and only objects. There is nothing "left over" that is not an object.
- Relationships would themselves become just another kind of object.
- Data: Because everything is an object, there is no data. Whatever manipulations and transformations are required of an object, even a character, are realized by that object itself instead of some other kind of thing (a procedure) acting upon that object.
- Procedures are organized collections of messages, and both the collection and the message are nothing more than ordinary objects.
Domain-Driven Object Discovery and Definition
- Both data and function are poor choices for being a decompositon tool. Use behavior instead.
- Behavior is the key to finding the natural joints in the real world.
Consider a cat and a tiger. How do they differ? Why do we have separate names for them? One can hurt us, the other provides companionship. It is this behavior that causes us to make the distinction. It is not attributes, because both have eye color, number of feet, tail length, and so on. The value of those attributes if quite different, but the attribute set remains relatively constant.
Responsibility is used to aid in discovering who (which object) should be charged with a task without the need to think about the object's structure. We should not know object's structure!
- Responsibilities are characterized from the point of view of a potential client of a service.
- Responsibilities are not functions, although there is a superficial resemblance. The easiest way to differentiate between a responsibility and a function is to remember that a responsibility reflects expectations in the problem space (domain), while a function reflects an implementation detail in the solution space (computer program).
Objects must be Composable
-
The purpose and capabilities of the object are clearly stated.
-
Language common to the domain.
-
The capabilities of an object do not vary as a function of the context in which it is used.
-
Reuse is not a goal of object thinking; composability is. Composable objects will be reused as a matter of course, so reuse is but a byproduct of a more general goal.
Distributed Cooperation and Communication
- Unlike puppet modules, objects are autonomous.
- Eliminating centralized control is one of the hardest lessons to be learned by objects developers.
Objects vs Data
There are two kinds of reusable things. Each have unique traits when it comes to reusability and encapsulation, and each require unique approach.
Data tend to provide as much as possible to the outside, hiding only those details, that compromise it's consistency.
Objects strive to solve certain single problem, hiding all the details and internals of how the problem is solved.
The maintainability of a program is inversely proportional to the amount of data that flows between objects. [a]
- You can't get rid of all data movement, particularly in an environment where several objects have to collaborate to accomplish some task, but you should try to minimize data flow as much as possible. [a]
Encapsulation
One of the most fundamental concepts in OO is to isolate the behavior you want to vary.
Encapsulation defines the "insides" (structural definition and enabling mechanism) of an object as private space to or of which no one except the object itself should have access or knowledge.
Encapsulation implies more than respecting the public/private boundary. Object users should not make assumptions about what is behind the barrier either.
- Integrity of objects should not be violated.
Extra flexibility is unavoidable in boundary APIs, so the boundary-layer classes are loaded with accessor and mutator methods. [a]
- The closer you get to the procedural boundary of an OO system (database, UI, etc), the harder it is to hide implementation. The judicious use of accessors and mutators has a place in this boundary layer. [a]
Business Requirement
A business requirement might be satisfied by an individual object or by a group of cooperating objects. Those objects can be human, mechanical, or software based.
If a business requirement can be satisfied with by a single object, it becomes a responsibility of that object. If a group of objects is required, the business requirement is most likely to be expressed as a set of individual object responsibilities plus a script that ensures the proper coordinated invocation of those responsibilities.
A story, as used in XP, is a synonym for business requirement.
Abstraction, Generalization, Classification and Essentialism
Abstraction is the act of separation characteristics into the relevant and irrelevant to facilitate focusing on the relevant without distraction or undue complexity.
- "Keep things simple by not providing abstractions until the abstractions provide simplicity." -- Kent Beck
- The abstraction should arise from refactoring rather than from activities typically associated with the analysis and design.
Generalization also involves separation but more in terms of shared and not shared characteristics.
Classification involves (depending on the linguistic theory) either separation based on conformance to a set definition or similarity to a prototype - the separation being in the class or not.
Essentialism is an attempt to separate accident and essence - to identify the characteristic that is essential to being considered an instance of some class.
The Impedance Mismatch Problem
-
Database philosophy is almost totally inconsistent with the philosophy behind object thinking.
-
Databases violate the axiom that everything is an object. They violate the principle that no object can do things to another object. Databases celebrate centralized control.
-
Objects are not defined based on their attributes, but entities and the tables that they inhabit in a database are defined in precisely that manner.
-
Objects have variables that can contain arbitrarily complex objects, including collections and multivalued constructs. Databases can store only a defined, and limited, set of primitive types.
-
There is no assurance that an object's value in the application is consistent with its value in the database.
-
Databases do not preserve the encapsulation of an object.
Null References
All hatred towards null
is driven by the fact, that it's a huge hole in the type system, that detonates trust in a reference you accept.
From that perspective, Optional
doesn't violate anything. It's not the same as null
, because if you, for instance, state Optional
argument in your method, you will be forced by the type system to handle the optional nature of it. With plain reference and null
support, you can omit the "handle" part and bump into the NPE.
Elegant Objects
- Never use -er names
- Make one constructor primary
- Keep constructors code-free
- Encapsulate something at the very least
- Always use interfaces
- Choose method names carefully
- Builders (suppliers) are nouns (
InputStream stream(URL url)
,String content(File f)
,int sum (int a, int b
) - Manipulators are verbs (
void paint(Color color)
)
- Builders (suppliers) are nouns (
- Don't use public constrains (objects should not share anything)
- Be immutable (sometimes we can't - like lazy loading in Java)
- Failure atomicity (either complete and solid or a failure)
- Lack of temporal coupling
- Side effect-free
- No
NULL
references - Thread safety
- Smaller and simpler
- Write tests instead of documentation
- Don't tell, demonstrate
- Don't mock, use fakes
- Mocking is a bad practice and should be used as a last resort
- Don't write your tests to satisfy "fake" classes
- Keep interfaces short
- Expose fewer than five public methods
- Don't use static methods
- Never accept
NULL
arguments- If we have objects and no pointers, why do we have
null
in Java? - Don't pollute your code with extra checks.
NullPointerException
is a proper indicator of an incorrectly passedNULL
argument.
- If we have objects and no pointers, why do we have
- Never return
NULL
- Be loyal and immutable, or constant
- Never use getters and setters
- Getters and setters are convenient instrument for violating the principle of encapsulation in OOP
- No matter what is inside the implementation of your getters and setters, they are data and they represent data, not behavior
- Don't use
new
outside of constructors - Avoid type introspection and casting
- Catch, chain and rethrow
- Don't catch exceptions unless you really have to, and there is no other choice, rethrow them
- Always chain exceptions
- Recovery only once at the highest level (there is no such thing as recovery except in the main application)
- Be either final or abstract
- Inheritance, intuitively, is a top-down process, where child classes inherit code from parent classes. Method overriding makes it possible for a parent class to access the code of a child class. Such reverse thinking goes against common sense.
class Document {
public int length() { return this.content().length(); }
public byte[] content() { ... } // when overridden in a child, impacts the parent's `length()`
}
S. Kapralov on EO:
The impure world is for side effects. Files. Databases. RAM regions (mutable data structures). In the declarative paradigm, for the impure world, there are monads in general, and IO in particular. In imperative paradigm - you have just a sequence of statements, going one-by-one, classics.
The pure world is for business logic and rules. It consists of artifacts, that are free of all side effects. In the declarative world - purity is enforced by default: in functional languages, functions are always pure. In the imperative world - one needs to make certain efforts to achieve purity. The bridge between pure and impure world is artifacts composition and applying them on side effects. In the declarative paradigm, it's functional composition and monad binding correspondingly. In the imperative paradigm, it's just side-effect references substitution in a call, trivial as it sounds.
I see EO as a simple thinking model for designing the code, that is easy to maintain, on mainstream languages, like Java, which are not functional and will never be. FP fans see their imperative side-effects-full nature as a flaw, but I see it as an inevitability, embrace it and seek ways of how to deal with it gracefully. That's the niche EO covers. Answering "why EO if there is FP" - well, I personally don't believe FP languages will ever become mainstream or conquer enterprise, and I don't believe switching to FP will solve anything in the industry. Java, C++, Python, and others will be with us for long, and it's childish to say that they must die and be replaced with FP. Mainstream languages have the largest ecosystems and community, refusing it is way to nowhere. That's why for me it's more interesting to seek ways of how to deal with what we have, instead of dreaming about ivory towers.
S. Kapralov on mutability:
Mutability is hated because it brings temporal coupling: if A and B operate over C, and C is stateful, then the actual behavior of B depends on actions made on A, and vice versa. But there are certain places in the program where no matter how big and severe coupling is, it never means harm. These places are entry points, like the main method.
Conclusion: in order to survive, we must move actual temporal coupling to the place, where it won't bring harm. To entry points.
Actually, if we check FP, it already does the same. In Haskell, the entry point to the program is IO main, where IO means "stateful" (simply saying). Moreover, Haskell explicitly restricts referring to IO functions inside non-IO (stateless) ones, which literally means: in Haskell, the only place where you can bind IO to non-IO is IO main.
Now, the same we can make in Java. I separate all classes into two types. Those, who have only final attributes (the type of attributes doesn't matter) - are objects. The rest are data, the state, the source of mutations. I strive to model the logic with objects but can use data structures for data that I want to keep and mutate in memory. In the end, in the entry point, I compose the objects, giving them a reference to the data structures they need.
However, I don't advocate using data structures anywhere we want. They have certain limits of applicability: usually, they are handy for keeping data between transactions in memory. Like, game score in some game: it just exists on a screen and changes over time, modeling it with immutable objects only will cause pollution for GCs to dig through and extra meaningless allocations, placing it to files or databases will slow things down without a reason.
A class that forms a data structure, is modeled completely differently than typical EO objects. For data structures, EO is harmful, they just don't fit into the picture (that's probably why EO hates them that much), so if you need to design a mutable object, forget everything you know from cactoos book:
- For data structures, term maintainability is inapplicable. They are never designed to be maintainable, instead. they are designed to be consistent. That's crucial.
- Encapsulation means a completely different thing for data structures. For objects, encapsulation is used to hide the details, on which the clients should never couple. For data structures, encapsulation is used for keeping inner data consistent. An example is Java’s hashmap, which encapsulates its buckets.
- In object classes, there are typically no more than 4 methods - it's so because objects are designed with the single responsibility principle in mind. They serve a single business purpose, which is rarely expressed in an enormous set of methods. For data structures, this limitation doesn't make any sense: data structures' purpose is to struct data, bringing you as many possibilities to work with it, as it is possible without sacrificing consistency. If you start limiting them, it'll only make the data structure unusable. Typical examples are Java structures - they all have a large number of methods.
- Don't bother with interfaces and subtyping when it comes to data structures, at least until you know for sure. Most probably you'll fail with it. Like in Java, there is a Map contract, and IdentityHashMap, that violates it.
Printers instead of Getters
No matter what the meaning of the object is and what he really does, it must not have any getters. It must not look like a data container to its users.
- Instead of getting attributes out of it in order to process them later, the user must rely on the object and its behavior.
The procedural approach pays no respect to the object and treats it as an anemic bag of data elements. In contrast, the object-oriented approach makes no assumption about the internal structure of the object and fully delegates responsibilities to it.
class Book {
private final String isbn;
private final String title;
private final String author;
Book(String i, String t, String a) {
this.isbn = i; this.title = t; this.author = a;
}
byte[] printTo(Media media) {
return media
.with("isbn", this.isbn)
.with("title", this.title)
.with("author", this.author)
.bytes();
}
}
class ToJSON implements Media {
private final Map<String,String> attrs;
ToJSON() {
this(new HashMap<>());
}
ToJSON(Map<String,String> a) {
this.attrs = a;
}
@Override
public Media with(String name, String value) {
Map<String,String> map = new HashMap<>();
map.put(name, value);
return new ToJSON(map);
}
@Override
public byte[] bytes() {
// build a byte array somehow...
}
}
Media media = new ToJSON();
byte[] bytes = book.printTo(media);
Avoid Configurable Objects
Non-configurable objects are smaller and simpler, and so they're easier to understand. When we need to extend them with additional behavior, we introduce new objects or decorators.
Concept of System properties is wrong because they are static. System properties in Java are a map of global variables.
Avoid Annotations
Annotations-based processing like marshaling (transforming an object into a data format like XML) is an example of a very typical mistake in OOP - horizontal decomposition of responsibility. There are two simple functions that have to be implemented: a) storing string, b) printing an XML document. We don't want to put them both into one object, and that's right. We don't want our object to be huge, and that's why we have to decompose a bigger functionality horizontally, by creating two objects that are responsible for their own parts. The first object stores the data and the second one prints XML documents (the marshaller). They are both equal and that's why the decomposition is called horizontal.
Such a decomposition approach is wrong. Instead, we have to decompose vertically, creating objects that encapsulate each other like the Russian doll. One objects encapsulates another one, adding functionality to it. Such a multi-level nesting decomposition may have multiple objects inside, but there is always a single objects at the top. The top objects is always responsible for everything that its responsibility includes. We don't need anything else in order to print an XML document. The front object knows everything about itself and can do what's needed.
Aspect-oriented programming is good for OOP because it decomposes problems and responsibilities vertically. (But not the AOP implemented with annotations.)
Avoid MVC
MVC is not exactly a design pattern, it is just a name for a very obvious three-step paradigm: 1) get the data, 2) transform the data, 3) render the data.
#!/bin/bash
cat a.txt | head -5 > b.txt
The traditional MVC design pattern is all about horizontal decomposition of responsibility, and that's why it is wrong.
Don't Use ORM
The very idea of matching a relational model with an object-oriented one is wrong. Rows from relational databases must never be mapped to objects simply because objects are not containers and their attributes are not data. We must use SQL.
While NoSQL databases are technically schemaless meaning that they allow us to store documents in any shape we want, the notion of schema itself doesn’t vanish from our domain model.
Fail Fast vs. Fail Safe
Stability and robustness can only be achieved if errors are revealed and immediately reported. The sooner we find the problem and the faster we crash, the better the overall quality will eventually be.
- Prefer to fail fast.
- Emphasize problems instead of hiding them. Make them visible and easy to trace.
References
- David West: Object Thinking
- Yegor Bugayenko: Elegant Objects
- Sandi Metz: 99 Bottles of OOP
- Allen Holub: Holub on Patterns [a]
- Pragmatic Objects by S. Kapralov
- Why getter and setter methods are evil
- Why
extend
is evil - Constructors or Static Factory Methods?
- OOP Alternative to Utility Classes
- Immutability and Pure Functions (for OOP)
- Putting SOLID into perspective
- CUPID
- Don't Create Objects That End With -ER
- Implementation Inheritance
- At the Boundaries, Applications are Not Object-Oriented
- Don't use IDs in domain entities
- Two layer repositories in Spring
- DRY is about knowledge
- Fully encapsulated
- Composition and Dependency Injection is OOP
- Joe Armstrong: Why OO Sucks
- Implementation Inheritance Is Evil
- Interfaces are not abstractions
- API Design Myth: Interface as Contract
- Reused Abstractions Principle (RAP)
- Objects in Functional Languages
- The Object Equality Problem in Java
- Enum Objects
- https://github.com/skapral/hangman/tree/pragmaticobjects