Решение на Трета задача от Димитър Димитров

Обратно към всички решения

Към профила на Димитър Димитров

Резултати

  • 6 точки от тестове
  • 0 бонус точки
  • 6 точки общо
  • 69 успешни тест(а)
  • 0 неуспешни тест(а)

Код

module Graphics
class Point
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def rasterize_on(canvas)
canvas.set_pixel x, y
end
def eql?(other_point)
x == other_point.x and y == other_point.y
end
alias == eql?
def hash
[x, y].hash
end
end
class Line
attr_reader :from, :to
def initialize(from, to)
if from.x > to.x or (from.x == to.x and from.y > to.y)
@from = to
@to = from
else
@from = from
@to = to
end
end
def rasterize_on(canvas)
BresenhamRasterization.new(from.x, from.y, to.x, to.y).rasterize_on(canvas)
end
def eql?(other_line)
from == other_line.from and to == other_line.to
end
alias == eql?
def hash
[from.hash, to.hash].hash
end
class BresenhamRasterization
def initialize(from_x, from_y, to_x, to_y)
@from_x, @from_y = from_x, from_y
@to_x, @to_y = to_x, to_y
end
def rasterize_on(canvas)
initialize_from_and_to_coordinates
rotate_coordinates_by_ninety_degrees if steep_slope?
swap_from_and_to if @drawing_from_x > @drawing_to_x
draw_line_pixels_on canvas
end
def steep_slope?
(@to_y - @from_y).abs > (@to_x - @from_x).abs
end
def initialize_from_and_to_coordinates
@drawing_from_x, @drawing_from_y = @from_x, @from_y
@drawing_to_x, @drawing_to_y = @to_x, @to_y
end
def rotate_coordinates_by_ninety_degrees
@drawing_from_x, @drawing_from_y = @drawing_from_y, @drawing_from_x
@drawing_to_x, @drawing_to_y = @drawing_to_y, @drawing_to_x
end
def swap_from_and_to
@drawing_from_x, @drawing_to_x = @drawing_to_x, @drawing_from_x
@drawing_from_y, @drawing_to_y = @drawing_to_y, @drawing_from_y
end
def error_delta
delta_x = @drawing_to_x - @drawing_from_x
delta_y = (@drawing_to_y - @drawing_from_y).abs
delta_y.to_f / delta_x
end
def vertical_drawing_direction
@drawing_from_y < @drawing_to_y ? 1 : -1
end
def draw_line_pixels_on(canvas)
@error = 0.0
@y = @drawing_from_y
@drawing_from_x.upto(@drawing_to_x).each do |x|
set_pixel_on canvas, x, @y
calculate_next_y_approximation
end
end
def calculate_next_y_approximation
@error += error_delta
if @error >= 0.5
@error -= 1.0
@y += vertical_drawing_direction
end
end
def set_pixel_on(canvas, x, y)
if steep_slope?
canvas.set_pixel y, x
else
canvas.set_pixel x, y
end
end
end
end
class Rectangle
attr_reader :left, :right
def initialize(left, right)
if left.x > right.x or (left.x == right.x and left.y > right.y)
@left = right
@right = left
else
@left = left
@right = right
end
end
def rasterize_on(canvas)
[
Line.new(top_left, top_right),
Line.new(top_right, bottom_right),
Line.new(bottom_right, bottom_left),
Line.new(bottom_left, top_left),
].each { |line| line.rasterize_on canvas }
end
def top_left
Point.new left.x, [left.y, right.y].min
end
def top_right
Point.new right.x, [left.y, right.y].min
end
def bottom_right
Point.new right.x, [left.y, right.y].max
end
def bottom_left
Point.new left.x, [left.y, right.y].max
end
def eql?(other)
top_left == other.top_left and bottom_right == other.bottom_right
end
alias == eql?
def hash
[top_left, bottom_right].hash
end
end
class Canvas
attr_reader :width, :height
def initialize(width, height)
@width = width
@height = height
@pixels = {}
end
def set_pixel(x, y)
@pixels[[x, y]] = true
end
def draw(shape)
shape.rasterize_on(self)
end
def render_as(renderer)
renderer.new(self).render
end
def pixel_at?(x, y)
@pixels[[x, y]]
end
end
module Renderers
class Base
attr_reader :canvas
def initialize(canvas)
@canvas = canvas
end
def render
raise NotImplementedError
end
end
class Ascii < Base
def render
pixels = 0.upto(canvas.height.pred).map do |y|
0.upto(canvas.width.pred).map { |x| pixel_at(x, y) }
end
join_lines pixels.map { |line| join_pixels_in line }
end
private
def pixel_at(x, y)
canvas.pixel_at?(x, y) ? full_pixel : blank_pixel
end
def full_pixel
'@'
end
def blank_pixel
'-'
end
def join_pixels_in(line)
line.join('')
end
def join_lines(lines)
lines.join("\n")
end
end
class Html < Ascii
TEMPLATE = '<!DOCTYPE html>
<html>
<head>
<title>Rendered Canvas</title>
<style type="text/css">
.canvas {
font-size: 1px;
line-height: 1px;
}
.canvas * {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 5px;
}
.canvas i {
background-color: #eee;
}
.canvas b {
background-color: #333;
}
</style>
</head>
<body>
<div class="canvas">
%s
</div>
</body>
</html>
'.freeze
def render
TEMPLATE % super
end
private
def full_pixel
'<b></b>'
end
def blank_pixel
'<i></i>'
end
def join_lines(lines)
lines.join('<br>')
end
end
end
end

