📜 ⬆️ ⬇️

Vs interface

One of the principles of object-oriented design is programming at the interface level, not at the implementation level. Apparently, due to the fact that the code in books and articles on design is presented mainly in Java, programmers in other languages, especially with dynamic typing, have difficulty transferring knowledge from these books and articles to their working programming language.


Often, the difficulty in understanding the principle of "programming at the interface level" lies in concentrating on the instrument, and not on the meaning. Because of the presence in Java of the interface keyword, a distortion in the understanding of the principle occurs, and it becomes “program using the interface ”. Since Python does not have a tool in the form of the interface keyword, some Pythonists skip this principle.


In the book Gangs of Four, examples are given in Smalltalk and C ++. Both of these languages ​​do not have the interface keyword, but this does not prevent the authors from applying the principle using the language constructs at their disposal:


Manipulating objects strictly through an abstract class interface has two advantages:

  • the client does not need to have information about the specific types of objects that he uses, provided that they all have the interface expected by the client;
  • it is not necessary for the client to "know" about the classes with which the objects are implemented. The client is only aware of the abstract class (or classes) that define the interface.


These advantages so significantly reduce the number of dependencies between subsystems, that one can even formulate the principle of object-oriented design for reuse: program in accordance with the interface, and not with the implementation .

But even the advantages cited in the quotation are not the only ones, if you look at the principle from a wider angle.


Real world


The most common definition of the interface from the Russian-language Wikipedia looks like this:


The interface is a “common border” between the individual systems through which they interact; a set of tools and rules that ensure the interaction of individual systems.

Man is a system, which means that each of us is faced daily with many interfaces for interacting with other systems. Examples of interesting interfaces in the real world can be seen in the posts from Mosigra ( one , two , three , four ). Interaction with a good interface occurs without unnecessary trouble, we do not notice how to use it. Moreover, interacting with even the most terrible interface, we pay attention only to the inconvenience of the interface, while the implementation in both cases is hidden from our eyes.


When working with a microwave oven, we use a set of tools: controls to turn on the oven, set the time, power, and the heating mode. As well as a set of rules: in order to warm up the dish, you need to put it inside the oven, close the door and start heating up. You have to agree that if we needed to change the electric circuit of the furnace to heat up the dinner, setting up the frequency of the magnetron, this would be inconvenient. Controlling the oven with the help of the interface allows us not only to reduce the time and labor costs to achieve the ultimate goal - to warm up lunch, but also gives the opportunity to use the knowledge of the interface when working with microwave ovens of other types and from other manufacturers.


World of code


Conceptually, the meaning of interfaces in the program code does not differ from that in the real world. The interface is all the same "common border" between separate systems. Only in this case, systems are services, microservices, packages, classes, or even functions. Each of these units of software code is a system with its own boundary, through which it is necessary to interact and which should not be violated.


The situation described above about the need to change the electrical circuit of a microwave oven to heat a dinner seems absurd. But the programmer has to deal with this in daily practice. Even a bad name for a method that does not express its intent can lead to a disclosure of the implementation:


 class UserCollection: #    def linear_search_for(user: User) -> bool: for saved_user in self._all_users: if saved_user == user: return True return False 

This name tells us about the algorithm used inside, as well as the data structure that underlies the UserCollection . All this information is superfluous at this level of abstraction, poorly conveys intentions and inconvenient for further expansion. To make the interface cleaner, let’s express in the name of the method “what” the code does, and not “how” it does:


 class UserCollection: #    def includes(user: User) -> bool: '''    ''' 

Reflective names make a large contribution to the ability to program at the interface level, but just names are not enough to implement the principle. For example, this simple function, which has a clear name, is difficult to use, knowing only the interface:


 from utils import DatabaseConfig # DatabaseConfig    , #     def is_password_valid(password: str) -> bool: min_length = DatabaseConfig().password_min_length return len(password) > min_length 

The interface deceives us by stating that only a password is enough for work. Calling this function in an environment where the required database is not raised will result in an error that will force you to turn to the implementation. Enrich the interface to eliminate the need to access the implementation. In this case, an explicit transmission of the min_length parameter is min_length :


 #   DatabaseConfig    def is_password_valid(password: str, min_length: int) -> bool: return len(password) > min_length 

