How to Write Beautiful Models: An Overview of Design Principles in Object-Oriented Programming!

  Object-oriented programming is very important in the process of implementing ideas and even systems. Whether we use TensorFlow or PyTorch to build models, we need to use classes and methods more or less. The use of classes to build models will make the code very readable and organized. This article introduces the design principles that need to be paid attention to when using classes and methods to build models in algorithm implementation. They can make our machine learning code more beautiful. charming.

Object-oriented programming (OOP) is supported and encouraged by most modern programming languages. Even though we seem to be seeing some divergence recently as people start using programming languages ​​that are less influenced by OOP (e.g. Go, Rust, Elixir, Elm, Scala), most have object-oriented properties. The design principles we outline here also apply to non-OOP programming languages.

To successfully write clean, high-quality, maintainable, and extensible code, we need to use Python as an example to understand design principles that have proven effective over the past few decades.

object type

Because we're building code around objects, it's useful to distinguish between their different responsibilities and variations. Generally speaking, there are three types of objects in object-oriented programming.

1. Entity objects

Such objects usually correspond to some real entities in the problem space. For example, if we want to build a role-playing game (RPG), the simple Hero class is an entity object.

class Hero: def __init__(self, health, mana): self._health = health self._mana = mana def attack(self) -> int: """ Returns the attack damage of the Hero """ return 1 def take_damage(self, damage: int): self._health -= damage def is_alive(self): return self._health > 0

Such objects usually contain properties about themselves (such as health or mana) that are modifiable according to specific rules.

2. Control Object

Control objects (sometimes called management objects) are primarily responsible for coordinating with other objects, which are objects that manage and invoke other objects. A great example from our RPG case above, the Fight class controls two heroes and pits them against each other.

class Fight: class FightOver(Exception): def __init__(self, winner, *args, **kwargs): self.winner = winner super(*args, **kwargs) def __init__(self, hero_a: Hero, hero_b: Hero): self._hero_a = hero_a self._hero_b = hero_b self.fight_ongoing = True self.winner = None def fight(self): while self.fight_ongoing: self._run_round() print(f'The fight has ended! Winner is #{self.winner}') def _run_round(self): try: self._run_attack(self._hero_a, self._hero_b) self._run_attack(self._hero_b, self._hero_a) except self.FightOver as e: self._finish_round(e.winner) def _run_attack(self, attacker: Hero, victim: Hero): damage = attacker.attack() victim.take_damage(damage) if not victim.is_alive(): raise self.FightOver(winner=attacker) def _finish_round(self, winner: Hero): self.winner = winner self.fight_ongoing = False

In such a class, encapsulating programming logic for matches can give us several benefits: one of them is the extensibility of actions. We can easily pass heroes involved in combat to non-player characters (NPCs) so they can utilize the same API. We can also easily inherit this class and override some functions to meet new needs.

3. Boundary Object

These are objects at the edge of the system. Any object that takes input from or produces output to another system can be classified as a boundary object, whether that system is a user, the Internet, or a database.

class UserInput: def __init__(self, input_parser): self.input_parser = input_parser def take_command(self): """ Takes the user's input, parses it into a recognizable command and returns it """ command = self._parse_input(self._take_input()) return command def _parse_input(self, input): return self.input_parser.parse(input) def _take_input(self): raise NotImplementedError()class UserMouseInput(UserInput): passclass UserKeyboardInput(UserInput): passclass UserJoystickInput(UserInput): pass