Лог от изпълнението

.....................................................................

Finished in 0.08742 seconds
69 examples, 0 failures

История (3 версии и 6 коментара)

Димитър обнови решението на 14.12.2013 19:41 (преди почти 11 години)

+xys

Димитър обнови решението на 14.12.2013 19:42 (преди почти 11 години)

-xys
+module Graphics
+ class Point
+ attr_reader :x, :y
+
+ def initialize(x, y)
+ @x = x
+ @y = y
+ end
+
+ def rasterize_on(canvas)
+ canvas.set_pixel x, y
+ end
+
+ def eql?(other_point)
+ x == other_point.x and y == other_point.y
+ end
+
+ alias == eql?
+
+ def hash
+ [x, y].hash
+ end
+ end
+
+ class Line
+ attr_reader :from, :to
+
+ def initialize(from, to)
+ if from.x > to.x
+ @from = to
+ @to = from
+ else
+ @from = from
+ @to = to
+ end
+ end
+
+ def rasterize_on(canvas)
+ BresenhamRasterization.new(from.x, from.y, to.x, to.y).rasterize_on(canvas)
+ end
+
+ def eql?(other_line)
+ from == other_line.from and to == other_line.to
+ end
+
+ alias == eql?
+
+ def hash
+ [from.hash, to.hash].hash
+ end
+
+ class BresenhamRasterization
+ def initialize(from_x, from_y, to_x, to_y)
+ @from_x, @from_y = from_x, from_y
+ @to_x, @to_y = to_x, to_y
+ end
+
+ def rasterize_on(canvas)
+ initialize_from_and_to_coordinates
+ rotate_coordinates_by_ninety_degrees if steep_slope?
+ swap_from_and_to if @drawing_from_x > @drawing_to_x
+
+ draw_line_pixels_on canvas
+ end
+
+ def steep_slope?
+ (@to_y - @from_y).abs > (@to_x - @from_x).abs
+ end
+
+ def initialize_from_and_to_coordinates
+ @drawing_from_x, @drawing_from_y = @from_x, @from_y
+ @drawing_to_x, @drawing_to_y = @to_x, @to_y
+ end
+
+ def rotate_coordinates_by_ninety_degrees
+ @drawing_from_x, @drawing_from_y = @drawing_from_y, @drawing_from_x
+ @drawing_to_x, @drawing_to_y = @drawing_to_y, @drawing_to_x
+ end
+
+ def swap_from_and_to
+ @drawing_from_x, @drawing_to_x = @drawing_to_x, @drawing_from_x
+ @drawing_from_y, @drawing_to_y = @drawing_to_y, @drawing_from_y
+ end
+
+ def error_delta
+ delta_x = @drawing_to_x - @drawing_from_x
+ delta_y = (@drawing_to_y - @drawing_from_y).abs
+
+ delta_y.to_f / delta_x
+ end
+
+ def vertical_drawing_direction
+ @drawing_from_y < @drawing_to_y ? 1 : -1
+ end
+
+ def draw_line_pixels_on(canvas)
+ @error = 0.0
+ @y = @drawing_from_y
+
+ @drawing_from_x.upto(@drawing_to_x).each do |x|
+ set_pixel_on canvas, x, @y
+ calculate_next_y_approximation
+ end
+ end
+
+ def calculate_next_y_approximation
+ @error += error_delta
+
+ if @error >= 0.5
+ @error -= 1.0
+ @y += vertical_drawing_direction
+ end
+ end
+
+ def set_pixel_on(canvas, x, y)
+ if steep_slope?
+ canvas.set_pixel y, x
+ else
+ canvas.set_pixel x, y
+ end
+ end
+ end
+ end
+
+ class Rectangle
+ attr_reader :left, :right
+
+ def initialize(left, right)
+ if left.x > right.x
+ @left = right
+ @right = left
+ else
+ @left = left
+ @right = right
+ end
+ end
+
+ def rasterize_on(canvas)
+ [
+ Line.new(top_left, top_right),
+ Line.new(top_right, bottom_right),
+ Line.new(bottom_right, bottom_left),
+ Line.new(bottom_left, top_left),
+ ].each { |line| line.rasterize_on canvas }
+ end
+
+ def top_left
+ Point.new left.x, [left.y, right.y].min
+ end
+
+ def top_right
+ Point.new right.x, [left.y, right.y].min
+ end
+
+ def bottom_right
+ Point.new right.x, [left.y, right.y].max
+ end
+
+ def bottom_left
+ Point.new left.x, [left.y, right.y].max
+ end
+
+ def eql?(other)
+ top_left == other.top_left and bottom_right == other.bottom_right
+ end
+
+ alias == eql?
+
+ def hash
+ [top_left, bottom_right].hash
+ end
+ end
+
+ class Canvas
+ attr_reader :width, :height
+
+ def initialize(width, height)
+ @width = width
+ @height = height
+ @pixels = {}
+ end
+
+ def set_pixel(x, y)
+ @pixels[[x, y]] = true
+ end
+
+ def draw(shape)
+ shape.rasterize_on(self)
+ end
+
+ def render_as(renderer)
+ renderer.new(self).render
+ end
+
+ def pixel_at?(x, y)
+ @pixels[[x, y]]
+ end
+ end
+
+ module Renderers
+ class Base
+ attr_reader :canvas
+
+ def initialize(canvas)
+ @canvas = canvas
+ end
+
+ def render
+ raise NotImplementedError
+ end
+ end
+
+ class Ascii < Base
+ def render
+ pixels = 0.upto(canvas.height.pred).map do |y|
+ 0.upto(canvas.width.pred).map { |x| pixel_at(x, y) }
+ end
+
+ join_lines pixels.map { |line| join_pixels_in line }
+ end
+
+ private
+
+ def pixel_at(x, y)
+ canvas.pixel_at?(x, y) ? full_pixel : blank_pixel
+ end
+
+ def full_pixel
+ '@'
+ end
+
+ def blank_pixel
+ '-'
+ end
+
+ def join_pixels_in(line)
+ line.join('')
+ end
+
+ def join_lines(lines)
+ lines.join("\n")
+ end
+ end
+
+ class Html < Ascii
+ TEMPLATE = '<!DOCTYPE html>
+ <html>
+ <head>
+ <title>Rendered Canvas</title>
+ <style type="text/css">
+ .canvas {
+ font-size: 1px;
+ line-height: 1px;
+ }
+ .canvas * {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 5px;
+ }
+ .canvas i {
+ background-color: #eee;
+ }
+ .canvas b {
+ background-color: #333;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="canvas">
+ %s
+ </div>
+ </body>
+ </html>
+ '.freeze
+
+ def render
+ TEMPLATE % super
+ end
+
+ private
+
+ def full_pixel
+ '<b></b>'
+ end
+
+ def blank_pixel
+ '<i></i>'
+ end
+
+ def join_lines(lines)
+ lines.join('<br>')
+ end
+ end
+ end
+end

Димитър обнови решението на 14.12.2013 23:18 (преди почти 11 години)

module Graphics
class Point
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def rasterize_on(canvas)
canvas.set_pixel x, y
end
def eql?(other_point)
x == other_point.x and y == other_point.y
end
alias == eql?
def hash
[x, y].hash
end
end
class Line
attr_reader :from, :to
def initialize(from, to)
- if from.x > to.x
+ if from.x > to.x or (from.x == to.x and from.y > to.y)
@from = to
@to = from
else
@from = from
@to = to
end
end
def rasterize_on(canvas)
BresenhamRasterization.new(from.x, from.y, to.x, to.y).rasterize_on(canvas)
end
def eql?(other_line)
from == other_line.from and to == other_line.to
end
alias == eql?
def hash
[from.hash, to.hash].hash
end
class BresenhamRasterization
def initialize(from_x, from_y, to_x, to_y)
@from_x, @from_y = from_x, from_y
@to_x, @to_y = to_x, to_y
end
def rasterize_on(canvas)
initialize_from_and_to_coordinates
rotate_coordinates_by_ninety_degrees if steep_slope?
swap_from_and_to if @drawing_from_x > @drawing_to_x
draw_line_pixels_on canvas
end
def steep_slope?
(@to_y - @from_y).abs > (@to_x - @from_x).abs
end
def initialize_from_and_to_coordinates
@drawing_from_x, @drawing_from_y = @from_x, @from_y
@drawing_to_x, @drawing_to_y = @to_x, @to_y
end
def rotate_coordinates_by_ninety_degrees
@drawing_from_x, @drawing_from_y = @drawing_from_y, @drawing_from_x
@drawing_to_x, @drawing_to_y = @drawing_to_y, @drawing_to_x
end
def swap_from_and_to
@drawing_from_x, @drawing_to_x = @drawing_to_x, @drawing_from_x
@drawing_from_y, @drawing_to_y = @drawing_to_y, @drawing_from_y
end
def error_delta
delta_x = @drawing_to_x - @drawing_from_x
delta_y = (@drawing_to_y - @drawing_from_y).abs
delta_y.to_f / delta_x
end
def vertical_drawing_direction
@drawing_from_y < @drawing_to_y ? 1 : -1
end
def draw_line_pixels_on(canvas)
@error = 0.0
@y = @drawing_from_y
@drawing_from_x.upto(@drawing_to_x).each do |x|
set_pixel_on canvas, x, @y
calculate_next_y_approximation
end
end
def calculate_next_y_approximation
@error += error_delta
if @error >= 0.5
@error -= 1.0
@y += vertical_drawing_direction
end
end
def set_pixel_on(canvas, x, y)
if steep_slope?
canvas.set_pixel y, x
else
canvas.set_pixel x, y
end
end
end
end
class Rectangle
attr_reader :left, :right
def initialize(left, right)
- if left.x > right.x
+ if left.x > right.x or (left.x == right.x and left.y > right.y)
@left = right
@right = left
else
@left = left
@right = right
end
end
def rasterize_on(canvas)
[
Line.new(top_left, top_right),
Line.new(top_right, bottom_right),
Line.new(bottom_right, bottom_left),
Line.new(bottom_left, top_left),
].each { |line| line.rasterize_on canvas }
end
def top_left
Point.new left.x, [left.y, right.y].min
end
def top_right
Point.new right.x, [left.y, right.y].min
end
def bottom_right
Point.new right.x, [left.y, right.y].max
end
def bottom_left
Point.new left.x, [left.y, right.y].max
end
def eql?(other)
top_left == other.top_left and bottom_right == other.bottom_right
end
alias == eql?
def hash
[top_left, bottom_right].hash
end
end
class Canvas
attr_reader :width, :height
def initialize(width, height)
@width = width
@height = height
@pixels = {}
end
def set_pixel(x, y)
@pixels[[x, y]] = true
end
def draw(shape)
shape.rasterize_on(self)
end
def render_as(renderer)
renderer.new(self).render
end
def pixel_at?(x, y)
@pixels[[x, y]]
end
end
module Renderers
class Base
attr_reader :canvas
def initialize(canvas)
@canvas = canvas
end
def render
raise NotImplementedError
end
end
class Ascii < Base
def render
pixels = 0.upto(canvas.height.pred).map do |y|
0.upto(canvas.width.pred).map { |x| pixel_at(x, y) }
end
join_lines pixels.map { |line| join_pixels_in line }
end
private
def pixel_at(x, y)
canvas.pixel_at?(x, y) ? full_pixel : blank_pixel
end
def full_pixel
'@'
end
def blank_pixel
'-'
end
def join_pixels_in(line)
line.join('')
end
def join_lines(lines)
lines.join("\n")
end
end
class Html < Ascii
TEMPLATE = '<!DOCTYPE html>
<html>
<head>
<title>Rendered Canvas</title>
<style type="text/css">
.canvas {
font-size: 1px;
line-height: 1px;
}
.canvas * {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 5px;
}
.canvas i {
background-color: #eee;
}
.canvas b {
background-color: #333;
}
</style>
</head>
<body>
<div class="canvas">
%s
</div>
</body>
</html>
'.freeze
def render
TEMPLATE % super
end
private
def full_pixel
'<b></b>'
end
def blank_pixel
'<i></i>'
end
def join_lines(lines)
lines.join('<br>')
end
end
end
end

Защо логиката по рисуването е изнесена в класовете, които ще се рисуват? Не е ли по-добре всеки от тях да дава свое консистентно представяне (списък от всичките си точки / (х, у) координати), което да се ползва от Canvas? Например ако в момента решим, че центърът на координатната система трябва да е долу ляво, а не горе ляво трябва да променим или всички 'форми', или всички renderer-и.

Защо Html < Ascii? Не можеше ли render логиката да бъде в Base или модул, който да се include-ва?

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

Относно Html < Ascii, както и сам си си отговорил, наследявам, за да преизползвам логиката от Ascii. Дали не може тази логика да е в Base – да, би могло. Така или иначе, в момента няма много смисъл от Base класа, а ако преместя render там, ще има повече смисъл. Въпросът е, обаче, че тази логика на обхождане и рендериране е специфична по-скоро за ASCII-варианти на рендеринг, без значение дали са форми на ASCII art или HTML и мястото ѝ не е точно в Base, ако бихме имали и други renderer-и, примерно някакъв curses-базиран, или пък някакъв реално графичен рендерер, или да кажем, някакъв SVG такъв. Да е в модул, който да се include-ва не смятам, че е добра идея. Това рядко е добра идея.

Логиката за растеризация смятам, че трябва да се намира в обекта "форма". Къде би била тази логика, иначе? Пак в някакви други обекти, защото не е малка. Не смятам, че паното трябва да се занимава с това. То просто носи знание за пиксели и толкова. Нещо като виртуална координатна система. Как се рендерира това пано, е задача на рендерерите. А как се растеризира геометрична форма на паното – задача на формите. Така съм разпределил отговорностите аз. Разбира се, моят избор има своите недостатъци, но не съм намерил друго по-подходящо решение.

Във връзка с проблема, който посочваш – "ако сменим координатната система...", не се сещам за решение, което би изисквало да променим по-малко неща. А и спекулативният дизайн не е добра идея. Тоест, неща тип "ако занапред стане така, че" не трябва да е нещо, на което слагаш голяма тежест, когато си правиш дизайна. Имаш конкретни изисквания сега и се съобразяваш с тях. Ако се променят занапред, тогава ще си промениш дизайна по съответния начин, но не предварително, защото вероятността да се променят точно така, както си представяш, е много малка. Да, вероятно ще има промени, но не и такива, каквито ти би очаквал.

Не успявам да видя и ползата от това всяка форма да си носи отговорността за растеризация, но да експортира само "списък от точки", защото не виждам смисъл в тези висящи в нищото точки. От тях има смисъл само ако са в контекста на координатна система, тоест пано.

Защо тогава нямаме клас, който да наследява Base, от който Ascii и Html да наследяват?

Съгласен съм, че растеризирането трябва да е във 'формите'. Но мисля, че изобразяването върху паното трябва да става в паното. Или може би в клас, от който наследяват всички форми. Решението на проблема с огледално рисуване ще се състои само в добавянето на 1 метод в Canvas, при представянето със списък от точки. Не мисля, че е чак такава спекулация - звучи нормално да искаме към програмата ни за рисуване да може да се добавят нови универсални за всички фигури операции (огледален образ, въртене, запълване, т.н.).

Не помня на лекции да сме говорили за това кога е по-добре да include-ваме модули и кога да наследяваме класове. Може ли да обясниш?

Защо тогава нямаме клас, който да наследява Base, от който Ascii и Html да наследяват?

Може, има някаква логика в това, но смятам, че са твърде много абстракции, излишно много за тази проста задача. Тук по-скоро бих махнал класа Base и бих оставил Html < Ascii, отколкото да добавя още един клас.

Друг вариант би бил Base да се казва Text или нещо подобно и да носи общата логика и за двата рендерера, като Ascii < Text и Html < Text. Така е малко по-подредено и е на ръба на позволеното ниво от абстракции за моделиране на този проблем :)

