Properties vs methods – InformTFB

Properties vs methods

Properties vs methods

At first glance, such a question as choosing a property or method seems simple. But this is as long as you don’t encounter any misunderstanding in your team. Although there are well-established practices, their wording is rather vague. There is a certain degree of freedom in this kind of question, which makes it difficult for us to choose, and the apparent simplicity gives fertile ground for disputes.

Background of Java programmers

A programming language is a programmer’s primary tool. The presence or absence of any constructs forms a certain coding style. For example, in Java, there are no properties, only fields and methods.

Let’s take the following class as an example:

public class Point {
    public double x;
    public double y;
}

We have a class that describes a point on the plane. What’s wrong with it? First, since these are public fields, they are editable from the outside. Second, we reveal the implementation details that we store a point in Cartesian coordinates.

That’s why they usually don’t write this way, but rather encapsulate the fields behind getters and setters:

public class Point {

    private double x;
    private double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }
}

Standard practice in Java, the IDE even has special generators for this.
We have removed direct access to fields using the get and set methods.

Although they look like functions, they are essentially getters and setters.

Sugar in Kotlin

In Kotlin we have properties and we can not write a sheet from the get and set methods:

class Point(var x: Double, var y: Double)

It looks concise, doesn’t it? Accessing the property is also convenient: xinstead getX()of .

If necessary, we can redefine the getter or setter.:

var x: Double = 0 
    set(value) {
        if (value >= 0) field = value
    }

In Kotlin we are always dealing with the properties, the fields are hidden behind the setters and getters. In other words, we have all the advantages of methods, but we can still treat them as fields.

They gave us sugar rides, but the old habits remained. I often notice that Java programmers continue to write get and set methods in Kotlin classes when this is no longer necessary. The name of the methods themselves indicates that they can be converted to properties. But is it always necessary to prefer a property over a method? If we have a function without parameters, the choice is not always obvious.

Generally accepted agreements

The official documentation tells us that functions without parameters can be interchanged with read-only properties.

Below is an algorithm that can be used to determine when to choose a property over a method:

  • if the property does not throw an exception)
  • cheap to compute (or can be cached on first run)
  • returns the same value for each call, if the object’s state has not changed

I was expecting a more detailed guide. For the first point, I would add about any side effect that can occur when calling the function. The last point is also not so simple.

For example, if we have the User class:

class User(
    val firstName: String,
    val lastName: String
)

Do I need to make the full name a property or a method?

val fullName get() = "$firstName $lastName"

In this case, a new String object is always created, although this is an implementation detail. But the values will always be equal when compared via equals. In extreme cases, you can cache the fullName, sacrificing memory.

But the point about computability raises the most questions.

Complex or easily computable properties

This seems to be a rather vague requirement. What does hard-to-calculate mean? If heavy calculations are meant, such as a request to the network or database, then we will have to put the call in a separate thread. In this case, the asynchronous call will look different: a method with a callback, a reactive thread, or a coroutine. But it’s probably not about that.

Consider the following example:

class DocumentModel {
     val activePageIndex: Int
}

We have a document model class that has an activePageIndex property that returns the current page index. We don’t know the internal implementation, but we assume that this is a property and according to the accepted Convention, we can not worry about performance and safely use it in a loop:

images.forEach { image ->
   document addImage(image, document.activePageIndex)
}

For example, to get the current page, you need to run through the entire document, that is, do some calculations. In this case, it is optimal to save the current page to a variable before using it in a loop.:

val pageIndex = document.activePageIndex
images.forEach { image ->
   document addImage(image, pageIndex)
}

But to understand this, you need to look into the implementation. When a programmer sees a property, he makes some assumptions about its use, assuming that the class author took care of the accepted Convention. In this case, the author was negligent and misled. In a good way, you need to make a method instead of a property to get the active page and name it something else, for example, findActivePageIndex.

Interface is more important than implementation

On the one hand, the example above shows how important it is to think about the interface as it will be used on the client side. On the other hand, the implementation imposes a constraint on the interface. If the calculations are complex, then use a function instead of a property. Here we come into some contradiction, which is more primary, the implementation or the interface? We can’t say anything about efficiency in advance, and it’s tempting to go to the extreme of always making methods in the interface. Especially after Java, it is unusual to see properties in the interface. At the same time, a method that starts with the word get or set does not bother anyone.

When designing an interface, in my opinion, we should first think about the client code. If we put the implementation ahead of the interface, we will get a bad class API. The price of such an error can be refactoring all calls in the project. Library developers understand this very well when an API change breaks third-party code or changes the behavior of a class that the client did not expect.

A common situation is that an interface is created by refactoring in the IDE, extracting from an existing class. This seems to be a vicious practice. As a result of such refactoring, we end up with a mess of methods that are often unrelated to each other.

The interface is just as important as choosing the right name for a variable or function. This is a contract between the author and the user of the interface. There can be many implementations, including the most suboptimal ones. These are the details. When implementing properties, we must make sure that it conforms to the accepted Convention of easy computability.

Fundamental differences

OK, let’s assume we assume that the interface is primary to the implementation. But what considerations should be taken into account when designing it? When should I choose a property and when should I use a method?

The properties and methods there are deeper differences. When we design a class, it can be divided into two conditional parts::

  • Condition. It can be considered as data that describes the characteristics or features of an object. In this case, properties are more appropriate.
  • Behaviour. That is, what can be done with the object. Methods are responsible for this.
    They usually change state.

This is a fairly simple rule that will help you choose a property or method.
Methods usually start with a verb, and if you can’t find anything better than get/set, then this is a clear sign of the property.

Instead of concluding

Let’s analyze the initial example, but make it an interface:

interface Point {
    var x: Double
    var y: Double
}

So what’s wrong with it?

First, two separate setters for the x and y coordinates. When we define a point in space, we define them in pairs, that is, atomically. By changing them independently, we create the possibility for mistakes.

Add a method for setting coordinates and make the x and y coordinates read-only:

interface Point {
    val x: Double
    val y: Double
    fun setCoordinates(x: Double, y: Double)
}

Secondly, the interface is not flexible enough. Sometimes it is convenient to work with polar coordinates, but only rectangular ones are shown in the interface. So we implicitly expose the implementation.

Expanding the interface:

interface Point {
    val x: Double
    val y: Double
    val radius: Double
    val angle: Double
    fun setCartesian(x: Double, y: Double)
    fun setPolar(radius: Double, angle: Double)
}

As we can see, designing a good interface is not so easy. Although it would be possible to limit yourself to the data class:

data class Point(
    var x: Double,
    var y: Double
)

Anderson
Anderson
Web site editor and tester.

Leave a Reply

Your email address will not be published. Required fields are marked *