Diferències
Ací es mostren les diferències entre la revisió seleccionada i la versió actual de la pàgina.
| Ambdós costats versió prèvia Revisió prèvia | |||
| info:cursos:pue:python-pcpp1:m1:2.8 [05/11/2023 21:34] – suprimit - edició externa (Unknown date) 127.0.0.1 | info:cursos:pue:python-pcpp1:m1:2.8 [05/11/2023 21:34] (actual) – ↷ Page moved from info:cursos:pue:python-pcpp1:2.8 to info:cursos:pue:python-pcpp1:m1:2.8 mate | ||
|---|---|---|---|
| Línia 1: | Línia 1: | ||
| + | = 2.8 Composition vs Inheritance - two ways to the same destination | ||
| + | |||
| + | So far we've been using and following the inheritance concept when modeling our classes to represent real-life issues. Inheritance is a great concept, one of the most important foundations of object-oriented programming that models a tight relation between two classes: the base class and the derived class, called a subclass. | ||
| + | |||
| + | The result of this relation is a subclass class that inherits **all** methods and **all** properties of the base class, and allows a subclass to extend everything that has been inherited. By extending a base class, you are creating a more specialized class. Moreover, we say that these classes are **tightly** coupled. | ||
| + | |||
| + | **Inheritance** models what is called an **is a** relation. | ||
| + | Examples: | ||
| + | |||
| + | * a Laptop **is a** (specialized form of) Computer; | ||
| + | * a Square **is a** (specialized form of) Figure; | ||
| + | * a Hovercraft **is a** Vehicle. | ||
| + | |||
| + | The primary use of inheritance is to reuse the code. If two classes perform similar tasks, we can create a common base class for them, to which we transfer identical methods and properties. This will facilitate testing and potentially increase application reliability in case of changes. In case of any problems, it will also be easier to find the cause of the error. | ||
| + | |||
| + | As a result, your inheriting classes could form a tree. | ||
| + | |||
| + | {{ : | ||
| + | |||
| + | Note: the hierarchy grows from top to bottom, like tree roots, not branches. The most general, and the widest, class is always at the top (the superclass) while its descendants are located below (the subclasses). | ||
| + | |||
| + | What could be inherited in this “Vehicles” structure? | ||
| + | |||
| + | All classes derived from Vehicles own properties and methods responsible for informing the user of its mileage, starting and stopping the vehicle, fueling, etc. Once you inherit a “mileage” property from the base class, then it is present in all subclasses. | ||
| + | |||
| + | The same principle should apply to the '' | ||
| + | |||
| + | The inheritance concept is a powerful one, but you should remember that with great power comes great responsibility. When you are reckless, then with the inheritance (especially multiple inheritances) you can create a huge, complex, and hierarchical structure of classes. | ||
| + | |||
| + | This hierarchy would be hard to understand, debug, and extend. This phenomenon is known as the class explosion problem, and is one of the antipatterns of programming. | ||
| + | |||
| + | Inheritance is not the only way of constructing adaptable objects. You can achieve similar goals by using a concept named composition. | ||
| + | |||
| + | This concept models another kind of relation between objects; it models what is called a **has a** relation. | ||
| + | |||
| + | Examples: | ||
| + | |||
| + | * a Laptop **has a** network card; | ||
| + | * a Hovercraft **has a** specific engine. | ||
| + | |||
| + | Composition is the process of composing an object using other different objects. The objects used in the composition deliver a set of desired traits (properties and/or methods) so we can say that they act like blocks used to build a more complicated structure. | ||
| + | |||
| + | It can be said that: | ||
| + | * inheritance extends a class' | ||
| + | * composition projects a class as a container (called a composite) able to store and use other objects (derived from other classes) where each of the objects implements a part of a desired class' | ||
| + | |||
| + | Let's try to write some code to see how composition works. | ||
| + | |||
| + | Look at the simple code presented in the editor pane. | ||
| + | |||
| + | <code python> | ||
| + | class Car: | ||
| + | def __init__(self, | ||
| + | self.engine = engine | ||
| + | |||
| + | |||
| + | class GasEngine: | ||
| + | def __init__(self, | ||
| + | self.hp = horse_power | ||
| + | |||
| + | def start(self): | ||
| + | print(' | ||
| + | |||
| + | |||
| + | class DieselEngine: | ||
| + | def __init__(self, | ||
| + | self.hp = horse_power | ||
| + | |||
| + | def start(self): | ||
| + | print(' | ||
| + | |||
| + | |||
| + | my_car = Car(GasEngine(4)) | ||
| + | my_car.engine.start() | ||
| + | my_car.engine = DieselEngine(2) | ||
| + | my_car.engine.start() | ||
| + | |||
| + | </ | ||
| + | |||
| + | The “Car” class is loosely coupled with the “engine” component. It’s a composite object. | ||
| + | |||
| + | The main advantages are: | ||
| + | |||
| + | * whenever a change is applied to the engine object, it does not influence the “Car” class object structure; | ||
| + | * you can decide what your car should be equipped with. | ||
| + | |||
| + | Our “Car” could be equipped with two different kinds of engine – a gas one or a diesel one. The developer' | ||
| + | |||
| + | Run the code to confirm your expectations. | ||
| + | |||
| + | <code ; output> | ||
| + | Starting 4hp gas engine | ||
| + | Starting 2hp diesel engine | ||
| + | </ | ||
| + | |||
| + | To favor composition over inheritance is a design principle that gives the design higher flexibility, | ||
| + | |||
| + | In fact, with the composition approach you can more easily respond to the requirement changes regarding classes, as it does not require deep dependency investigations which you would spot while implementing code with the inheritance approach. | ||
| + | |||
| + | |||
| + | On the other hand, there is a clear drawback: composition transfers additional responsibilities to the developer. The developer should assure that all component classes that are used to build the composite should implement the methods named in the same manner to provide a common interface. | ||
| + | |||
| + | In the case of inheritance, | ||
| + | |||
| + | Which way should you choose? | ||
| + | |||
| + | Before we answer the question, let's mention a few more things: | ||
| + | |||
| + | * inheritance and composition are not mutually exclusive. Real-life problems are hardly every pure “is a” or “has a” cases; | ||
| + | * treat both inheritance and composition as supplementary means for solving problems; | ||
| + | * there is nothing wrong with composing objects of ... classes that were built using inheritance. The next example code should shed some light on this case. | ||
| + | |||
| + | <code python> | ||
| + | class Base_Computer: | ||
| + | def __init__(self, | ||
| + | self.serial_number = serial_number | ||
| + | |||
| + | |||
| + | class Personal_Computer(Base_Computer): | ||
| + | def __init__(self, | ||
| + | super().__init__(sn) | ||
| + | self.connection = connection | ||
| + | print(' | ||
| + | |||
| + | |||
| + | class Connection: | ||
| + | def __init__(self, | ||
| + | self.speed = speed | ||
| + | |||
| + | def download(self): | ||
| + | print(' | ||
| + | |||
| + | |||
| + | class DialUp(Connection): | ||
| + | def __init__(self): | ||
| + | super().__init__(' | ||
| + | |||
| + | def download(self): | ||
| + | print(' | ||
| + | super().download() | ||
| + | |||
| + | |||
| + | class ADSL(Connection): | ||
| + | def __init__(self): | ||
| + | super().__init__(' | ||
| + | |||
| + | def download(self): | ||
| + | print(' | ||
| + | super().download() | ||
| + | |||
| + | |||
| + | class Ethernet(Connection): | ||
| + | def __init__(self): | ||
| + | super().__init__(' | ||
| + | |||
| + | def download(self): | ||
| + | print(' | ||
| + | super().download() | ||
| + | |||
| + | # I started my IT adventure with an old-school dial up connection | ||
| + | my_computer = Personal_Computer(' | ||
| + | my_computer.connection.download() | ||
| + | |||
| + | # then it came year 1999 with ADSL | ||
| + | my_computer.connection = ADSL() | ||
| + | my_computer.connection.download() | ||
| + | |||
| + | # finally I upgraded to Ethernet | ||
| + | my_computer.connection = Ethernet() | ||
| + | my_computer.connection.download() | ||
| + | |||
| + | </ | ||
| + | |||
| + | You should always examine the problem your code is about to solve before you start coding. If the problem can be modeled using an “is a” relation, then the inheritance approach should be implemented. | ||
| + | |||
| + | Otherwise, if the problem can be modeled using a “has a” relation, then the choice is clear – composition is the solution. | ||
| + | |||
| + | Don't hesitate to experiment and re-implement your solution. | ||
| + | |||
| + | <code ; output> | ||
| + | The computer costs $1000 | ||
| + | Dialling the access number ... Downloading at 9600bit/s | ||
| + | Waking up modem ... Downloading at 2Mbit/s | ||
| + | Constantly connected... | ||
| + | </ | ||
| + | |||
| + | * There is a “Base_Computer” class that represents a generic computer. A generic computer has only a serial number; | ||
| + | * there is a “Personal_Computer” class that is built upon the “Base_Computer” class and represents a computer that is able to connect to the internet; | ||
| + | * there is a generic “Connection” class that holds information about the connection speed and handles the download() method. This class is independent of any computer class; | ||
| + | * there are the “Connection” subclasses, more specialized than the “Connection” class: | ||
| + | * “Dialup” | ||
| + | * “ADSL” | ||
| + | * “Ethernet” | ||
| + | |||
| + | * When we start with our personal computer, we set the serial number to 1995 and equip it with a dialup connection. This an example of composition. | ||
| + | * It is possible to download some data using a slow dialup connection; | ||
| + | * later, we equip our personal computer with a more advanced connection device. There is no need to recreate the computer object – we just arm it with a new component; | ||
| + | * the last steps are about arming our old computer with a fast connection and downloading some data. | ||
| + | |||
| + | == LAB | ||
| + | === Objectives | ||
| + | * improving the student' | ||
| + | === Scenario | ||
| + | Imagine that you are an automotive fan, and you are able to build a car from a limited set of components. | ||
| + | |||
| + | Your task is to : | ||
| + | * define classes representing: | ||
| + | * tires (as a bundle needed by a car to operate); methods available: '' | ||
| + | * engine; methods available: '' | ||
| + | * vehicle; method available: '' | ||
| + | * based on the classes defined above, create the following objects: | ||
| + | * two sets of tires: city tires (size: 15), off-road tires (size: 18) | ||
| + | * two engines: electric engine, petrol engine | ||
| + | * instantiate two objects representing cars: | ||
| + | * the first one is a city car, built of an electric engine and city tires | ||
| + | * the second one is an all-terrain car build of a petrol engine and off-road tires | ||
| + | * play with the cars by calling methods responsible for interaction with components. | ||
| + | |||
| + | === resposta | ||
| + | <code python> | ||
| + | # | ||
| + | # -*- coding: utf-8 -* | ||
| + | |||
| + | class Tires(): | ||
| + | def __init__(self, | ||
| + | self.size = size | ||
| + | def get_pressure(self): | ||
| + | print(" | ||
| + | def pump(self): | ||
| + | print(" | ||
| + | | ||
| + | class Engine(): | ||
| + | def __init__(self, | ||
| + | self.fuel_type = fuel_type | ||
| + | def start(self): | ||
| + | print(" | ||
| + | def stop(self): | ||
| + | print(" | ||
| + | def get_state(self): | ||
| + | print(" | ||
| + | |||
| + | class Car(): | ||
| + | def __init__(self, | ||
| + | self.engine = engine | ||
| + | self.tires = tires | ||
| + | |||
| + | cityTires = Tires(15) | ||
| + | offRoadTires = Tires(18) | ||
| + | electricEngine = Engine(" | ||
| + | petrolEngine = Engine(" | ||
| + | |||
| + | car1 = Car(electricEngine, | ||
| + | car1.engine.start() | ||
| + | car1.tires.get_pressure() | ||
| + | car1.tires.pump() | ||
| + | car1.engine.get_state() | ||
| + | car1.engine.stop() | ||
| + | |||
| + | car2 = Car(petrolEngine, | ||
| + | car2.engine.start() | ||
| + | car2.tires.get_pressure() | ||
| + | car2.tires.pump() | ||
| + | car2.engine.get_state() | ||
| + | car2.engine.stop() | ||
| + | |||
| + | car1.tires=offRoadTires | ||
| + | car1.engine.get_state() | ||
| + | car1.tires.get_pressure() | ||
| + | </ | ||
| + | <code ; output> | ||
| + | starting | ||
| + | tires pressure | ||
| + | tires pump 15 | ||
| + | car state Electric | ||
| + | stopping | ||
| + | starting | ||
| + | tires pressure | ||
| + | tires pump 18 | ||
| + | car state Petrol | ||
| + | stopping | ||
| + | car state Electric | ||
| + | </ | ||