Съгласен съм, че растеризирането трябва да е във 'формите'. Но мисля, че изобразяването върху паното трябва да става в паното. Или може би в клас, от който наследяват всички форми.

Не мога да си представя това така, че да има смисъл от него. Ако напишеш конкретен примерен код и го споделиш като Gist, може да го обсъдим.

Решението на проблема с огледално рисуване ще се състои само в добавянето на 1 метод в Canvas, при представянето със списък от точки. Не мисля, че е чак такава спекулация - звучи нормално да искаме към програмата ни за рисуване да може да се добавят нови универсални за всички фигури операции (огледален образ, въртене, запълване, т.н.).

Сега мисля, че те разбирам. В предния си коментар нарекох спекулация очакването, че някой може да реши, че вместо горе вляво, началото на нашата КС да стане долу вляво, например. Това е спекулация за промяна в базовите изисквания. Но е спекулация.

Ако имаш предвид, че някой може да иска да рисува огледален образ, примерно (хоризонтален или вертикален), това е друго. Това е вече фийчър, може би полезен/интересен. Нормално е да си мислиш за такива фийчъри, неизбежно дори. Но, въпреки това, също е спекулация, макар и от различен тип. Няма го и в условието. Ако ти отговаряше за дизайна на библиотеката, може би щеше да искаш да вкараш такава функционалност. Това си е твое решение, като тук трябва да се пазиш от scope creep – да не нарастне твърде много (излишно много) обема от функционалности, които се опитваш да покриеш с кода си. Когато не отговаряш ти за изисквания дизайн (като фийчъри), какъвто е случаят тук и какъвто би бил в някакъв проект с клиент, да си мислиш такива неща е спекулация. Грешно е да си базираш дизайна на неслучили се изисквания.