Implicit DatabaseConfig dependency resolved. In addition to this, we obtained a testable function, which is a real black box with all the necessary input parameters and a known type of output parameter.


Implicit dependencies reveal the implementation of any piece of software code. Python allows you to write code that will be executed when you import the file. At first glance, this may seem harmless, but the implicit dependency created in this way entails many problems:


 # utils.py class DatabaseConfig: '''        ''' config = DatabaseConfig() #          def is_password_valid(password: str, min_length: int) -> bool: return len(password) > min_length 

 # user.py from utils import is_password_valid #      # DatabaseConfig     class User: def __init__(self, name: str, password: str): self.name = name self.password = password def change_password(self, new_password: str) -> None: if not is_password_valid(new_password, min_length=6): raise Exception('Invalid password') self.password = new_password 

Importing the User class into the interpreter or the test, if the required database is not running, will again end with an error that reveals the implementation. There is no point in from utils import is_password_valid class interface, since the cause of the trouble in this case is the expression from utils import is_password_valid , namely the global variable that is created during the import. Another disadvantage of a global variable is that it can cause inability to program at the interface level. You can solve the problem by creating an instance of DatabaseConfig at the time the application starts and explicitly passing the instance to all interested objects.


The absence of implicit dependencies and reflecting the essence of the name, protecting us from implementation details, still does not allow us to get all the benefits of programming at the interface level. It's time to turn to programming strictly through the abstract class interface. Kent Beck, in his book "Smalltalk Best Practice Patterns", writes:


I am in a good shape.
')
...

Replacing objects - Good style leads to easily replaceable objects. “I’m trying to make it a bit different,” he said.

Using an interface defined by an abstract class instead of a specific class is a convenient technique for creating replaceable objects. Python has the ability to create abstract classes using the abc standard library module , but for code compactness, the examples will use an approach where unrealized methods of the abstract class throw NotImplementedError .


Suppose we need to implement the display of the weather forecast for today and for the current week. We receive the weather forecast from some third-party resource. In order not to bind to a specific resource, as well as to the data format returned by the resource, it is necessary to formalize the way of communicating in the form of an abstract class, and the format of the data as a value object:


 # weather.py from typing import List, NamedTuple class Weather(NamedTuple): max_temperature_: int avg_temperature_: int min_temperature_c: int class WeatherService: def get_today_weather(self, city: str) -> Weather: raise NotImplementedError def get_week_weather(self, city: str) -> List[Weather]: raise NotImplementedError 

Without a specific implementation, the client of our code, relying on the provided interfaces, will already be able to start testing and developing, using instead of a real service substitute objects:


 # test.py from client import WeatherWidget from weather import Weather, WeatherService class FakeWeatherService(WeatherService): def __init__(self): self._weather = Weather(max_temperature_ = 24, avg_temperature_ = 20, min_temperature_c = 16) def get_today_weather(self, city: str) -> Weather: return self._weather def get_week_weather(self, city: str) -> List[Weather]: return [self._weather for _ in range(7)] def test_present_today_weather_in_string_format(): weather_service = FakeWeatherService() widget = WeatherWidget(weather_service) expected_string = ('Maximum Temperature: 24 °C' 'Average Temperature: 20 °C' 'Minimum Temperature: 16 °C') assert widget.today_weather == expected_string 

The interface gives us flexibility: if our users are not satisfied with the accuracy of the forecast, we can easily switch to another weather forecast resource by writing a class that implements the WeatherService interface.


Using the principle of "program in accordance with the interface, but not with the implementation of" allows you to create a more flexible design of the application, relieve the head of the developer and improve communication within the team. All this makes the system more suitable to support and add new functionality. In Python, there is no interface keyword, but there are other ways to implement the principle: reflecting the essence of the name, eliminating implicit dependencies, and using abstract classes. Let's pay more attention to the essence of the principles underlying the good code, and not focus all our attention on the tools.


UPD
pacahon suggested python-way interface for a UserCollection


 class UserCollection: #    def __contains__(user: User) -> bool: '''    ''' 

The __contains__ method allows __contains__ to check the ownership of elements with in and not in . If you use type-hints in the __contains__ interface, PyCharm will tell you that int in this case is the wrong type:


 print(1 in UserCollection()) 

Source: https://habr.com/ru/post/332578/


All Articles