Някои добри практики и обектно-ориентиран дизайн

„ Програмиране с Python“, ФМИ

02.06.2011

Disclaimer

Писането на код като работа

Когато пишете код, рядко го правите самоцелно. Вместо това, вие опитвате се да решите някакъв реален проблем със собствена логика, терминология и особености. Винаги когато пишете код трябва да се стараете той да отразява много добре този проблем - това го прави много по-четим, по-лесен за поддръжка и по-разбираем от външни хора. Още повече, така вие ясно показвате намерението което вашия код има, вместо да карате читателя да задълбава в особенностите на вашата реализация.

Първо правило: Добри имена на променливи

Променливите обикновенно отговарят за съществуващи обекти/концепции в реалния проблем, който решавате. Това ги прави идеални за комуникиране на идеята на кода. За целта, обаче, се налага да избирате смислени имена.

Типична грешка

# Грешно
temp = sqrt(b ** 2 - 4 * a * c)
x1 = (-b + temp) / (2 * a)
x2 = (-b - temp) / (2 * a)

# По-правилно
discriminant = sqrt(b ** 2 - 4 * a * c)
x1 = (-b + discriminant) / (2 * a)
x2 = (-b - discriminant) / (2 * a)

Лоши имена…

old = readOld()
tupple = getValues("c:/")
tup = {}
for t in tupple:
    if old[t] != tupple[t]: continue
    tup.update({t:tupple[t]})

show(tup)
save(tupple)

…и добри имена

oldHashsums = readCachedHashsums()
newHashsums = findHashsums('c:/')

changedFiles = {}
for filename in oldHashsums:
    if oldHashsums[filename] != newHashsums[filename]:
        changedFiles[filename] = newHashsums[filename]

reportChanges(changedFiles)
saveHashsums(newHashsums)

Функции / методи / рутини

Рутините са едно от най-често използваните средства в програмирането. И все пак, причините за които има смисъл да създавате рутина са.

Именуване на рутини

При именуване на рутини се съобразявайте внимателно със следните неща.

Кохезия

„Кохезията“ на една рутина смътно описва действието й. Като говорим за „добра кохезия“ имаме предвид, че една рутина прави едно единствено нещо и го прави добре. Най-силния вид „кохезия“ е функционалната. Други видове са:

Неприемлив вид кохезия

Аргументи на рутините

Действието на една рутина не трябва да зависи от стойностите на неин аргумент.

Реакция при грешни параметри на рутината

Лош подход.

def storePerson(name, age, address):
	if not isinsntace(name, str) or len(name) < 6: 
	    raise ValueError("Illegal name")
	if not isinstance(age, int, long) or age < 0:
	    raise ArithmeticError("Illegal age")
	if not hasattr(address, "street") or not hassattr(address, "number"):
	    raise ValueError("Illegal address")

	storeData(name, age, address.street, address.number)

Реакция при грешни параметри на рутината (2)

По-добър подход.

def storePerson(name, age, address):
    assert long(age) >= 6
    assert isinstance(name, str) and len(name) >= 6
		
    storeData(name, age, address.street, address.number)

Не ползвайте глобални променливи

Когато пиеше функции, не ползвайте глобални променливи. Ама въобще. Най-честия случай на неправилно ползване на глобавни променливи е когато се употребяват за комуникация между функции. Никога не правете това. В рядките случаи, в които имате нужда от „глобални“ обекти правете Singleton-и или thread.local-и.

Не ползвайте goto

В Python няма goto. Ако случайно пишете на друг език, в който има goto, това правило остава - не ползвайте goto.

Не използвайте глупави низове в
съобщенията за грешка

Abstract Data Types (ADT)

Обектно-ориентираното програмиране ни позволява да моделираме концепции от външния свят в структури, разбираеми за машината. Това се нарича създаване на абстрактни типове на данни.

class Polynomial(object)

class Polynomial(object):
    def __init__(self, *cfs): ...
    def order(self): ...
    ...

a = Polynomial(1, 4, 2) # x^2 + 4x + 2
print a[3], a[2], a[1], a[0] # 0 1 4 2
print a.order() # 2
b = Polynomial(1, 1, 4, 1) # x^3 + x^2 + 4x + 1
c = b - a # x^3 - 1
d = Polynomial(1, 0, 0, 1) # x^3 + 1
e = c * d # x^6 - 1

Състояние и функционалност

Всеки обект може да се разглежда като сбор от две неща:

Интерфейс и имплементация

Интерфейс:

y = power(x, a)

Имплементация:

def power(x, a): return 1 if a == 0 else power(x, a - 1) * x


def power(x, a):
    if a == 0: return 1
    r = power(x, a/2)
    return x * x * (x if a % 2 else 1)

def power(x, a): return reduce(lambda a, b: a * b, [x] * a)


def power(x, a):
    r = 1
    for _ in range(a): r *= x
    return x

def power(x, a): return x ** a

Представяне

Има различни начини, по които може да представим абстрактната концепция в примитивите на езика.

(1, 2, 3)                 -> x^2 + 2x + 3
(3, 1)                    -> 3x + 1

((1, 2), (2, 1), (3, 0))  -> x^2 + 2x + 3
((1, 1), (2, 1), (1, 0))  -> 3x + 1

{2: 1, 1: 2, 0: 3}        -> x^2 + 2x + 3
{1: 3, 0: 1}              -> 3x + 1

Инвариант на представянето

Обикновено има разлика между множеството от стойности, които структурата на представянето може да приеме и множеството от стойности, които имат смисъл. Последните се наричат инвариант на представянето (representation invariant)

Например ако моделирате интервал от цели числа, може да го представите вътрешно като наредена двойка (лява граница, дясна граница). Съответно, (2, 6) е смислено представяне, но (6, 2) - не.

Абстрактна функция

Функцията (в математически смисъл) съпоставяща инварианта на представянето на множеството от моделирани концепции се нарича абстрактна функция на този абстрактен тип.

Състоянието е зло

Като Дарт Вейдър, само дето накрая убива Люк, а не императора.

Мутатори и наблюдатели

Методите на един обект могат да се разделят на два вида:

Mutable и immutable обекти

Еквивалентност

Имате два обекта. Те са равни ако…

Observational equivalence

…с произволна поредица от observer методи не може да разберете дали те са различни или не.

Behavioral equivalence

…с произволна поредица от observer-и и мутатори не може да разберете дали те са различни или не.

Design by Contract

За всеки метод се дефинира следното:

Наследяването е зло

Кой кого?

class Rectangle:
	def a(self): ...
	def b(self): ...
	def setA(self, a): ...
	def setB(self, b): ...
	
class Square(self):
	def a(self): ...
	def setSide(self, side): ...

Liskov's Substitution Principle

Клас Б може да наследи от клас А, само ако на всички места на които може да използвате инстанция на А може да използвате инстанция на Б.

Liskov's Substitution Principle (2)

В термините на Design by Contract:

Б може да наследи А ако:

Law of Demeter

Един метод може да праща съобщения на:

или:

**Не говори с непознати**

Design Patterns

The second coming of Jesus.

Има смисъл в Python

Още думички

Искате още?

Още въпроси?