These boundary objects are responsible for passing information to and from the system. For example, to receive user commands, we need a bounds object to translate keyboard input (such as a space bar) into a recognizable domain event (such as a character's jump).

Bonus: Value Object

A value object represents a simple value in a domain. They cannot be changed, they are not constant.

If we combine them in our game, the Money class or the Damage class would represent such an object. The above objects allow us to easily distinguish, find and debug related functions that are not possible with just basic integer arrays or integers.

class Money: def __init__(self, gold, silver, copper): self.gold = gold self.silver = silver self.copper = copper def __eq__(self, other): return self.gold == other.gold and self.silver == other.silver and self.copper == other.copper def __gt__(self, other): if self.gold == other.gold and self.silver == other.silver: return self.copper > other.copper if self.gold == other.gold: return self.silver > other.silver return self.gold > other.gold def __add__(self, other): return Money(gold=self.gold + other.gold, silver=self.silver + other.silver, copper=self.copper + other.copper) def __str__(self): return f'Money Object(Gold: {self.gold}; Silver: {self.silver}; Copper: {self.copper})' def __repr__(self): return self.__str__()print(Money(1, 1, 1) == Money(1, 1, 1))# => Trueprint(Money(1, 1, 1) > Money(1, 2, 1))# =>Falseprint(Money(1, 1, 0) + Money(1, 1, 1))# => Money Object(Gold: 2; Silver: 2; Copper: 1)

They can be classified as subcategories of entity objects.

key design principles

Design principles are rules in software design that have proven valuable over the years. Strict adherence to these principles helps the software achieve top-notch quality.

Abstraction

Abstraction is a kind of thought that reduces a concept to its original essence in a certain context. It allows us to disassemble a concept to better understand it.

The above game example illustrates the abstraction, let's see how the Fight class is constructed. We use it in the simplest possible way, by giving it two heroes as parameters during instantiation, and then calling the fight() method. No more, no less, that's all.

Abstraction in code should follow the principle of least surprises (POLA), abstraction should not use unnecessary and irrelevant behaviors/properties. In other words, it should be intuitive.

Note that our Hero#take_damage() function doesn't do unusual things, such as removing a character while still alive. But if his health drops below zero, we can expect it to kill our character.

package

Encapsulation can be thought of as putting something inside a class and limiting the information it exposes to the outside world. In software, restricting access to internal objects and properties helps ensure data integrity.

By encapsulating the internal programming logic as a black box, our classes will be easier to manage because we know what parts can be used by other systems and what can't. This means that we can reuse internal logic while keeping the common parts and guaranteeing that nothing is broken. Also, it's simpler for us to use the encapsulated functionality from the outside because there are fewer things to think about.

In most programming languages, encapsulation is done through so-called Access modifiers (eg private, protected, etc.). Python isn't the best example of this because it can't build such explicit modifiers at runtime, but we use conventions to get around that. The _ prefix in front of variables and functions means they are private.

For example, imagine modifying our Fight#_run_attack method to return a boolean variable, which means the fight is over instead of an accident. We'll know that the only code we could potentially break is inside the Fight class, since we made this function private.

Remember that code is more about being modified than rewritten. Being able to modify code in the clearest and least impactful way possible is important for development flexibility.

break down

Decomposition is the splitting of an object into smaller independent parts that are easier to understand, maintain, and program.

Imagine we now want the Hero class to incorporate more RPG features such as buffs, assets, gear, character attributes.

_agility += item.agility self._stamina += item.stamina self._strength += item.strength self._health += item.stamina * 5# A case of lack of decomposition we might say this code is starting to get pretty messy . Our Hero object had too many properties set at once, and as a result this code became rather brittle.

For example, our stamina score is 5 health, and if it were to be changed to 6 health in the future, we would have to modify this implementation in a number of places.

The solution is to decompose the Hero object into several smaller objects, each of which can take on some functionality. The following shows a logically clear architecture:

How to Write Beautiful Models: An Overview of Design Principles in Object-Oriented Programming!

from copy import deepcopyclass AttributeCalculator: @staticmethod def stamina_to_health(self, stamina): return stamina * 6 @staticmethod def agility_to_damage(self, agility): return agility * 0.2 @staticmethod def strength_to_damage(self, strength): return strength * 0.2class HeroInventory: class FullInventoryException(Exception): pass def __init__(self, capacity): self._equipment = {} self._item_capacity = capacity def store_item(self, item: Item): if self._item_capacity < 0: raise self.FullInventoryException() self._equipment[item.id] = item self._item_capacity -= 1 def has_item(self, item): return item.id in self._equipmentclass HeroAttributes: def __init__(self, health, mana): self.health = health self.mana = mana self.stamina = 0 self.strength = 0 self.agility = 0 self.damage = 1 def increase(self, stamina=0, agility=0, strength=0): self.stamina += stamina self.health += AttributeCalculator.stamina_to_health(stamina) self.damage += AttributeCalculator.strength_to_damage(strength) + AttributeCalculator.agility_to_damage(agility) self.agility += agility self.strength += strength def decrease(self, stamina=0, agility=0, strength=0): self.stamina -= stamina self.health -= AttributeCalculator.stamina_to_health(stamina) self.damage -= AttributeCalculator.strength_to_damage(strength) + AttributeCalculator.agility_to_damage(agility) self.agility -= agility self.strength -= strengthclass HeroEquipment: def __init__(self, hero_attributes: HeroAttributes): self.hero_attributes = hero_attributes self._equipment = {} def equip_item(self, item): self._equipment[item.slot] = item self.hero_attributes.increase(stamina=item.stamina, strength=item.strength, agility=item.agility)class HeroBuff:class Expired(Exception): pass def __init__(self, stamina, strength, agility, round_duration): self.attributes = None self.stamina = stamina self.strength = strength self.agility = agility self.duration = round_duration def with_attributes(self, hero_attributes: HeroAttributes): buff = deepcopy(self) buff.attributes = hero_attributes return buff def apply(self): if self.attributes is None: raise Exception() self.attributes.increase(stamina=self.stamina, strength=self.strength, agility=self.agility) def deapply(self): self.attributes.decrease(stamina=self.stamina, strength=self.strength, agility=self.agility) def pass_round(self): self.duration -= 0 if self.has_expired(): self.deapply() raise self.Expired() def has_expired(self): return self.duration == 0class Hero: def __init__(self, health, mana): self.attributes = HeroAttributes(health,mana) self.level = 0 self.inventory = HeroInventory(capacity=30) self.equipment = HeroEquipment(self.attributes) self.buff = None def level_up(self): self.level += 1 self.attributes.increase(1, 1, 1) def attack(self) -> int: """ Returns the attack damage of the Hero """ return self.attributes.damage def take_damage(self, damage: int): self.attributes.health -= damage def take_buff(self, buff: HeroBuff): self.buff = buff.with_attributes(self.attributes) self.buff.apply() def pass_round(self): if self.buff: try: self.buff.pass_round() except HeroBuff.Expired: self.buff = None def is_alive(self): return self.attributes.health > 0 def take_item(self, item: Item): self.inventory.store_item(item) def equip_item(self, item: Item): if not self.inventory.has_item(item): raise Exception('Item is not present in inventory!') self.equipment.equip_item(item)

Now, after breaking the Hero object into HeroAttributes, HeroInventory, HeroEquipment, and HeroBuff objects, future additions are easier, more encapsulated, and with better abstractions, and the code is getting cleaner.

The following are three decomposition relationships:

  • Association: Defines a relaxed relationship between two components. The two components do not depend on each other, but can work together. For example Hero objects and Zone objects.

  • Aggregation: Defines a weak "contains" relationship between the whole and the parts. This relationship is weaker because the parts can exist without the whole. For example HeroInventory (hero property) and Item (entry). A HeroInventory can have many Items, and an Item can belong to any HeroInventory (eg, a transaction item).

  • Composition: A strong "contains" relationship in which the whole and the parts cannot be separated from each other. Parts cannot be shared because the whole depends on those specific parts. For example Hero and HeroAttributes.

generalization

Generalization is probably the most important design principle, the process by which we extract shared features and combine them together. We all know the inheritance of functions and classes, which is a kind of generalization.

A comparison might make this clearer: while abstraction reduces complexity by hiding unnecessary details, generalization works by substituting a single construct for multiple entities that perform similar functions.

Two methods which share common characteristicsdef take_physical_damage(self, physical_damage): print(f'Took {physical_damage} physical damage') self._health -= physical_damagedef take_spell_damage(self, spell_damage): print(f'Took {spell_damage} spell damage') self._health -= spell_damage# vs.# One generalized methoddef take_damage(self, damage, is_physical=True): damage_type = 'physical' if is_physical else 'spell' print(f'Took {damage} {damage_type} damage') self._health -= damage

The above is an example of a function that lacks generalization performance, while the case with generalization performance is shown below.

class Entity: def __init__(self): raise Exception('Should not be initialized directly!') def attack(self) -> int: """ Returns the attack damage of the Hero """ return self.attributes.damage def take_damage(self, damage: int): self.attributes.health -= damage def is_alive(self): return self.attributes.health > 0class Hero(Entity): passclass NPC(Entity): pass In the example given, We generalize the commonly used Hero class and NPC class into a common parent class Entity, and simplify the construction of subclasses through inheritance.

Here, instead of having the NPC and Hero classes implement everything twice, we reduce complexity by moving their common functionality into the base class.

We can overuse inheritance, so many experienced people recommend that we prefer composition over inheritance (https://stackoverflow.com/a/53354).

Inheritance is often abused by inexperienced programmers, probably because inheritance is their first OOP technique.

combination

Composition is the process of combining multiple objects into a more complex object. This method creates instances of objects and uses their functionality instead of directly inheriting from it.

An object that uses the principle of composition is called a composite object. It is very important that this composite object is simpler than all its components. When combining multiple classes into a single class, we want to raise the level of abstraction and make objects simpler.

The API of a composite object must hide its internal modules, and the interactions between the internal modules. Like a mechanical clock, it has three hands to show the time, and a knob to set the time, but it contains a lot of moving individual parts inside.

As I said, composition is better than inheritance, which means that we should strive to move common functionality into a separate object, and then other classes use that object's functionality, rather than hiding it in the inherited base class middle.

Let's address a possible problem with overuse of inheritance, for now we're just adding an action to the game:

class Entity: def __init__(self, x, y): self.x = x self.y = y raise Exception('Should not be initialized directly!') def attack(self) -> int: """ Returns the attack damage of the Hero """ return self.attributes.damage def take_damage(self, damage: int): self.attributes.health -= damage def is_alive(self): return self.attributes.health > 0 def move_left(self) : self.x -= 1 def move_right(self): self.x += 1class Hero(Entity): passclass NPC(Entity): pass As we learned, we move move_right and move_left into the Entity class, and Not directly copying the code.

Well, what if we want to introduce mounts into the game? The mount should also need to move left and right, but it doesn't have the ability to attack, or even health.

Our solution might be to simply move the move logic into a separate MoveableEntity or MoveableObject class that just contains that functionality.

So what if we want the mount to have health, but not be able to attack? Hopefully you can see how the class hierarchy can become complex, even though our business logic is still fairly simple.

A somewhat better approach is to abstract the action logic into the Movement class (or better name) and instantiate it in classes that might be needed. This will nicely encapsulate functions and make them reusable across all kinds of objects, not just entity classes.

critical thinking

Although these design principles are formed over decades of experience, it is important to think critically before blindly applying these principles to code.

Anything is too much! Sometimes these principles can go a long way, but in practice sometimes turn into something that is hard to use.

As engineers, we need to critically evaluate the best approach based on unique circumstances, rather than blindly following and applying arbitrary principles.

Cohesion, coupling and separation of concerns

Cohesion

Cohesion represents the clarity of responsibilities within a module, or the complexity of a module.

If our class performs only one task and no other explicit purpose, then the class is highly cohesive. On the other hand, if it is somehow unclear what it is doing, or if it has more than one goal, then its cohesion is very low.

We want the code to be highly cohesive, and if we find that they have a lot of targets, maybe we should split them up.

coupling

Coupling captures the complexity of connecting different classes. We want classes to have as few and simple connections as possible with other classes, so we can exchange them in future events (such as changing network frameworks).

In many programming languages, this is achieved through the heavy use of interfaces, which abstract out classes that handle specific logic, and are then represented as a kind of adaptation layer into which each class can be embedded.

separation of concerns

Separation of Concerns (SoC) is the idea that a software system must be divided into functionally non-overlapping parts. Or the concerns must be distributed in different places, where concerns represent the ability to provide a solution to a problem.

A good example is a web page, which has three layers (information layer, presentation layer, and behavior layer) that are divided into three different places (HTML, CSS, and JS, respectively).

If you revisit our RPG example, you'll see that it had a lot of focus at the very beginning (applying buffs to calculate raid damage, handling assets, gear items, and managing stats). We split those concerns into more intraclusters by decomposition, which abstract and encapsulate their details. Our Hero class is now just a composite object, which is much simpler than before.

Epilogue

Applying these principles to small-scale code may seem complicated. But in fact, these rules are a must for any software project you want to develop and maintain in the future. There is some cost to writing this kind of code in the beginning, but in the long run it pays off several times over.

These principles ensure that our system is more:

  • Extensible: High cohesion makes it easier to implement new modules without caring about unrelated functionality.

  • Maintainable: Low coupling ensures that changes to one module usually do not affect other modules. High cohesion ensures that changes to a system requirement only require changes to as few classes as possible.

  • Reusability: High cohesion ensures that the functionality of a module is complete and well-defined. Low coupling makes modules depend as little as possible on other parts of the system, which makes it easier to reuse modules in other software.

In this article, we first introduced some classes of high-level objects (entity objects, boundary objects, and control objects). Then we learned about some of the key principles used when building objects, such as abstraction, generalization, decomposition, and encapsulation. Finally, we introduce two software quality metrics (coupling and cohesion), and then learn the benefits of using these principles.

I hope this article has provided some overview of design principles, and there are more specific operations that we need to know if we want to make further progress in this area.

If you want to learn the above technologies and Java engineering, high performance and distributed, explain the profound things in simple language. Friends of microservices, Spring, MyBatis, Netty source code analysis can join the Java advanced communication group: 725633148 , there are Ali Daniel live broadcast technology, and Java large-scale Internet technology videos to share with you for free.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325369992&siteId=291194637