Uma imagem deve ser capaz de se redimensionar no OOP?

9

Estou escrevendo um aplicativo que terá uma Imageentidade e já estou tendo problemas para decidir de quem é a responsabilidade de cada tarefa.

Primeiro eu tenho a Imageaula. Possui um caminho, largura e outros atributos.

Em seguida, criaram uma ImageRepositoryclasse, para obter imagens com um método simples e testados, por exemplo: findAllImagesWithoutThumbnail().

Mas agora eu também preciso ser capaz createThumbnail(). Quem deve lidar com isso? Eu estava pensando em ter uma ImageManagerclasse, que seria uma classe específica do aplicativo (também haveria um componente reutilizável de manipulação de imagem de terceiros, não estou reinventando a roda).

Ou talvez seja 0K para deixar o Imageredimensionamento em si? Ou deixar a ImageRepositorye ImageManagerser da mesma classe?

O que você acha?

ChocoDeveloper
fonte
O que o Image faz até agora?
Winston Ewert
Como você espera redimensionar as imagens? Você está apenas encolhendo imagens ou pode imaginar querer voltar ao tamanho original?
Gort the Robot
@WinstonEwert Gera dados como a resolução formatada (por exemplo: a string '1440x900'), URLs diferentes e permite modificar algumas estatísticas, como 'visualizações' ou 'votos'.
ChocoDeveloper
@StevenBurnap Considero a imagem original a coisa mais importante, só uso miniaturas para uma navegação mais rápida ou se o usuário deseja redimensionar para caber em sua área de trabalho ou algo assim, então elas são uma coisa separada.
ChocoDeveloper 31/08/2012
Eu tornaria a classe Image imutável, para que você não precise copiá-la defensivamente ao distribuí-la.
dan_waterworth

Respostas:

8

A pergunta feita é muito vaga para ter uma resposta real, pois realmente depende de como os Imageobjetos serão usados.

Se você estiver usando apenas a imagem em um tamanho e estiver redimensionando porque a imagem de origem é do tamanho errado, talvez seja melhor que o código de leitura faça o redimensionamento. Faça com que seu createImagemétodo tenha uma largura / altura e retorne a imagem redimensionada nessa largura / altura.

Se você precisar de vários tamanhos e se a memória não for uma preocupação, é melhor manter a imagem na memória como lida originalmente e redimensionar no momento da exibição. Nesse caso, existem alguns modelos diferentes que você pode usar. A imagem widthe heightseria corrigida, mas você teria um displaymétodo que assumisse uma posição e uma altura / largura de destino ou teria algum método usando uma largura / altura que retornasse um objeto a qualquer sistema de exibição que você estiver usando. A maioria das APIs de desenho que usei permitem especificar o tamanho do destino no momento da criação.

Se os requisitos causarem desenhos em tamanhos diferentes com frequência suficiente para que o desempenho seja uma preocupação, você pode ter um método que crie uma nova imagem de tamanho diferente com base no original. Uma alternativa seria fazer com que sua Imageclasse armazene em cache diferentes representações internamente, para que, na primeira vez em que você chame displaycom o tamanho da miniatura, ela seja redimensionada, na segunda vez que apenas desenhe a cópia em cache salva da última vez. Isso usa mais memória, mas é raro você ter mais do que alguns redimensionamentos comuns.

Outra alternativa é ter uma única Imageclasse que retém a imagem base e que essa classe contenha uma ou mais representações. Um em Imagesi não teria altura / largura. Em vez disso, começaria com um ImageRepresentationque tivesse altura e largura. Você desenharia essa representação. Para "redimensionar" uma imagem, solicite Imageuma representação com determinadas métricas de altura / largura. Isso faria com que ela contivesse essa nova representação, bem como a original. Isso oferece muito controle sobre exatamente o que está pendurado na memória, com o custo de complexidade extra.

Pessoalmente, não gosto de classes que contenham a palavra Managerporque "gerente" é uma palavra muito vaga que realmente não diz muito sobre exatamente o que a classe faz. Ele gerencia a vida útil do objeto? Ele fica entre o restante do aplicativo e o que ele gerencia?

Gort the Robot
fonte
"realmente depende de como os objetos de imagem serão usados." Essa afirmação não está realmente de acordo com a noção de encapsulamento e acoplamento flexível (embora possa estar levando o princípio um pouco longe demais neste caso). Um ponto de diferenciação melhor seria se o estado da imagem inclui significativamente o tamanho ou não.
Chris Bye
Parece que você está falando sobre um ponto de vista do aplicativo de edição de imagem. Não está totalmente claro se o OP queria isso ou não, hein?
andho
6