Но отново, ако напишеш конкретен опростен примерен код, илюстриращ как смяташ, че би се адресирал по-добре този проблем и го пуснеш в Gist, можем да го коментираме. Всичко друго, което си говорим без код, на сухо, също е спекулация :)

Но хайде, като за последно по темата, да спекулираме, че някой дойде и каже "искам да мога да рисувам огледално и да запълвам фигури". Това са два различни фийчъра. Само към първата част има много начини да се подходи и много въпроси. Огледално обръщане на цяло пано, или само на една фигура? Само линията има смисъл да се обръща огледално при нас, а и това не е особено интересно и може да се постигне на база на съществуващата линия, с проста трансформация. Може би метод в линия, връщащ нова линия със завъртяни координати. Рендерирането остава непроменено. Огледално пано? Това има повече смисъл, това бих го направил като метод в пано, който връща ново пано. Изобщо, много различни начини за имплементация, в зависимост от конкретните изисквания. Тъй като няма такива изисквания, само бих спекулирал и това би ми навредило на дизайна. Запълването на фигура е интересен проблем и към него също има много начини да се подходи, в зависимост какво е изискването. Може да е нов клас, FilledRectangle, например. Запълване на зона в пано (като инструмента "кофа" в MS Paint) е друга функционалност. Съвсем друга. За нея трябват други алгоритми и друг дизайн. Хиляди въпросителни, нула отговора = спекулация = лошо за дизайна.

