The Object-Oriented Thought Process
OO fundamentals:
encapsulation
every object should ideally be its own unit of data and behavior it can hide from others, not depending on others; modular, pluggable, trying to get messages and not tell other objects how to generate their stuff => data hiding
-This is why we make interfaces, to provide a means to ask for an output while hiding the data of HOW the thing we’re asking makes it. This makes it more modular, protected, etc. (Hey car interface, get me vroom(). Car interface asks car. Car produces vroom, passes it up. Neither I nor car interface need to go in there and make our own vroom. We TRUST car to make vroom.)
polymorphism
similar objects can respond to the same message in different ways
example: you can send a star, a circle, and a square shape the Draw message and each one will draw itself in its own way
inheritance
you should be able to safely replace super with sub classes in your design and it turns out fine: you can do this with inheritance
-sub classes pull from super classes, inheriting data and methods, and it’s often good to make super classes more vague and abstract so you can make more detailed subclasses as needed but without making a TON of them
Cat IS-A Mammal = inheritance design
composition
objects are built from or are composed of other objects
Car object = DrivingWheel + SteeringWheel (super is Wheel!) + Frame + Engine, etc.
Car HAS-A Engine = composition design
CLASSES are BLUEPRINTS for OBJECTS
objects are represented with data and behavior, properties and methods, to mimic real world things and ideas
A Person can be an object. An abstract idea like a Payment can be an object too.
PRINCIPLES:
SOLID
Law of Demeter
DESIGN PATTERNS:
Common problems have been arranged in OOP as design patterns for easier resolution\
2. HOW TO THINK IN TERMS OF OBJECTS
Three important concepts:
- know the difference between the interface and implementation
- think more abstractly
- give the user the most minimal interface possible\
-know the difference between the interface and implementation
know what the user NEEDS to know and what they DON’T need to know. Remember the classic example of a car. A car has an interface, the steering wheel. The driver uses this interface. He doesn’t NEED to know how the car works, the implementation. Have users (or designers, or developers) work to access through interfaces which grabs the results of implementations.
THUS, changing an implementation SHOULD NOT change the user’s code, AKA, using the interface. Separate the interface from the implementation.
EXAMPLE: so let’s say a developer needs to read a database via an API. The API handles implementation, like connect to the DB, read, find records, etc. But the API also offers the interface of the dev making a GET request.
-in this regard, interfaces can also serve as middleware. If an object-oriented program needs to make a GET request to a relational database, middleware can transcribe that into the relational model and pass it on.\
-think more abstractly
To improve class reusuability, design abstract classes with abstract interfaces. The less concrete and specific, the better. Don’t think of coding seperate foods like Grape, Banana, Pizza, etc - instead, code the abstract class Food, and then when you subclass implement it, THEN it has the details that make it a grape, a pizza, etc.\
-give the user the most minimal interface possible
IN TURN, make sure the user has as few, minimal interfaces as possible. Add more only as needed based on required needs. This will develop based on iteration. You must consider all the users.
So, for a cabbie and a passenger, they would both be objects/users. Think about how the target object needs to be used, and start your interfaces around there. Those are the public interfaces. Anything beyond that is the implementation and should probably be private, and hidden from the users.\
3. More Object-Oriented Concepts
Constructors
Constructors are the blueprint for the object, often taking parameters and customizing the object accordingly on initialization. Even an object with no constructor will have one - usually it’s the super() one though, since objects tend to have Object as their parent class anyway. An object can have multiple constructors with different parameters. (Methods can be reused the same way, that’s called overloading.)
Super() gets called first because objects are subclasses of the superclass Object. It uses this to initialize class values first.
As a general tip, it’s a good idea to initialize all the attributes in a class and include constructors even if they don’t have anything to add.
Error Handling
The best way to handle unexpected errors in a program is to throw an exception. There are lots of types of exceptions. This allows the program to detect problems and properly handle them, such as asking for proper input. This helps with preventing crashes. try/catch blocks can run code when it catches exceptions, or pass it to the higher level block. In Java, this is:
try { //code } catch(Exception e) { //error code }
Scope
LOCAL ATTRIBUTES: owned by specific method
OBJECT ATTRIBUTES: owned by object and available to object methods
CLASS ATTRIBUTES: set object attribute to static so EVERYTHING gets to it due to same memory location; closest to global attributes
Others
Operator Overloading: make an operator do multiple things (like add matrixes)
Multiple Inheritance: a class can inherit from more than one class; powerful but complex
Comparing Objects: this is harder because objects often contain references to data and not the actual data itself so you can’t really say one thing equals another. You might have to work around this with a mechanism or make deep copies of objects, memory references intact, to compare them.\
4. The Anatomy of a Class
Knowing the anatomy of a class is good to know when designing classes that interact with each other.
-Name: make it clear
-Attributes: state of the object, keep these private and use getters/setter interfaces to
set data and have other objects grab it
-Constructors: you should probably set a constructor even if it’s nothing to avoid possible issues with the default constructor; set attributes to null (which allows you to error check by testing for null)
-Accessor Methods (public interfaces): these are the getter/setter methods. It’s very good practice to have attributes be private and use getter/setter methods so any class can’t nilly-willy mess with object attributes, and you can control access to sensitive information.
-private implementations: These are methods that are set to private, exclusively to work within the class they belong to. This hides methods from other classes and again provides data integrity and only providing what is ABSOLUTELY ESSENTIAL to other classes.\
5. Class Design Guidelines
Classes should represent logical components and model real-world systems. A car is a class. A database connection is a component, and thus, a class. Objects are modeled and interact with each other.\
- Design objects in a way that represents the object’s true behavior. Have them interact with each other through their public interfaces, such as getters/setters and objects that transfer data between objects.
-Set a minimum public interface, the exact one to do the job and nothing more, restricted otherwise. All the user needs to do is get return data and need or know about nothing else. Hide the implementation.
“All fields shall be private.”
CONSTRUCTORS
Use a constructor to arm an object in an initial, default, safe state.
You can also inject other objects and such into the constructor.
ERROR HANDLING
Design classes to handle errors, to avoid crashes and preserve data.
Make sure to document well with comments so people can understand why things are the way they are.
Make sure classes are good to interact with others.
REUSE
Write more abstract classes that can be reused in different systems. Don’t rely on super-specific implementations and needs.
EXTENSIBILITY
It’s a good idea to prefer extending the functionality of other objects by coding to interfaces, over going in and changing the guts or reusing too much code. Shoot for classes that don’t have design features that make it hard to extend. (Make a Person class, Employee inherits from it, etc.) This is also good to avoid touching live code.
Make names descriptive.
Put codes that depend on particular hardware in wrapper classes.
Keep scope small and provide a way to copy and compare objects so they behave as expected.
MAINTAINABILITY
IT’s a good idea to basically organize your code into many managable piedes so they’re easier to update and maintain in the future. Make it so changes in one class won’t drastically impact other classes. This way changes don’t ripple - decouple classes from each other and lower their dependencies.
To do this, code in small chunks and build and test at each step. Shoot for tracer bullet style development so you can test the design early and catch problems ahead of time. Make sure you’ve got a good testing plan.
Test the interfaces too, use small versions of them called stubs to make sure they’re working properly and work well for the user. (IE, test the interface by having the data returned be provided by dummy data and methods.) Keep them for later even if they’re commented out.
OBJECT PERSISTENCE
This is when objects are saved in programs for later, often for business systems to preserve the date. You can do this in several ways:
-save as a flat file: XML, JSON (serialization)
-put in a database (SQL or noSQL like MongoDB) (SQL would need middleware for conversion)
Serialization is when an object is deconstructed, sent over a wire, and then reconstructed.
6. Designing With Objects
A system is a group of classes that interact with each other, and good object-oriented design takes into account how all these classes play with each other. It’s a good idea to do good design up front before engineering to make the system better. (In fact, this problem has come up so often that design patterns are a thing in OOP that have been used to solve the same problems over and over again.)
There’s a lot of design methods, and what you can bet on is that a design will change and iterate over time. Rapid prototyping and refinement over multiple iterations tends to lead to a better product. It also keeps the cost of design changes small during the design phase, because fixing AFTER launch is often way more expensive.
Overall, though, you want to follow these steps:\
- Doing the proper analysis
- Developing a statement of work that describes the system
- Gathering the requirements from this statement of work
- Developing a prototype for the user interface
- Identifying the classes
- Determining the responsibilities of each class
- Determining how the various classes interact with each other
- Creating a high-level model that describes the system to be built
The high-level model often has diagrams and class interaction drawings so you can understand how everything works. Often UML is used for this.
FIRST: analyze the project.
-Get the requirements of the project, what the users want to do in what way. Hit each one as a bullet item.
-Settle on a high-level understanding of the system.
-Create a prototype - this can be actual code of the user interface or just diagrams. You don’t need logic at this point, just get an ideal of how the interface will flow from screen to screen and what’s needed.
NEXT: identify and design classes.
-Use the requirements and your UI prototype to ID classes, usually nouns and other objects. Get something down on the first pass, add and eliminate classes as needed over multiple passes.
-Figure out what each object needs to store for data and what operations they need to perform.
-Determine how the classes collab and interact with each other.
-Create a class model (UML is one way to do this) to show how all the classes interfact with each other. This is pretty good when you have to design massive systems.
THEN: prototype.
-Create a prototype with some tool, like actual code, a prototpying tool, or even code. Use it to hone down the look and feel from the user.
OBJECT WRAPPERS
Think of objects as wrappers around structured programming code (IE, methods). You could use this fact to incorporate legacy structured code into OO systems. This is also good for using nonportable code.
You may also want to wrap someone else’s existing class in an object wrapper and change the implementation/interface. Middleware essentially does this, providing an interface between objects and relational databases. This allows reuse and easier problem solving.
7. Mastering Inheritance and Composition
One of the big things to consider in any object-oriented design is the use of inheritance vs. composition. Since it’s all about reusing objects and their code, both of these are ways to reuse objects to construct others that share data and behaviors. Both of these approaches have tradeoffs - keep them in mind when considering a design or design patterns.
INHERITANCE: inherit behaviors and attributes from super/parent classes
EX: a dog is-a mammal
COMPOSITION: create a new object out of other objects
EX: a car has-a engine
INHERITANCE
If Class B is a Class A, then it’s probably good for inheritance.
With inheritance, you can reuse methods and data in similar subclasses, making it easier to code, test, reuse, and maintain. Things are in a single place.
The drawback is that not all subclasses need all of the methods or data of their parent classes. For example, a Kiwi is a Bird, but while a Bird can fly(), a Kiwi can’t. You could override it, but if you’re doing that a lot, it’s not ideal. According to the Liskov Substitution Principle, a subclass should be easily substituted for a parent class without issues, but with an example like that, it would cause one.
As you design downwards with inheritance, subclasses get more specific when you factor out commonality, such as pushing them out as interfaces, and this can make the tree model more complex as you balance against fuctionality, especially for the future.
COMPOSITION
You build composite objects with other objects. The car composite object has a SteeringWheel, an Engine, etc. Of course, it’s very easy to make this model complex too with a ton of sub objects being used to make up the composite. Like with inheritance, how functional do you want the model VS. how complex do you want to make it? More functionality, or easier to maintain?
ENCAPSULATION
Remember that encapsulation is about classes being divided into interfaces and implementations. Inheritance weakens this a little because it needs to know and access things from its superclass, so if you change the superclass, that change ripples through all its subclasses. Typically as a result you want to abstract out behaviors and inherit attributes, so good testing doesn’t become harder.
As a result: make sure you stick to X IS-A Y superclass tightly so ripple changes make sense. For example, a Bird is-a Animal. A Window ISN’T a Rectangle, because GUI windows are much more than its shape.
POLYMORPHISM
This allows you to send the same message to subclasses and they respond based on their type. These superclasses are often abstract classes.
EXAMPLE: Shape has getArea(). Circle implements Shape and defines getArea() for a circle. Square does the same but its getArea() does it for a square. Only the subclasses define this, Shape itself doesn’t.
8. Frameworks and Reuse: Designing with Interfaces and Abstract Classes
Reusing Code
Code reuse is great, and while you can develop reusable code in functional languages, OO-languages give you tools to make this easier to do (frameworks, abstract classes, interfaces).
Frameworks
A framework’s “standardized”, AKA, plug and play. Frameworks have ready made tools and solutions for various coding problems. These include libraries, compilers, tools, APIs, etc. React is a framework. Django is a framework. WordPress is a framework. It’s a foundation.
Contracts and Abstract Classes
When you comply with the rules in the framework to do something, you’re forming a contract. They’re set up for good dev practices and are enforced within the framework to keep things under control and standard.
One way of doing this is to work with abstract classes, which have methods that aren’t instatinated and must be defined with what extends it. It can help enforce polymorphism by having subclasses implement their own version of the same method that behaves as expected.
Defining Interfaces
Since Java doesn’t support multiple inheritance, you can use multiple interfaces to cover more than just inheriting from one abstract class. Interfaces allow you to enforce contracts more easily. But remember: abstract classes can provide abstract and concrete methods - interfaces can only do abstract.
Remember these types of relationships:
Dos IS-A Mammal: inheritance
Dog implements Nameable: interface
Dog HAS-A Head: composition
Making a Contract
Use an abstract class or interface to provide an unimplemented method. The subclass then complies with the contract by providing an implementation. This keeps everything standard. Using contracts are plug-in points to help make parts of a system abstract.
By factoring out as much commonality as possible into abstract designs, we can reuse systems, made the code more modular, and roll out changes more easily across the multiple systems that use it. You avoid having a bunch of single implementations and save time and energy.
9. Building Objects and Object-Oriented Design
Composition is a powerful tool for increasingly complex software systems. By building in phases, using components, you can start simple, grow outwards, and have more reassurance that the system is stable. Plus, you can change or swap one component at a time as needed, which costs less time and money, with greater safety since there’s way less risk that you’ll break the whole system. this is why it’s so often preferred over inheritance. Building in phases like this also allows easier and more comprehensive testing.
Stable systems:
- Take the form of a hierarchy, broken down into increasingly simple subsytems.
- Can be identified by the parts that make up the system.
- Usually only have a few kinds of subsystems in different arrangements.
- Often evolve from simpler systems that work.
Types of Composition
There’s two types: aggregations and associations.
Aggregations are when one complex object is made up of other objects, and you have a tendency to see the whole over the parts. For example, a car is an aggregation. You buy it all together and typically don’t pick and choose the parts.
Associations are when you can see the parts that make up the whole and how it’s associated with each other. A computer system has associations for its equipment, in that maybe it knows how to do mouse stuff but it still needs a mouse object to work with.
Of course it’s not always that simple, often it’s a mix of aggregations and associations. For example, a computer monitor within an associated system is still aggregated because it’s made up of inner objects.
Avoiding Dependencies
Dependencies are when objects deeply depend on each other to work. This is why you’re always using interfaces and keeping your implementations in concrete separate classes - this way, if one part breaks, you don’t need to repair or redo the whole chunk of system. It’s a tradeoff of convenience VS. stability.
Look at an iPhone. Sure is convenienent! But what if the battery messes up? You can’t switch it, other parts are very dependent on it, so the whole system is down.
This is related, by the way, when we talk about injecting dependencies. Rather than making a new object inside of another, you make them seperately, and you pass object A into object B so A can use B to work. This keeps down maintenance and improves the design in case something has to change or breaks. (See below section about Depndency Injection.)
Cardinality
The number of objects that participate in an association and whether it’s optional or mandatory. It’s a good idea to check if optional associations are null to handle it as a valid condition.
Cardinality can be written like this:
1 (mandatory, only one association)
1..n (mandatory, n associations)
0..1 (optional, 1 association)
0..n (optional, n associations)
10. Design Patterns
Design patterns pair great with object-oriented development, providing reusuable software designs to solve a variety of common software problems.
Why use design patterns?
Since software often runs into solving the same problems over and over, much like using a blueprint for buildings and cities, you can use design patterns to easilyy re-solve the same problems.
A pattern has four elements:
- Pattern name
- Problem: when to apply the pattern and other specific problems/conditions
- Solution: describes design, relationship,s reponsibilities, and collaborations: a general abstract description on how to solve the problem
- Consequences: using patterns will often have trade-offs along with results, and should be evaluated accordingly.
Model/View/Controller
A popular design pattern for UIs in Smalltalk.
Model: application object and data
View: the screen presentation
Controller: defines how UI reacts to input
-> REMEMBER: you want to seperate the interface from other interfaces and implementations as much as possible. MVC was one of the earliest way to do this, and is still used today.
Types of Design Patterns
A. Creational
These create objects for you instead of you instantiating them, which makes things more flexible to decide what ought to be created for a given case. - One example is the factory method. These methods use classes to create objects, which can then be invoked by other classes, and maybe even be extended by subclasses that talk to the factory to get the parameters they needed. This encourages polymorphism and code reusability so you only have to worry about one spot of code for the object being made in the factory.
B. Structural
Compose groups of objects into larger structures, like a user interface. This is a handy thing to consider for seperating implementations from interfaces. - The Adapter method essentially wraps a class to create a different interface for a class that already exists, to gain the benefits of having an object. You can change the MailInterface to change how it gets mail from the MailTool, but you don’t have to touch MailTool as a result.
C. Behavioral
Define communication between objects in system and how flow is controlled. - The Iterator pattern is used for traversing a collection and provides functionality while hiding information. Java natively has one of these.
Antipatterns
Antipatterns are basically practices to avoid, and since they’re designed to solve problems that have already happened, they have the benefit of hindsight and can help understand the root cause of a problem.
There are two kinds:\
- antipatterns that are bad solutions to problems\
- antipatterns that get out of a bad situation and how to get to a good solution
Robust artifacts are patterns that meet general needs and are tested well, great for usuability. If they don’t get reused, however, they become reuseless artifacts and ought to be reviewed and revised.\
11. Avoiding Dependencies and Highly Coupled Classes
For all of the benefits of inheritance, it does have a side effect of creating dependencies between classes, coupling them together. Thiis can make problems for maintenance and testing. A common alternative to inheritance is composition, which is essentially containing multiple classes into one. However, if you’re not careful, composition can have the same problem.
Essentially, for composition, you want to pass objects into other objects via parameter (associations), and NOT create objects within objects via the new keyword (aggregations).
Despite this, you should still know how inheritance works, when working with legacy code.
Inheritance
EX: Dog IS-A Mammal
As you know, you can extend classes and have subclasses inherit their stuff. The issue is when subclasses may not need to use all of the methods of their parent class. When it gets out of control, you could be making a lot of subclasses to account for every possible behavior. Sometimes inheritance is fine, though, when absolutely every subclass needs a parent behavior.
Composition
EX: GameEntity HAS-A Controllable, Shootable interface
This is more creating individual classes for behaviors and injecting them into the classes that need them. If you have a game Entity, it could be Controllable or Shootable or Collectable. Then you can put these into the Entity to make a player character, or an enemy, or even just a Collectable item entity.
Think of it like this:
-if an object can exist independently of its parent, that’s aggregation. A Student can exist without a Class.
-If not, then it’s a relationship that indicates composition. A Room can’t exist without a House.
Dependency Injection
This is when you inject behavior into a class as opposed to making a new class of behavior inside an existing object. Doing it the former way decouples the objects, promoting greater composition. You can inject via a constructor or by a setter method.
Coupled version:
class Dog {
Walkable walker = new Walkable();
}
Injected:
class Dog {
Walkable walkable;
public Dog(Walkable w) {
this.walkable = w;
}
}
12. The SOLID Principles of Object-Oriented Design
OO is about managing dependencies by inverting key dependecies to prevent rigid, fragile, or non-reusable code.
Rigidity - when change to one part of program breaks another
Fragility - when things break in unrelated places
Immobility - when code can’t be reused outside of original context
To combat this, the five principles that make up SOLID are used for good object-oriented design.
SRP - Single Responsbility Principle
A class should only have a single reason to change and thus should focus on a single task. Changes to one method shouldn’t affect the other methods. This is why we put things in separate classes.
OCP - Open/Close Principle
You should be able to extend a class’s behavior without modifying it. This is why abstract classes are favored and then extended with new functionality, so you don’t have to change the core area.
LSP - Liskov Substitution Principle
Essentially, if a parent class can do something, the child class must fully be able to do it too. Again, this is why abstract classes are favored and extended from. This helps with maintenance and to ease confusion. Make sure your child classes aren’t substitutes for the parent classes.
IPS - Interface Segregation Principle
It’s better to have a lot of small interfaces than a few larger ones. This allows better composition and easier maintenance.
DIP - Dependency Inversion Principle
Code should depend on abstractions and it’s better to couple something abstract instead of complete. This has a few terms:
Dependency inversion—The principle of inverting the dependencies
Dependency injection—The act of inverting the dependencies
Constructor injection—Performing dependency injection via the constructor
Parameter injection—Performing dependency injection via the parameter of a
method, like a setter
Simply put, it’s better to inject what you need into other objects, and try to make new objects as far up the chain as possible (such as in the main method) and rely on using interfaces to extend and inject dependencies in while constructing the objects you need. IE, pass in what you need via constructors and setter/getter methods, try not to make new objects within other objects directly.