JavaRush /Java Blog /Random EN /Designing Classes and Interfaces (Translation of the arti...
fatesha
Level 22

Designing Classes and Interfaces (Translation of the article)

Published in the Random EN group
Designing Classes and Interfaces (Translation of the article) - 1

Content

  1. Introduction
  2. Interfaces
  3. Interface markers
  4. Functional interfaces, static methods and default methods
  5. Abstract classes
  6. Immutable (permanent) classes
  7. Anonymous classes
  8. Visibility
  9. Inheritance
  10. Multiple inheritance
  11. Inheritance and composition
  12. Encapsulation
  13. Final classes and methods
  14. What's next
  15. Download source code

1. INTRODUCTION

No matter what programming language you use (and Java is no exception), following good design principles is the key to writing clean, understandable, and verifiable code; and also create it to be long-lived and easily support problem solving. In this part of the tutorial, we're going to discuss the fundamental building blocks that the Java language provides and introduce a couple of design principles in an effort to help you make better design decisions. More specifically, we're going to discuss interfaces and interfaces using default methods (a new feature in Java 8), abstract and final classes, immutable classes, inheritance, composition, and revisit the visibility (or accessibility) rules we briefly touched on in Part 1 lesson "How to create and destroy objects" .

2. INTERFACES

In object - oriented programming, the concept of interfaces forms the basis for the development of contracts . In a nutshell, interfaces define a set of methods (contracts) and each class that requires support for that specific interface must provide an implementation of those methods: a fairly simple but powerful idea. Many programming languages ​​have interfaces in one form or another, but Java in particular provides language support for this. Let's take a look at a simple interface definition in Java.
package com.javacodegeeks.advanced.design;

public interface SimpleInterface {
void performAction();
}
In the snippet above, the interface we called SimpleInterface, declares only one method called performAction. The main difference between interfaces and classes is that interfaces outline what the contact should be (they declare a method), but do not provide their implementation. However, interfaces in Java can be more complex: they can include nested interfaces, classes, counts, annotations, and constants. For example:
package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefinitions {
    String CONSTANT = "CONSTANT";

    enum InnerEnum {
        E1, E2;
    }

    class InnerClass {
    }

    interface InnerInterface {
        void performInnerAction();
    }

    void performAction();
}
In this more complex example, there are several restrictions that interfaces unconditionally impose on nesting constructs and method declarations, and these are enforced by the Java compiler. First of all, even if not explicitly declared, every method declaration in an interface is public (and can only be public). Thus the following method declarations are equivalent:
public void performAction();
void performAction();
It's worth mentioning that every single method in an interface is implicitly declared abstract , and even these method declarations are equivalent:
public abstract void performAction();
public void performAction();
void performAction();
As for declared constant fields, in addition to being public , they are also implicitly static and marked final . Therefore the following declarations are also equivalent:
String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT";
Finally, nested classes, interfaces, or counts, in addition to being public , are also implicitly declared static . For example, these declarations are also equivalent to:
class InnerClass {
}

static class InnerClass {
}
The style you choose is a personal preference, but knowing these simple properties of interfaces can save you from unnecessary typing.

3. Interface marker

A marker interface is a special kind of interface that does not have methods or other nested constructs. How the Java library defines it:
public interface Cloneable {
}
Interface markers are not contracts per se, but are a somewhat useful technique for "attaching" or "associating" some specific trait with a class. For example, with respect to Cloneable , the class is marked as cloneable, but the way in which this can or should be implemented is not part of the interface. Another very famous and widely used example of an interface marker is Serializable:
public interface Serializable {
}
This interface marks the class as being suitable for serialization and deserialization, and again, it does not specify how this can or should be implemented. Interface markers have their place in object-oriented programming, although they do not satisfy the main purpose of an interface to be a contract. 

4. FUNCTIONAL INTERFACES, DEFAULT METHODS AND STATIC METHODS

Since the releases of Java 8, interfaces have gained some very interesting new features: static methods, default methods, and automatic conversion from lambdas (functional interfaces). In the interfaces section, we emphasized the fact that interfaces in Java can only declare methods, but do not provide their implementation. With a default method, things are different: an interface can mark a method with the default keyword and provide an implementation for it. For example:
package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefaultMethods {
    void performAction();

    default void performDefaulAction() {
        // Implementation here
    }
}
Being at the instance level, the default methods could be overridden by each interface implementation, but interfaces can now also include static methods, for example: package com.javacodegeeks.advanced.design;
public interface InterfaceWithDefaultMethods {
    static void createAction() {
        // Implementation here
    }
}
It could be said that providing the implementation in the interface defeats the whole purpose of contract programming. But there are many reasons why these features were introduced into the Java language and no matter how useful or confusing they are, they are there for you and your use. Functional interfaces are a completely different story and have been proven to be very useful additions to the language. Basically, a functional interface is an interface with just one abstract method declared on it. RunnableThe standard library interface is a very good example of this concept.
@FunctionalInterface
public interface Runnable {
    void run();
}
The Java compiler treats functional interfaces differently and can turn a lambda function into a functional interface implementation where it makes sense. Let's consider the following function description: 
public void runMe( final Runnable r ) {
    r.run();
}
To call this function in Java 7 and below, an implementation of the interface must be provided Runnable(for example using anonymous classes), but in Java 8 it is sufficient to provide an implementation of the run() method using lambda syntax:
runMe( () -> System.out.println( "Run!" ) );
Additionally, the @FunctionalInterface annotation (annotations will be covered in detail in Part 5 of the tutorial) hints that the compiler can check whether an interface contains only one abstract method, so any changes made to the interface in the future will not violate this assumption.

5. ABSTRACT CLASSES

Another interesting concept supported by the Java language is the concept of abstract classes. Abstract classes are somewhat similar to interfaces in Java 7 and are very close to the default method interface in Java 8. Unlike regular classes, an abstract class cannot be instantiated, but it can be subclassed (refer to the Inheritance section for more details ). More importantly, abstract classes can contain abstract methods: a special kind of method without an implementation, just like an interface. For example:
package com.javacodegeeks.advanced.design;

public abstract class SimpleAbstractClass {
    public void performAction() {
        // Implementation here
    }

    public abstract void performAnotherAction();
}
In this example, the class SimpleAbstractClassis declared as abstract and contains one declared abstract method. Abstract classes are very useful; most or even some parts of the implementation details can be shared among many subclasses. Be that as it may, they still leave the door ajar and allow you to customize the behavior inherent in each of the subclasses using abstract methods. It's worth mentioning that unlike interfaces, which can only contain public declarations, abstract classes can use the full power of accessibility rules to control the visibility of an abstract method.

6. IMMEDIATE CLASSES

Immutability is becoming more and more important in software development nowadays. The rise of multi-core systems has raised many issues related to data sharing and parallelism. But one problem has definitely arisen: having little (or even no) mutable state leads to better extensibility (scalability) and easier reasoning about the system. Unfortunately, the Java language does not provide decent support for class immutability. However, using a combination of techniques, it becomes possible to design classes that are immutable. First of all, all fields of the class must be final (marked as final ). This is a good start, but it is no guarantee. 
package com.javacodegeeks.advanced.design;

import java.util.Collection;

public class ImmutableClass {
    private final long id;
    private final String[] arrayOfStrings;
    private final Collection<String> collectionOfString;
}
Secondly, ensure proper initialization: if a field is a reference to a collection or array, do not assign those fields directly from constructor arguments, make copies instead. This will ensure that the state of the collection or array is not modified outside of it.
public ImmutableClass( final long id, final String[] arrayOfStrings,
        final Collection<String> collectionOfString) {
    this.id = id;
    this.arrayOfStrings = Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
    this.collectionOfString = new ArrayList<>( collectionOfString );
}
And finally, ensuring proper access (getters). For collections, the immutability must be provided as a wrapper  Collections.unmodifiableXxx: With arrays, the only way to provide true immutability is to provide a copy instead of returning a reference to the array. This may not be acceptable from a practical point of view, since it is very dependent on the size of the array and can put enormous pressure on the garbage collector.
public String[] getArrayOfStrings() {
    return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
}
Even this small example gives a good idea that immutability is not yet a first class citizen in Java. Things can get complicated if an immutable class has a field that refers to an object of another class. Those classes should also be immutable, but there is no way to ensure this. There are several decent Java source code analyzers, like FindBugs and PMD, that can greatly help by checking your code and pointing out common Java programming flaws. These tools are great friends of any Java developer.

7. ANONYMOUS CLASSES

In the pre-Java 8 era, anonymous classes were the only way to ensure classes were defined on the fly and instantiated immediately. The purpose of anonymous classes was to reduce boilerplate and provide a short and easy way to represent classes as a record. Let's take a look at the typical old-fashioned way to spawn a new thread in Java:
package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread(
            // Example of creating anonymous class which implements
            // Runnable interface
            new Runnable() {
                @Override
                public void run() {
                    // Implementation here
                }
            }
        ).start();
    }
}
In this example, the implementation of Runnableinterface is provided immediately as an anonymous class. Although there are some limitations associated with anonymous classes, the main disadvantages of using them are the highly verbose construct syntax that Java as a language obliges to. Even just an anonymous class that doesn't do anything requires at least 5 lines of code every time it's written.
new Runnable() {
   @Override
   public void run() {
   }
}
Luckily, with Java 8, lambda and functional interfaces, all these stereotypes will soon go away, finally writing Java code will look truly concise.
package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread( () -> { /* Implementation here */ } ).start();
    }
}

