Object oriented technology

Posted by anshu.sah on Sat, 02 Oct 2021 02:15:34 +0200


Software is becoming more and more complex, which has become a trend. The traditional process oriented method can not meet the needs of software development. In order to better maintain the software, people begin to consider corresponding the real-world entities with the modules in the software. The modules in the software not only have the attributes of the real-world entities, but also have the behavior of the real-world entities.

Object oriented technology is doing such a thing. Its purpose is to put appropriate attributes and behaviors into appropriate classes.

Object-oriented is a technology produced in practice. To understand the idea of object-oriented is inseparable from practical application. Therefore, the blog will illustrate each object-oriented technology with practical examples.

Four characteristics of object oriented

encapsulation

First, the first feature is introduced.
What's wrong with the following code?

public class Wallet{
	private string ID;
	private long createTime;
	private BigDecimal balance;
	private long balanceLastModifiedTime;
	public Wallet(){
	//initialize
	...
	}
	public string getID();
	public long getCreateTime();
	public BigDecimal getBalance():
	public long getBalanceLastModifiedTime();
	public void setID(string ID);
	public void setCreateTime(long createTime);
	public void setBalance(BigDecimal balance):
	public void setBalanceLastModifiedTime(long balanceLastModifiedTime);
}

This code seems to be object-oriented, but it is actually process oriented, because every property in the Wallet class has getter and setter methods. Imagine that this code can be used in the project to modify every property of the Wallet class, but in fact, the properties of some classes cannot be modified at will.

This code actually violates the first feature of object-oriented, encapsulation. Encapsulation is actually to control the access rights of classes. What should not be accessed should be encapsulated to avoid being accessed.

Let's complete the correct code.
After analysis, it can be seen that
1. The only attribute in the wallet class that we can actually change is balance, and balance cannot be modified at will. We can only increase or decrease a number.
2. The attribute balanceLastModifiedTime in the class should also be modified, but it should be changed inside the method we call to modify balance.
The final code is as follows

public class Wallet{
	private string ID;
	private long createTime;
	private BigDecimal balance;
	private long balanceLastModifiedTime;
	public Wallet(){
	//initialize
	...
	}
	public string getID();
	public long getCreateTime();
	public BigDecimal getBalance():
	public long getBalanceLastModifiedTime();
	public void increaseBalance(BigDecimal increasedAmount);//change balance and change balanceLastModifiedTime
	public void decreaseBalance(BigDecimal decreasedAmount);//change balance and change balanceLastModifiedTime
}

abstract

The second feature of object orientation is abstraction.
In java, there are two main ways to implement abstraction. The first is the interface, and the second is the abstract class.
First consider such a situation.

public interface IPictureStorage { 
    void savePicture(Picture picture); 
    Image getPicture(String pictureId);
    void deletePicture(String pictureId);
    void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo);
}

public class PictureStorage implements IPictureStorage {
    // ... omit other properties  
    @Override public void savePicture(Picture picture) { ... } 
    @Override public Image getPicture(String pictureId) { ... }
    @Override public void deletePicture(String pictureId) { ... }
    @Override public void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) { ... }
    void func1();
    void func2();
    void func3();
    void func4();
}

We can see that there are many methods in our PictureStorage class. It is difficult to see which methods are called by other systems, and the interface can solve this problem.
The interface abstracts four methods. People who need to use this class will know which methods of this class can be called and which methods cannot be called at the first sight of the interface, which filters out a lot of unnecessary information for users and makes it easier to use. This is an abstraction.

Here is another case

public class FileLogger{  
    // Subclass of abstract class: output log to file
    private Writer fileWriter;  
    public FileLogger(String name, boolean enabled,  Level minPermittedLevel, String filepath)
    @Override  public void doLog(Level level, String mesage);
        // Format level and message and output to log file}  
}

public class MessageQueueLogger{ 
    // Subclass of abstract class: output log to message oriented middleware (such as kafka) 
    private MessageQueueClient msgQueueClient; 
    public MessageQueueLogger(String name, boolean enabled, 
         Level minPermittedLevel, MessageQueueClient msgQueueClient);

    @Override  protected void doLog(Level level, String mesage);
    // Format the level and message and output them to the message middleware
}