Не помня на лекции да сме говорили за това кога е по-добре да include-ваме модули и кога да наследяваме класове. Може ли да обясниш?

Няма една универсална формула. Според случая. Като цяло, бих се стремял да избягвам твърде многото модули и функционалност в тях, защото по този начин това не е ОО-програмиране или ОО-дизайн, а са си функции, капсулирани в именувани пространства, тоест някаква форма на процедурно програмиране. Стремя се да мисля в контекста на обекти с отговорности и състояния. Много рядко се налага да имаш споделена функционалност тип "миксин", какъвто е, например, Enumerable в Ruby. Той е сравнително добър пример, но няма много други такива.

Друг вариант би бил Base да се казва Text или нещо подобно и да носи общата логика и за двата рендерера, като Ascii < Text и Html < Text.

Това ми беше първоначалната идея, просто не обърнах внимание на името на класа, който се наследява.

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

Това бях казал и осъзнавам, че този пример е глупав. Но като цяло идеята ми е, че далеч не е спекулация фактът, че за да стане използваема нашата програма трябва да добавим още много 'форми' и операции с тях (кой ще я ползва, ако няма базови неща като овал и запълване). Ако фигурите ни се самоначертават на паното имаме проблем като искаме да добавим нови операции. Това с огледалната линия, представена чрез друга линия, разчита на прекалено конкретни свойства на операцията (симетрия) и множеството ни от фигури (което се предполага, че ще се разширява много).