8. VISIBILITY

We already talked a little about visibility and accessibility rules in Java in Part 1 of the tutorial. In this part, we're going to revisit this topic again, but in the context of subclassing. Designing Classes and Interfaces (Translation of the article) - 2Visibility at different levels allows or prevents classes from seeing other classes or interfaces (for example, if they are in different packages or nested within each other) or subclasses from seeing and accessing the methods, constructors, and fields of their parents. In the next section, inheritance, we'll see this in action.

9. INHERITANCE

Inheritance is one of the key concepts of object-oriented programming, serving as the basis for constructing a class of relationships. Combined with visibility and accessibility rules, inheritance allows classes to be designed into a hierarchy that can be extended and maintained. At a conceptual level, inheritance in Java is implemented using subclassing and the extends keyword , along with the parent class. A subclass inherits all public and protected elements of the parent class. Additionally, a subclass inherits the package-private elements of its parent class if both (subclass and class) are in the same package. That being said, it is very important, no matter what you are trying to design, to stick to the minimum set of methods that a class exposes publicly or to its subclasses. For example, let's look at the class Parentand its subclass Childto demonstrate the difference in visibility levels and their effects.
package com.javacodegeeks.advanced.design;

public class Parent {
    // Everyone can see it
    public static final String CONSTANT = "Constant";