We have two subclasses, FileLogger and MessageQueueLogger, which are used to output logs to files and message queues.
When using these classes, our client needs to create objects of these classes, but this design is very bad. If we need to add another class that outputs log information, we need to make great changes. At this time, our best way is to add a layer of abstraction to uniformly manage these special classes that output log information. When the client uses these classes, it is OK to use abstraction, so as to decouple the client from the specific classes that output log information. The code is as follows.

// abstract class
public abstract class Logger {  private String name; 
    private boolean enabled; 
    private Level minPermittedLevel; 
    public Logger(String name, boolean enabled, Level minPermittedLevel) 
    public void log(Level level, String message){}  

public class FileLogger extends Logger {  
    // Subclass of abstract class: output log to file
    private Writer fileWriter;  
    public FileLogger(String name, boolean enabled,  Level minPermittedLevel, String filepath)
    @Override  public void doLog(Level level, String mesage);
        // Format level and message and output to log file}  
}

public class MessageQueueLogger extends Logger { 
    // Subclass of abstract class: output log to message oriented middleware (such as kafka) 
    private MessageQueueClient msgQueueClient; 
    public MessageQueueLogger(String name, boolean enabled, 
         Level minPermittedLevel, MessageQueueClient msgQueueClient);

    @Override  protected void doLog(Level level, String mesage);
    // Format the level and message and output them to the message middleware
}

inherit

There is a well-known principle in software development. DRY(don’t repeat yourself). This principle says not to repeat what has been done, and one of the greatest benefits of inheritance is to realize software reuse.

If the two classes are very similar, we can extract the similar parts of the two classes and make them into a parent class, so that the two subclasses can reuse the code of the parent class and avoid repeated code writing many times.

However, we should pay attention to the use of inheritance. We should avoid using inheritance that is too deep or too complex, otherwise it will lead to poor readability of the code.

polymorphic

The usage of polymorphism is to make the parent object reference the subclass. At the same time, if the subclass overrides a method of the parent class, the method of the subclass is actually called when calling this method.
The code example is as follows

public class DynamicArray { 
    private static final int DEFAULT_CAPACITY = 10; 
    protected int size = 0; 
    protected int capacity = DEFAULT_CAPACITY; 
    protected Integer[] elements = new Integer[DEFAULT_CAPACITY]; 

    public int size() { return this.size; } 
    public Integer get(int index) { return elements[index];} 
    //... omit n multiple methods 

    public void add(Integer e) { 
        ensureCapacity(); 
        elements[size++] = e; 
    } 
    protected void ensureCapacity() { 
            //... if the array is full, expand the capacity... Omit the code 
    } 
} 

public class SortedDynamicArray extends DynamicArray { 
    @Override 
    public void add(Integer e){ 
        ensureCapacity(); 
        for (int i = size-1; i>=0; --i) { // Ensure that the data in the array is in order
            if (elements[i] > e) { 
            elements[i+1] = elements[i]; 
            } 
            else{ 
                break; 
            } 
        } 
        elements[i+1] = e; 
        ++size; 
    } 
} 
public class Example { 
    public static void test(DynamicArray dynamicArray) { 
        dynamicArray.add(5); 
        dynamicArray.add(1); 
        dynamicArray.add(3); 
        for (int i = 0; i < dynamicArray.size(); ++i) {
            System.out.println(dynamicArray[i]); 
        } 
    } 
    public static void main(String args[]) { 
        DynamicArray dynamicArray = new SortedDynamicArray(); 
        test(dynamicArray); // Print results: 1, 3, 5 
    }
}

Distinction between object oriented and process oriented

Some methods seem to be process oriented, but they are actually process oriented.
1. Abuse getter and setter methods
Adding a getter and setter method to each attribute of the class destroys the encapsulation characteristics of the class. It looks object-oriented, but it is actually process oriented.

2. Abuse of global variables and global methods
Putting all global variables and methods into one class is a typical process oriented method, which can easily lead to confusion and difficult maintenance of all global variables and methods.
We can classify global variables according to their purpose to facilitate future maintenance.

3. Separate class methods from attributes
The traditional MVC structure generally adopts this method, which divides the system into interface layer, service layer and data access layer. The variables and methods of each layer are divided, which is a typical object-oriented style. This approach will be more effective when building simple systems, but it is easy to be overwhelmed by complexity when building complex systems. So this practice is also called anemia model.

Programming for abstraction rather than implementation

In software design, there is a saying that the architecture of software system should not rely on complex and changeable concrete implementation, but on relatively unchanged abstraction. Using abstract architecture is a stable architecture.

There are two kinds of abstract methods: abstract class and abstract interface.

Abstract classes and interfaces

The use of abstract classes and abstract interfaces has been introduced in the above four object-oriented features and will not be repeated.

The difference between abstract classes and interfaces

So when should we use abstract classes and abstract interfaces?

The main difference between the two is that the interface is top-down. First define the required interface, and then define the class.

Abstract classes are bottom-up. We first have some classes, and then find that the code of these classes can be reused. We consider making an abstract class to realize code reuse and cope with future changes.

Of course, good design is generally performed, and there is no perfect design at the beginning. In practice, we generally design a rough system model based on our own experience, and then find problems in practice to evolve our system step by step.

Multi use combination and less inheritance

There is a principle in object-oriented: combination is better than inheritance, use more combination and less inheritance.
This is because inheritance can easily lead to the explosion of hierarchical relationships.

Suppose we want to design a class about birds. We define the abstract concept of "bird" as an abstract class AbstractBird. All more subdivided birds, such as sparrows, pigeons, crows, etc., inherit this abstract class.

But there is a problem. Some birds can fly and some can't fly for years. Should we add a fly() method to AbstractBird?
This is a difficult choice:

Because some birds cannot fly, the parent class should not have such a method, but if there is no such method in the parent class, the flying birds will not be able to call the fly() method in the reference of the parent class, which is obviously unreasonable.

One solution is to subdivide AbstractBird into birds that can fly and birds that can't fly. Years that can fly and years that can't fly are inherited from AbstractBird. This seems to solve this problem, but at this time, there is another question. Some birds can bark every year, and some birds can't. what should we do at this time?

Also inherit a bird that can call and can't call on the original basis? In this way, we have four abstract classes. AbstractFlyableTweetableBird,AbstractFlyableUnTweetableBird,
AbstractUnFlyableTweetableBird,AbstractUnFlyableUnTweetableBird
If you subdivide it, for example, whether it will lay eggs, it is eight, and then subdivide it into sixteen... Eventually lead to the combination explosion

This is the problem caused by too deep inheritance level.

In this case, we use the interface to solve the problem.

public interface Flyable { 
    void fly(); 
}
public interface Tweetable {
    void tweet(); 
} 
public interface EggLayable { 
    void layEgg(); 
} 
public class Ostrich implements Tweetable, EggLayable {// ostrich
//... omit other properties and methods 
    @Override 
    public void tweet() { //... 
    }
    @Override 
    public void layEgg() { //... 
    } 
} 
public class Sparrow impelents Flayable, Tweetable, EggLayable {// sparrow
//... omit other properties and methods 
    @Override 
    public void fly() { //... 
    } 
    @Override 
    public void tweet() { //... 
    }
    @Override 
    public void layEgg() { //... 
    }
}

There is a problem here. If the code for birds to fly, call and lay eggs in each interface is the same, it will lead to repeated code multiple times.

We can solve this problem by asking a class to implement the interface, and then the actual birds use the interface to complete the action.
For example, let a Flyability class implement the Fiyable interface, and then combine this class into the actual flying bird. The birds call the fly method in the Flyability class in the fly method, so that the code can be reused.

This is called an interface, composition, and delegate method.
The code is as follows

public interface Flyable { 
    void fly(); 
}
public class FlyAbility implements Flyable {
    @Override 
    public void fly() { //... 
    }
}
// Omit Tweetable/TweetAbility/EggLayable/EggLayAbility

public class Ostrich implements Tweetable, EggLayable {// ostrich
//... omit other properties and methods 
    private TweetAbility tweetAbility = new TweetAbility(); // combination
    private EggLayAbility eggLayAbility = new EggLayAbility(); // combination
    @Override 
    public void tweet() { //... 
        tweetAbility.tweet(); // entrust
    }
    @Override 
    public void layEgg() { //... 
        eggLayAbility.layEgg(); // entrust
    } 
} 

The source example comes from the blog post of geek time Wang Zheng in the beauty of design patterns, which has deleted the source program

Topics: Java Big Data kafka