Mantenha as coisas simples, desde que haja apenas alguns requisitos, e melhore seu design quando necessário. Acho que na maioria dos casos do mundo real não há nada de errado em começar com um design como esse:

class Image
{
    public Image createThumbnail(int sizeX, int sizeY)
    {
         // ...
         // later delegate the actual resize operation to a separate component
    }
}

As coisas podem se tornar diferentes quando você precisa passar mais parâmetros createThumbnail()e esses parâmetros precisam ter uma vida útil própria. Por exemplo, vamos supor que você criará miniaturas para várias centenas de imagens, todas com algum tamanho de destino, algoritmo de redimensionamento ou qualidade. Isso significa que você pode mover createThumbnailpara outra classe, por exemplo, uma classe de gerente ou uma ImageResizerclasse, onde esses parâmetros são passados ​​pelo construtor e, portanto, estão vinculados à vida útil do ImageResizerobjeto.

Na verdade, começaria com a primeira abordagem e refatoraria mais tarde, quando realmente preciso.

Doc Brown
fonte
bem, se eu já estou delegando a chamada para um componente separado, por que não torná-la uma responsabilidade de uma ImageResizerclasse em primeiro lugar? Quando você delega uma ligação em vez de passar a responsabilidade para uma nova classe?
Songo
2
@Songo: 2 razões possíveis: (1) o código para delegar ao componente separado - talvez já existente - não é apenas uma linha única, talvez apenas uma sequência de 4 a 6 comandos, portanto, você precisa de um local para armazená-lo . (2) Açúcar sintático / facilidade de uso: será muito bonito escrever Image thumbnail = img.createThumbnail(x,y).
Doc Brown
+1 aah eu vejo. Obrigado pela explicação :)
Songo
4

Eu acho que teria que fazer parte da Imageclasse, pois o redimensionamento em uma classe externa exigiria que o redimensionador conhecesse a implementação do Image, violando o encapsulamento. Suponho que Imageseja uma classe base, e você acabará com subclasses separadas para tipos de imagem concretos (PNG, JPEG, SVG etc.). Portanto, você precisará ter classes de redimensionamento correspondentes ou um redimensionador genérico com uma switchdeclaração que seja redimensionada com base na classe de implementação - um cheiro de design clássico.

Uma abordagem pode ser fazer com que o Imageconstrutor pegue um recurso contendo os parâmetros de imagem e altura e largura e crie-se adequadamente. O redimensionamento pode ser tão simples quanto criar um novo objeto usando o recurso original (armazenado em cache na imagem) e os novos parâmetros de tamanho. Por exemplo, foo.createThumbnail()seria simplesmente return new Image(this.source, 250, 250). ( Imagesendo o tipo concreto foo, é claro). Isso mantém suas imagens imutáveis ​​e suas implementações privadas.

TMN
fonte
Gosto dessa solução, mas não entendo por que você diz que o redimensionador precisa conhecer a implementação interna do Image. Tudo o que precisa é a origem e as dimensões de destino.
ChocoDeveloper 31/08/2012
Embora eu ache que ter uma classe externa não faça sentido porque seria inesperado, ele não teria que violar o encapsulamento nem exigiria uma instrução switch () ao executar um redimensionamento. Por fim, uma imagem é uma matriz 2D de algo, e você pode redimensionar definitivamente as imagens, desde que a interface permita obter e definir pixels individuais.
Whatsisname
4

Sei que o POO envolve encapsular dados e comportamento juntos, mas não acho que seja uma boa ideia que uma imagem tenha a lógica de redimensionamento incorporada nesse caso, porque uma imagem não precisa saber como se redimensionar para ser uma imagem.

Uma miniatura é na verdade uma imagem diferente. Talvez você tenha uma estrutura de dados que mantenha o relacionamento entre uma Fotografia e sua Miniatura (ambas são Imagens).

Tento dividir meus programas em coisas (como Imagens, Fotografias, Miniaturas, etc.) e Serviços (como PhotographRepository, ThumbnailGenerator, etc.). Acerte suas estruturas de dados e defina os serviços que permitem criar, manipular, transformar, persistir e recuperar essas estruturas de dados. Eu não coloco mais comportamento em minhas estruturas de dados do que garantir que elas sejam criadas corretamente e usadas adequadamente.