    // No one can access it
    private String privateField;
    // Only subclasses can access it
    protected String protectedField;

    // No one can see it
    private class PrivateClass {
    }

    // Only visible to subclasses
    protected interface ProtectedInterface {
    }

    // Everyone can call it
    public void publicAction() {
    }

    // Only subclass can call it
    protected void protectedAction() {
    }

    // No one can call it
    private void privateAction() {
    }

    // Only subclasses in the same package can call it
    void packageAction() {
    }
}
package com.javacodegeeks.advanced.design;

// Resides in the same package as parent class
public class Child extends Parent implements Parent.ProtectedInterface {
    @Override
    protected void protectedAction() {
        // Calls parent's method implementation
        super.protectedAction();
    }

    @Override
    void packageAction() {
        // Do nothing, no call to parent's method implementation
    }

    public void childAction() {
        this.protectedField = "value";
    }
}
Inheritance is a very large topic in itself, with a lot of fine detail specific to Java. However, there are a few rules that are easy to follow and can go a long way in keeping the class hierarchy brevity. In Java, each subclass can override any inherited methods of its parent unless it has been declared final. However, there is no special syntax or keyword to mark a method as overridden, which can lead to confusion. This is why the @Override annotation was introduced : whenever your goal is to override an inherited method, please use the @Override annotation to succinctly indicate it. Another dilemma that Java developers constantly face in design is the construction of class hierarchies (with concrete or abstract classes) versus the implementation of interfaces. We strongly recommend favoring interfaces over classes or abstract classes whenever possible. Interfaces are lighter, easier to test and maintain, and also minimize the side effects of implementation changes. Many advanced programming techniques, such as creating proxy classes in the Java standard library, rely heavily on interfaces.

10. MULTIPLE INHERITANCE

Unlike C++ and some other languages, Java does not support multiple inheritance: in Java, each class can have only one direct parent (with the class Objectat the top of the hierarchy). However, a class can implement multiple interfaces, and thus interface stacking is the only way to achieve (or simulate) multiple inheritance in Java.
package com.javacodegeeks.advanced.design;

public class MultipleInterfaces implements Runnable, AutoCloseable {
    @Override
    public void run() {
        // Some implementation here
    }

    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
Implementing multiple interfaces is actually quite powerful, but often the need to use the implementation over and over again leads to deep class hierarchy as a way to overcome Java's lack of support for multiple inheritance. 
public class A implements Runnable {
    @Override
    public void run() {
        // Some implementation here
    }
}
// Class B wants to inherit the implementation of run() method from class A.
public class B extends A implements AutoCloseable {
    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
// Class C wants to inherit the implementation of run() method from class A
// and the implementation of close() method from class B.
public class C extends B implements Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}
And so on... The recent release of Java 8 somewhat addresses the issue with default method injection. Because of default methods, interfaces actually provide not only a contract, but also an implementation. Therefore, classes that implement these interfaces will also automatically inherit these implemented methods. For example:
package com.javacodegeeks.advanced.design;

public interface DefaultMethods extends Runnable, AutoCloseable {
    @Override
    default void run() {
        // Some implementation here
    }