От друга страна можем безопасно да предположим, че операцията над фигури може да се извършва върху множеството от точки, което представя дадените фигури. Това не е задължително да е абсолютно вярно, но не мога да се сетя за контрапример, който не е изключително изкуствен. Sanity check: tool-овете във великия MS Paint :) не се интересуват какво сме нарисували, гледат само къде има пиксели. С други думи - ако искаме да добавим нова фигура, тя просто трябва да казва как може да се представи като списък от точки, а ако искаме да добавим нова операция над всички фигури просто добавяме операция, която се извършва над произволно множество от точки. Вече тази операция дали ще е във вътрешен клас за паното, или в Drawable/Shape (или някакво друго име за клас, от който всички фигури ще наследяват) е добър въпрос (може би по-добре второто), но не можем да се спасим от проблема, ако трябва да е във всяка отделна фигура.

Не съм сигурен какъв Gist искаш - ако просто да имплементирам огледален образ, въртене и запълване над произволно множество от точки не виждам какво ще покаже, освен че е възможно.

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

Само че, пак казвам, изискванията на задачата са други. Няма такива операции като изисквания и понеже задачата е с реално замразена спецификация – няма и да има. Само в контекста на задачата, това би била една излишна абстракция. Харесват ми дизайн-идеите, които имаш в посока развитие на функционалността, но са тема на отделна дискусия. Например, ако искаш да видиш дали би се получил добър дизайн, може да доразвиеш решението си, добавяйки тези растерни операции, за които си мислиш (огледални образи и прочее) с дизайна така, както си го представяш и може да споделиш кода, за да го обсъдим и да видим дали е станало добре. Това имах предвид, като казах "gist" – от самите идеи обсъдихме колкото можахме, от тук нататък единственият начин да продължим дискусията конструктивно, би било да обсъждаме реален код.

Нашата цел, решавайки задачи и предизвикателства, е да се опитаме да измислим най-оптималния – прост и изчистен – дизайн на даден проблем. Не така, както ни се иска на нас да бъде зададен, или както очакваме, че би се развил занапред, а така, както е зададен. Това понякога (може би даже често) е трудно и за мен.

Бих нарекъл това затруднение "the programmmer's cognitive bias". Свикваме да мислим за решението на даден проблем в контекста на код, дизайн и архитектура и пропускаме факта, че понякога по-малко код или дори никакъв код са всъщност най-подходящото решение. Имаме и естествена склонност към усложняване, без това да се изисква. Искаме полагайки основи, тези основи да могат да понесат 120-етажен небостъргач, който не се срутва при земетресения със сила 10+ по Рихтер, разбира се – кой не би искал това за неговата структура? И лесно пропускаме факта, че "клиентът" е искал просто баскетболно игрище...

Интересна дискуския се получи, благодаря ти за въпросите :)