Portanto, não, uma imagem não deve conter a lógica de como criar uma miniatura. Deve haver um serviço ThumbnailGenerator que tenha um método como:

Image GenerateThumbnailFrom(Image someImage);

Minha estrutura de dados maior pode ficar assim:

class Photograph : Image
{
    public Photograph(Image thumbnail)
    {
        if(thumbnail == null) throw new ArgumentNullException("thumbnail");
        this.Thumbnail = thumbnail;
    }

    public Image Thumbnail { get; private set; }
}

Claro que isso pode significar que você está fazendo um esforço que não deseja ao construir o objeto, então eu consideraria algo assim também:

class Photograph : Image
{
    private Image thumbnail = null;
    private readonly Func<Image,Image> generateThumbnail;

    public Photograph(Func<Image,Image> generateThumbnail)
    {
        this.generateThumbnail = generateThumbnail;
    }


    public Image Thumbnail
    {
        get
        {
            if(this.thumbnail == null)
            {
                this.thumbnail = this.generateThumbnail(this);
            }
            return this.thumbnail;
        }
    }
}

... no caso em que você deseja uma estrutura de dados com avaliação lenta. (Desculpe, não incluí minhas verificações nulas e não a tornei segura para threads, o que é algo que você deseja se estiver tentando imitar uma estrutura de dados imutável).

Como você pode ver, qualquer uma dessas classes está sendo criada por algum tipo de PhotographRepository, que provavelmente tem uma referência a um ThumbnailGenerator que foi obtido por injeção de dependência.

Scott Whitlock
fonte
Foi-me dito que não deveria criar classes sem nenhum comportamento. Não tenho certeza se, quando você diz 'estruturas de dados', está se referindo às classes ou algo do C ++ (essa linguagem é essa?). As únicas estruturas de dados que conheço e uso são as primitivas. Os serviços e a parte de DI estão no local, eu posso acabar fazendo isso.
ChocoDeveloper 31/08/2012
@ChocoDeveloper: ocasionalmente classes sem comportamento são úteis ou necessárias, dependendo da situação. Essas são chamadas de classes de valor . Classes normais de POO são classes com comportamento codificado. As classes compostas de OOP também têm comportamento codificado, mas sua estrutura de composição pode dar origem a muitos comportamentos necessários para um aplicativo de software.
rwong 01/09/12
3

Você identificou uma única funcionalidade que deseja implementar. Por que não deveria estar separada de tudo o que identificou até agora? É isso que o Princípio da Responsabilidade Única sugere que é a solução.

Crie uma IImageResizerinterface que permita passar uma imagem e um tamanho de destino e que retorne uma nova imagem. Em seguida, crie uma implementação dessa interface. Na verdade, existem várias maneiras de redimensionar imagens, para que você possa acabar com mais de uma!

Chris Pitman
fonte
+1 para SRP relevante, mas não estou implementando o redimensionamento real, que já foi delegado a uma biblioteca de terceiros, conforme indicado.
ChocoDeveloper 31/08/2012
3

Eu assumo esses fatos sobre o método, que redimensiona imagens:

  • Ele deve retornar uma nova cópia da imagem. Você não pode modificar a imagem propriamente dita, porque ela quebraria outro código que tem referência a esta imagem.
  • Não precisa acessar os dados internos da classe Image. As classes de imagem geralmente precisam oferecer acesso público a esses dados (ou cópia de).
  • O redimensionamento da imagem é complexo e requer muitos parâmetros diferentes. Talvez até pontos de extensibilidade para diferentes algoritmos de redimensionamento. Passar tudo resultaria em uma grande assinatura de método.

Com base nesses fatos, eu diria que não há razão para o método de redimensionamento de imagem fazer parte da própria classe de imagem. Implementá-lo como método de classe de auxiliar estático seria o melhor.

Eufórico
fonte
Boas suposições. Não sei por que deveria ser um método estático, no entanto, tento evitá-los para testabilidade.
ChocoDeveloper 31/08/2012
2

Uma classe de Processamento de imagem pode ser apropriada (ou Image Manager, como você a chamou). Passe sua imagem para um método CreateThumbnail do processador de imagens, por exemplo, para recuperar uma imagem em miniatura.

Uma das razões pelas quais eu sugeriria essa rota é que você diz estar usando uma biblioteca de processamento de imagens de terceiros. Remover a funcionalidade de redimensionamento da própria classe Image pode facilitar o isolamento de qualquer código específico de plataforma ou de terceiros. Portanto, se você puder usar sua classe Image básica em todas as plataformas / aplicativos, não precisará poluí-la com código específico de plataforma ou biblioteca. Tudo isso pode estar localizado no processador de imagens.

GrandmasterB
fonte
Bom ponto. A maioria das pessoas aqui não entendeu que eu já estou delegando a parte mais complicada para uma biblioteca de terceiros, talvez eu não tenha sido suficientemente clara.
ChocoDeveloper 31/08/2012
2

Basicamente, como Doc Brown já disse:

Crie um getAsThumbnail()método para a classe de imagem, mas esse método deve apenas delegar o trabalho a alguma ImageUtilsclasse. Portanto, seria algo como isto:

 class Image{
   // ...
   public Thumbnail getAsThumbnail{
     return ImageUtils.convertToThumbnail(this);
   }
   // ...
 }

E

 class ImageUtils{
   // ...
   public static Thumbnail convertToThumbnail(Image i){
     // ...
   }
   // ...
 }

Isso permitirá um código mais fácil de olhar. Compare o seguinte:

Image i = ...
someComponent.setThumbnail(i.getAsThumbnail());

Ou

Image i = ...
Thumbnail t = ImageUtils.convertToThumbnail(i);
someComponent.setThumbnail(t); 

Se o último parecer bom para você, você também pode continuar criando esse método auxiliar em algum lugar.

Deiwin
fonte
1

Eu acho que no "Domínio da Imagem", você apenas tem o objeto Imagem que é imutável e monádico. Você solicita à imagem uma versão redimensionada e ela retorna uma versão redimensionada de si mesma. Depois, você pode decidir se deseja se livrar do original ou manter os dois.

Agora, as versões em miniatura, avatar etc. da imagem são outro domínio inteiramente, que pode solicitar ao domínio da imagem versões diferentes de uma determinada imagem para fornecer a um usuário. Normalmente, esse domínio também não é tão grande ou genérico; portanto, você provavelmente pode manter isso na lógica do aplicativo.

Em um aplicativo de pequena escala, eu redimensionava as imagens em tempo de leitura. Por exemplo, eu poderia ter uma regra de reescrita do apache que delega para php um script se a imagem 'http://my.site.com/images/thumbnails/image1.png', onde o arquivo será recuperado usando o nome image1.png e redimensionado e armazenado em 'thumbnails / image1.png'. Então, na próxima solicitação para essa mesma imagem, o apache exibirá a imagem diretamente, sem executar o script php. Sua pergunta sobre findAllImagesWithoutThumbnails é respondida automaticamente pelo apache, a menos que você precise fazer estatísticas?

Em um aplicativo de grande escala, eu enviava todas as novas imagens para um trabalho em segundo plano, que cuida de gerar as diferentes versões da imagem e as salva nos locais apropriados. Eu não me incomodaria em criar um domínio ou uma classe inteira, pois é improvável que esse domínio se transforme em uma terrível bagunça de espaguete e molho ruim.

andho
fonte
0

Resposta curta:

Minha recomendação é adicionar esses métodos à classe de imagem:

public Image getResizedVersion(int width, int height);
public Image getResizedVersion(double percentage);

O objeto Image ainda é imutável, esses métodos retornam uma nova imagem.

Tulains Córdova
fonte
0

Já existem algumas ótimas respostas, então irei elaborar um pouco sobre uma heurística por trás do caminho para identificar objetos e suas responsabilidades.

OOP difere da vida real, pois os objetos na vida real geralmente são passivos e, na OOP, eles são ativos. E esse é o núcleo do pensamento sobre objetos . Por exemplo, quem redimensionaria uma imagem na vida real? Um humano, que é inteligente a esse respeito. Mas no POO não há humanos; portanto, os objetos são inteligentes. Uma maneira de implementar essa abordagem "centrada no ser humano" no OOP é utilizar classes de serviço, por exemplo, Managerclasses notórias . Assim, os objetos são tratados como dados passivos. Não é uma maneira de POO.

Portanto, existem duas opções. O primeiro, criando um método Image::createThumbnail(), já é considerado. O segundo é criar uma ResizedImageclasse. Pode ser um decorador de um Image(depende do seu domínio preservar Imageou não uma interface), embora isso resulte em alguns problemas de encapsulamento, pois ResizedImageprecisaria ter a Imagefonte de uma. Mas um Imagenão seria sobrecarregado com detalhes de redimensionamento, deixando-o em um objeto de domínio separado, agindo de acordo com um SRP.

Vadim Samokhin
fonte