    @Override
    default void close() throws Exception {
       // Some implementation here
    }
}

// Class C inherits the implementation of run() and close() methods from the
// DefaultMethods interface.
public class C implements DefaultMethods, Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}
Keep in mind that multiple inheritance is a very powerful, but at the same time dangerous tool. The well-known Diamond of Death problem is often cited as a major flaw in the implementation of multiple inheritance, forcing developers to design class hierarchies very carefully. Unfortunately, Java 8 interfaces with default methods also fall victim to these defects.
interface A {
    default void performAction() {
    }
}

interface B extends A {
    @Override
    default void performAction() {
    }
}

interface C extends A {
    @Override
    default void performAction() {
    }
}
For example, the following code snippet will fail to compile:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
At this point, it's fair to say that Java as a language has always tried to avoid the corner cases of object-oriented programming, but as the language evolves, some of those cases have suddenly begun to appear. 

11. INHERITANCE AND COMPOSITION

Fortunately, inheritance is not the only way to design your class. Another alternative that many developers believe is much better than inheritance is composition. The idea is very simple: instead of creating a hierarchy of classes, they need to be composed of other classes. Let's look at this example:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
The class Vehicleconsists of an engine and wheels (plus many other parts that are left aside for simplicity). However, it can be said that a class Vehicleis also an engine, so it can be designed using inheritance. 
public class Vehicle extends Engine {
    private Wheels[] wheels;
    // ...
}
Which design solution would be correct? The general core guidelines are known as the IS-A (is) and HAS-A (contains) principles. IS-A is an inheritance relationship: a subclass also satisfies the class specification of the parent class and a variation of the parent class. subclass) extends its parent. If you want to know whether one entity extends another, do a match test - IS-A (is).") Therefore, HAS-A is a composition relationship: a class owns (or contains) an object that In most cases, the HAS-A principle works better than IS-A for a number of reasons: 
  • Design is more flexible;
  • The model is more stable because change does not propagate through the class hierarchy;
  • A class and its composition are loosely coupled compared to composition, which tightly couples a parent and its subclass.
  • The logical train of thought in a class is simpler, since all its dependencies are included in it, in one place. 
Regardless, inheritance has its place and solves a number of existing design problems in a variety of ways, so it shouldn't be neglected. Please keep these two alternatives in mind when designing your object-oriented model.

12. ENCAPSULATION.

The concept of encapsulation in object-oriented programming is to hide all implementation details (like operating mode, internal methods, etc.) from the outside world. The benefits of encapsulation are maintainability and ease of change. The internal implementation of the class is hidden, working with class data occurs exclusively through public methods of the class (a real problem if you are developing a library or framework used by many people). Encapsulation in Java is achieved through visibility and accessibility rules. In Java, it is considered best practice to never expose fields directly, only through getters and setters (unless the fields are marked final). For example:
package com.javacodegeeks.advanced.design;

public class Encapsulation {
    private final String email;
    private String address;

    public Encapsulation( final String email ) {
        this.email = email;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getEmail() {
        return email;
    }
}
This example is reminiscent of what is called JavaBeans in the Java language: standard Java classes are written according to a set of conventions, one of which allows fields to be accessed only using getter and setter methods. As we already emphasized in the inheritance section, please always adhere to the minimum publicity contract in a class, using the principles of encapsulation. Everything that should not be public should become private (or protected/ package private, depending on the problem you are solving). This will pay off in the long run by giving you freedom to design without (or at least minimizing) breaking changes. 

13. FINAL CLASSES AND METHODS

In Java, there is a way to prevent a class from becoming a subclass of another class: the other class must be declared final. 
package com.javacodegeeks.advanced.design;

public final class FinalClass {
}
The same final keyword in a method declaration prevents subclasses from overriding the method. 
package com.javacodegeeks.advanced.design;

public class FinalMethod {
    public final void performAction() {
    }
}
There are no general rules for deciding whether a class or methods should be final or not. Final classes and methods limit extensibility and it is very difficult to think ahead whether a class should or should not be inherited, or whether a method should or should not be overridden in the future. This is especially important for library developers, since design decisions like this could significantly limit the applicability of the library. The Java Standard Library has several examples of final classes, the most famous being the String class. At an early stage, this decision was made to prevent any attempts by developers to come up with their own, “better” solution to implementing string. 

14. WHAT'S NEXT

In this part of the lesson, we covered the concepts of object-oriented programming in Java. We also took a quick look at contract programming, touching on some functional concepts and seeing how the language has evolved over time. In the next part of the lesson, we're going to meet generics and how they change the way we approach type safety in programming. 

15. DOWNLOAD SOURCE CODE

You can download the source here - advanced-java-part-3 Source: How to design Classes an
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION